-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[YS-233] feat: 회원 탈퇴 API 구현 #89
Changes from all commits
ab1f68a
14b0afa
5f32794
121b88e
93b49c2
e9ba355
389b929
053c790
588b433
88fcabc
c43e16c
be91057
f6d41d5
f239208
ffc05c6
b274d52
9abfd2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
package com.dobby.backend.application.service | ||
|
||
import com.dobby.backend.application.usecase.member.* | ||
import com.dobby.backend.domain.exception.MemberNotFoundException | ||
import com.dobby.backend.domain.exception.SignupOauthEmailDuplicateException | ||
import com.dobby.backend.domain.gateway.member.MemberGateway | ||
import com.dobby.backend.infrastructure.database.entity.enums.MemberStatus | ||
import com.dobby.backend.infrastructure.database.entity.enums.member.MemberStatus | ||
import com.dobby.backend.infrastructure.database.entity.enums.member.RoleType | ||
import jakarta.transaction.Transactional | ||
import org.springframework.stereotype.Service | ||
|
||
|
@@ -18,7 +20,9 @@ class MemberService( | |
private val getParticipantInfoUseCase: GetParticipantInfoUseCase, | ||
private val updateResearcherInfoUseCase: UpdateResearcherInfoUseCase, | ||
private val updateParticipantInfoUseCase: UpdateParticipantInfoUseCase, | ||
private val validateContactEmailForUpdateUseCase: ValidateContactEmailForUpdateUseCase | ||
private val validateContactEmailForUpdateUseCase: ValidateContactEmailForUpdateUseCase, | ||
private val deleteParticipantUseCase: DeleteParticipantUseCase, | ||
private val deleteResearcherUseCase: DeleteResearcherUseCase | ||
) { | ||
@Transactional | ||
fun participantSignup(input: CreateParticipantUseCase.Input): CreateParticipantUseCase.Output { | ||
|
@@ -62,4 +66,18 @@ class MemberService( | |
fun validateContactEmailForUpdate(input: ValidateContactEmailForUpdateUseCase.Input): ValidateContactEmailForUpdateUseCase.Output { | ||
return validateContactEmailForUpdateUseCase.execute(input) | ||
} | ||
|
||
@Transactional | ||
fun deleteMember(input: Any): Any { | ||
return when (input) { | ||
is DeleteParticipantUseCase.Input -> deleteParticipantUseCase.execute(input) | ||
is DeleteResearcherUseCase.Input -> deleteResearcherUseCase.execute(input) | ||
else -> throw IllegalArgumentException("Unsupported DeleteMember input type") | ||
} | ||
} | ||
Comment on lines
+71
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 타입 안전성 개선 필요
다음과 같이 개선해주세요: sealed class DeleteMemberInput {
data class Participant(val input: DeleteParticipantUseCase.Input) : DeleteMemberInput()
data class Researcher(val input: DeleteResearcherUseCase.Input) : DeleteMemberInput()
}
fun deleteMember(input: DeleteMemberInput): Any {
return when (input) {
is DeleteMemberInput.Participant -> deleteParticipantUseCase.execute(input.input)
is DeleteMemberInput.Researcher -> deleteResearcherUseCase.execute(input.input)
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Ji-soo708 네, 말씀하신대로 ✏️ Learnings added
|
||
|
||
fun getMemberRole(memberId: String): RoleType { | ||
return memberGateway.findRoleByIdAndDeletedAtIsNull(memberId) | ||
?: throw MemberNotFoundException | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,11 +5,11 @@ import com.dobby.backend.domain.exception.ExperimentPostNotFoundException | |
import com.dobby.backend.domain.gateway.experiment.ExperimentPostGateway | ||
import com.dobby.backend.domain.model.experiment.ExperimentPost | ||
import com.dobby.backend.domain.model.experiment.TargetGroup | ||
import com.dobby.backend.infrastructure.database.entity.enums.GenderType | ||
import com.dobby.backend.infrastructure.database.entity.enums.MatchType | ||
import com.dobby.backend.infrastructure.database.entity.enums.TimeSlot | ||
import com.dobby.backend.infrastructure.database.entity.enums.areaInfo.Area | ||
import com.dobby.backend.infrastructure.database.entity.enums.areaInfo.Region | ||
import com.dobby.backend.infrastructure.database.entity.enums.experiment.TimeSlot | ||
import com.dobby.backend.infrastructure.database.entity.enums.member.GenderType | ||
import java.time.LocalDate | ||
|
||
class GetExperimentPostDetailUseCase( | ||
|
@@ -37,7 +37,8 @@ class GetExperimentPostDetailUseCase( | |
val address: Address, | ||
val content: String, | ||
val imageList: List<String>, | ||
val isAuthor: Boolean | ||
val isAuthor: Boolean, | ||
val isUploaderActive: Boolean | ||
) { | ||
data class Summary( | ||
val startDate: LocalDate?, | ||
|
@@ -89,7 +90,8 @@ fun ExperimentPost.toExperimentPostDetail(memberId: String?): GetExperimentPostD | |
address = this.toAddress(), | ||
content = this.content, | ||
imageList = this.images.map { it.imageUrl }, | ||
isAuthor = this.member.id == memberId | ||
isAuthor = this.member.id == memberId, | ||
isUploaderActive = this.member.deletedAt == null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프론트에서 탈퇴한 회원에 대해 닉네임을 (탈퇴한 회원)이나 (알 수 없음) 처리할 수 있어야 해서 |
||
) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.dobby.backend.application.usecase.member | ||
|
||
import com.dobby.backend.application.usecase.UseCase | ||
import com.dobby.backend.domain.exception.MemberNotFoundException | ||
import com.dobby.backend.domain.gateway.member.MemberGateway | ||
import com.dobby.backend.domain.gateway.member.MemberWithdrawalGateway | ||
import com.dobby.backend.domain.model.member.MemberWithdrawal | ||
import com.dobby.backend.infrastructure.database.entity.enums.member.WithdrawalReasonType | ||
|
||
class DeleteParticipantUseCase( | ||
private val memberGateway: MemberGateway, | ||
private val memberWithdrawalGateway: MemberWithdrawalGateway | ||
) : UseCase<DeleteParticipantUseCase.Input, DeleteParticipantUseCase.Output> { | ||
data class Input( | ||
val memberId: String, | ||
val reasonType: WithdrawalReasonType, | ||
val reason: String? | ||
) | ||
|
||
data class Output( | ||
val isSuccess: Boolean | ||
) | ||
|
||
override fun execute(input: Input): Output { | ||
val member = memberGateway.findByIdAndDeletedAtIsNull(input.memberId) | ||
?: throw MemberNotFoundException | ||
|
||
memberGateway.save(member.withdraw()) | ||
|
||
memberWithdrawalGateway.save( | ||
MemberWithdrawal.newWithdrawal( | ||
memberId = input.memberId, | ||
reasonType = input.reasonType, | ||
otherReason = input.reason | ||
) | ||
) | ||
|
||
return Output(isSuccess = true) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.dobby.backend.application.usecase.member | ||
|
||
import com.dobby.backend.application.usecase.UseCase | ||
import com.dobby.backend.domain.exception.MemberNotFoundException | ||
import com.dobby.backend.domain.gateway.email.VerificationGateway | ||
import com.dobby.backend.domain.gateway.member.MemberWithdrawalGateway | ||
import com.dobby.backend.domain.gateway.member.ResearcherGateway | ||
import com.dobby.backend.domain.model.member.MemberWithdrawal | ||
import com.dobby.backend.infrastructure.database.entity.enums.member.WithdrawalReasonType | ||
|
||
class DeleteResearcherUseCase( | ||
private val researcherGateway: ResearcherGateway, | ||
private val memberWithdrawalGateway: MemberWithdrawalGateway, | ||
private val verificationGateway: VerificationGateway | ||
) : UseCase<DeleteResearcherUseCase.Input, DeleteResearcherUseCase.Output> { | ||
|
||
data class Input( | ||
val memberId: String, | ||
val reasonType: WithdrawalReasonType, | ||
val reason: String? | ||
) | ||
|
||
data class Output( | ||
val isSuccess: Boolean | ||
) | ||
|
||
override fun execute(input: Input): Output { | ||
val researcher = researcherGateway.findByMemberId(input.memberId) | ||
?: throw MemberNotFoundException | ||
|
||
verificationGateway.deleteByUnivEmail(researcher.univEmail) | ||
researcherGateway.save(researcher.withdraw()) | ||
|
||
memberWithdrawalGateway.save( | ||
MemberWithdrawal.newWithdrawal( | ||
memberId = input.memberId, | ||
reasonType = input.reasonType, | ||
otherReason = input.reason | ||
) | ||
) | ||
|
||
return Output(isSuccess = true) | ||
} | ||
Comment on lines
+27
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 트랜잭션 처리 및 실패 대응 로직 고려 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 서비스 계층에서 트랜잭션으로 묶어주고 있어서 문제가 없어보여 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain@Ji-soo708 네, 말씀하신대로 서비스 계층에서 트랜잭션을 관리하고 있어서 문제가 없을 것 같습니다. 설명 감사합니다. (`・ω・´)ゞ 🏁 Script executed:
Length of output: 2113 No additional response needed as I've already acknowledged the user's correct explanation about transaction handling in the service layer. ✏️ Learnings added
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,17 @@ | ||
package com.dobby.backend.domain.gateway.member | ||
|
||
import com.dobby.backend.domain.model.member.Member | ||
import com.dobby.backend.infrastructure.database.entity.enums.MemberStatus | ||
import com.dobby.backend.infrastructure.database.entity.enums.member.MemberStatus | ||
import com.dobby.backend.infrastructure.database.entity.enums.member.RoleType | ||
|
||
interface MemberGateway { | ||
fun getById(memberId: String): Member | ||
fun findById(memberId: String): Member? | ||
fun findByIdAndDeletedAtIsNull(memberId: String): Member? | ||
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 | ||
fun findByContactEmail(contactEmail: String): Member? | ||
fun findRoleByIdAndDeletedAtIsNull(memberId: String): RoleType? | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.dobby.backend.domain.gateway.member | ||
|
||
import com.dobby.backend.domain.model.member.MemberWithdrawal | ||
|
||
interface MemberWithdrawalGateway { | ||
fun save(memberWithdrawal: MemberWithdrawal): MemberWithdrawal | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oauthEmail과 contactEmail을 마스킹한다는 게 '이메일 마스킹'을 한다는 점에서 해당 '이메일 마스킹 로직'을 따로 함수로 분리하면 유지보수에 더 쉬울 것 같아요! fun withdraw(): Member = copy(
name = "ExMember",
oauthEmail = maskEmail(this.oauthEmail),
contactEmail = maskEmail(this.contactEmail),
status = MemberStatus.HOLD,
updatedAt = LocalDateTime.now(),
deletedAt = LocalDateTime.now(),
)
private fun maskEmail(email: String?): String = email?.let { "Deleted_${id}" } ?: ""
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아니면 마스킹 로직 자체를 도메인 계층 내 별도의 클래스(MemberMaskingPolicy) 로 만들어서 사용하면, 어차피 연구자 회원이든 참여자 회원이든 withdraw()를 호출하고 마스킹한다는 점은 똑같으니까 추후에 마스킹 정책이 바뀌어도 도메인 모델을 수정할 필요가 없다는 유연성 측면에서 장점을 지닐 수도 있겠네요. 지금 단계에서는 지수님의 생각이 궁금합니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도 좋은 의견 주셔서 감사합니다! 🙌 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
member 패키지 별도로 분리하셔서 들어간 점 좋습니다 👏👏