Skip to content
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-68] feat: 연구자 회원가입 API 구현 #19

Merged
merged 26 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
593ca56
feat: implements participant signup
chock-cho Jan 3, 2025
399fcbb
test: add test codes for SignupMapper and ParticipantSignupUseCase
chock-cho Jan 3, 2025
44e0b73
fix: resolve duplicate requested authentication
chock-cho Jan 3, 2025
699fa1f
refact: move DB Transaction to seperate responsibility to satisfy clean
chock-cho Jan 3, 2025
6741665
refact: Applay automatic Component Scan for UseCase classes
chock-cho Jan 3, 2025
dcf3ac6
style: rename API endpoint signup to meet the convention rule
chock-cho Jan 3, 2025
af04963
refact: update annotations to validate DateTime Format
chock-cho Jan 3, 2025
81375b2
refact: rename mapper function to match its usecase
chock-cho Jan 3, 2025
9bee38c
fix: reflect updated codes to test code
chock-cho Jan 3, 2025
2af113f
feat: merge from dev branch
chock-cho Jan 4, 2025
e84a874
feat: implement researcher member signup
chock-cho Jan 4, 2025
66f6e27
test: update test codes for adjust additional logic
chock-cho Jan 4, 2025
6b8e640
test: add test codes for AuthenticationUtils
chock-cho Jan 4, 2025
83f3275
test: add test codes to UseCases and Service related to signup logic
chock-cho Jan 4, 2025
7016272
feat: add verification entity
chock-cho Jan 6, 2025
813436f
feat: implement Email Send Code and Verification using SMTP
chock-cho Jan 6, 2025
fe7229f
fix: add exception codes for Email Verification logic
chock-cho Jan 6, 2025
672a2e9
docs: add application-yml file to .gitignore
chock-cho Jan 6, 2025
7b2fd25
fix: resolve merge conflicts with dev branch
chock-cho Jan 6, 2025
faa6ca0
fix: resolve merge conflicts with dev branch
chock-cho Jan 6, 2025
e4659db
chore: move packages structure
chock-cho Jan 6, 2025
b88a38b
fix: adjust test codes for non-failed test configuration
chock-cho Jan 7, 2025
ab97b8c
test: add test codes for Email send verification logic
chock-cho Jan 7, 2025
6e9a2b8
feat: add email verification condition to researcherSignup
chock-cho Jan 7, 2025
ad9310c
fix: add test codes for SignupServiceTest to meet the code coverage
chock-cho Jan 7, 2025
9e7d3c6
refact: reflect updation from the code review
chock-cho Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing
@SpringBootApplication
@ConfigurationPropertiesScan
@EnableFeignClients
@EnableJpaAuditing
class DobbyBackendApplication

fun main(args: Array<String>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ object VerificationMapper {

fun toSendResDto() : EmailSendResponse{
return EmailSendResponse(
isSucceess = true,
isSuccess = true,
message = "해당 학교 이메일로 성공적으로 코드를 전송했습니다. 10분 이내로 인증을 완료해주세요."
)
}

fun toVerifyResDto() : EmailVerificationResponse {
return EmailVerificationResponse(
isSucceess = true,
isSuccess = true,
message = "학교 메일 인증이 완료되었습니다."
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class SignupService(
@Transactional
fun researcherSignup(input: ResearcherSignupRequest) : SignupResponse{
if(!input.emailVerified) {
println("Email verification failed: ${input.univEmail}")
throw EmailNotValidateException()
}
verifyResearcherEmailUseCase.execute(input.univEmail)
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 가지를 고려할 부분이 많아서 구현하기 힘들었을텐데 고생 많으셨습니다!
코드 잘 작성되었는데, 한 유즈케이스 안에 로직이 집중되다 보니 코드가 길어진 것 같습니다. 해당 유즈케이스에서 사용되는 private method는 EmailUtils와 같은 별도의 유틸리티 클래스로 분리해서 사용하는 것도 좋을 것 같습니다. 이를 통해 코드의 재사용성과 가독성이 향상될 것 같아요!

Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,35 @@ import com.dobby.backend.infrastructure.database.entity.enum.VerificationStatus
import com.dobby.backend.infrastructure.database.repository.VerificationRepository
import com.dobby.backend.presentation.api.dto.request.signup.EmailSendRequest
import com.dobby.backend.presentation.api.dto.response.signup.EmailSendResponse
import com.dobby.backend.util.EmailUtils
import java.time.LocalDateTime
import java.util.Hashtable
import javax.naming.directory.InitialDirContext
import javax.naming.directory.Attributes

class EmailCodeSendUseCase(
private val verificationRepository: VerificationRepository,
private val emailGateway: EmailGateway
) : UseCase<EmailSendRequest, EmailSendResponse> {
override fun execute(input: EmailSendRequest): EmailSendResponse {
if(!isDomainExists(input.univEmail)) throw EmailDomainNotFoundException()
if(!isUnivMail(input.univEmail)) throw EmailNotUnivException()
validateEmail(input.univEmail)

val code = EmailUtils.generateCode()
reflectVerification(input, code)

sendVerificationEmail(input, code)
return VerificationMapper.toSendResDto()
}

private fun validateEmail(email : String){
if(!EmailUtils.isDomainExists(email)) throw EmailDomainNotFoundException()
if(!EmailUtils.isUnivMail(email)) throw EmailNotUnivException()
}

private fun reflectVerification(input: EmailSendRequest, code: String) {
val existingInfo = verificationRepository.findByUnivMail(input.univEmail)
val code = generateCode()

if(existingInfo != null) {
if (existingInfo != null) {
when (existingInfo.status) {
VerificationStatus.HOLD -> {
existingInfo.verificationCode = code
existingInfo.status = VerificationStatus.HOLD
existingInfo.expiresAt = LocalDateTime.now().plusMinutes(10)
verificationRepository.save(existingInfo)
}
Expand All @@ -37,61 +45,27 @@ class EmailCodeSendUseCase(
throw EmailAlreadyVerifiedException()
}
}
}
else {
} else {
val newVerificationInfo = VerificationMapper.toEntity(input, code)
verificationRepository.save(newVerificationInfo)
}
}

private fun sendVerificationEmail(input: EmailSendRequest, code: String) {
val content = EMAIL_CONTENT_TEMPLATE.format(code)
emailGateway.sendEmail(input.univEmail, EMAIL_SUBJECT, content)
}

val subject= "그라밋 - 이메일 인증 코드 입니다."
val content = """
companion object {
private const val EMAIL_SUBJECT = "그라밋 - 이메일 인증 코드 입니다."
private const val EMAIL_CONTENT_TEMPLATE = """
안녕하세요, 그라밋입니다.

아래의 코드는 이메일 인증을 위한 코드입니다:

$code
%s

10분 이내에 인증을 완료해주세요.
""".trimIndent()
emailGateway.sendEmail(input.univEmail, subject, content)
return VerificationMapper.toSendResDto()
"""
}

private fun extractDomain(email:String): String {
if(!email.contains("@")) throw EmailFormatInvalidException()
return email.substringAfter("@")
}
private fun isDomainExists(email: String): Boolean {
val domain = extractDomain(email)
return try {
val env = Hashtable<String, String>()
env["java.naming.factory.initial"] = "com.sun.jndi.dns.DnsContextFactory"
val ctx = InitialDirContext(env)
val attributes: Attributes = ctx.getAttributes(domain, arrayOf("MX"))
val mxRecords = attributes.get("MX")
println("MX Records for $domain: $mxRecords")
mxRecords != null
} catch (ex: EmailDomainNotFoundException) {
println("DNS lookup failed for $domain: ${ex.message}")
false
}
}


private fun isUnivMail(email: String): Boolean {
val eduDomains = setOf(
"postech.edu",
"kaist.edu",
"handong.edu",
"ewhain.net"
)
return email.endsWith("@ac.kr") || eduDomains.any {email.endsWith(it)}
}

private fun generateCode() : String {
val randomNum = (0..999999).random()
return String.format("%06d", randomNum)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@Tag(name = "이메일 인증 API - /v1/email")
@Tag(name = "이메일 인증 API - /v1/emails")
@RestController
@RequestMapping("/v1/email")
@RequestMapping("/v1/emails")
class EmailController(
private val emailService: EmailService
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema

data class EmailSendResponse(
@Schema(description = "학교 이메일 인증을 성공하여, 코드를 성공적으로 전송했는지 여부입니다.")
val isSucceess: Boolean,
val isSuccess: Boolean,

@Schema(description = "반환 성공 메시지 입니다.")
val message : String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema

data class EmailVerificationResponse(
@Schema(description = "학교 이메일 인증이 성공했는지 여부입니다.")
val isSucceess: Boolean,
val isSuccess: Boolean,

@Schema(description = "인증 코드 인증 성공 메시지 입니다.")
val message : String
Expand Down
40 changes: 40 additions & 0 deletions src/main/kotlin/com/dobby/backend/util/EmailUtils.kt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굳 좋습니다! 나중에 이메일 템플릿 생성 로직 같은 기능이 추가될 가능성을 고려한다면, 조금 더 SRP 원칙에 따라 세분화하는 방향도 생각해볼 수 있을 것 같아요. 하지만 이는 지금 당장 고민하기보다는 확장 필요성이 생겼을 때 고려해도 충분할 것 같아요. 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.dobby.backend.util

import com.dobby.backend.domain.exception.EmailFormatInvalidException
import java.util.*
import javax.naming.directory.Attributes
import javax.naming.directory.InitialDirContext

object EmailUtils{
private fun extractDomain(email:String): String {
if(!email.contains("@")) throw EmailFormatInvalidException()
return email.substringAfter("@")
}
fun isDomainExists(email: String): Boolean {
val domain = extractDomain(email)
return try {
val env = Hashtable<String, String>()
env["java.naming.factory.initial"] = "com.sun.jndi.dns.DnsContextFactory"
val ctx = InitialDirContext(env)
val attributes: Attributes = ctx.getAttributes(domain, arrayOf("MX"))
val mxRecords = attributes["MX"]
mxRecords != null
} catch (ex: Exception) {
false
}
}

fun isUnivMail(email: String): Boolean {
val eduDomains = setOf(
"postech.edu",
"kaist.edu",
"handong.edu",
"ewhain.net"
)
return email.endsWith("@ac.kr") || eduDomains.any { email.endsWith(it) }
}
fun generateCode(): String {
val randomNum = (0..999999).random()
return String.format("%06d", randomNum)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class VerificationMapperTest : BehaviorSpec({

then("EmailSendResponse 객체를 반환해야 한다") {
result shouldBe EmailSendResponse(
isSucceess = true,
isSuccess = true,
message = "해당 학교 이메일로 성공적으로 코드를 전송했습니다. 10분 이내로 인증을 완료해주세요."
)
}
Expand All @@ -44,7 +44,7 @@ class VerificationMapperTest : BehaviorSpec({

then("EmailVerificationResponse 객체를 반환해야 한다") {
result shouldBe EmailVerificationResponse(
isSucceess = true,
isSuccess = true,
message = "학교 메일 인증이 완료되었습니다."
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class EmailCodeSendUseCaseTest : BehaviorSpec({
val result = emailCodeSendUseCase.execute(request)

then("정상적으로 EmailSendResponse를 반환해야 한다") {
result.isSucceess shouldBe true
result.isSuccess shouldBe true
}

then("save 메서드가 호출되어야 한다") {
Expand All @@ -61,12 +61,10 @@ class EmailCodeSendUseCaseTest : BehaviorSpec({
val request = EmailSendRequest(univEmail = "[email protected]")

`when`("코드 전송 요청을 하면") {
val exception = shouldThrow<EmailNotUnivException> {
emailCodeSendUseCase.execute(request)
}

then("EmailNotUnivException 예외가 발생해야 한다") {
exception.message shouldBe "Email domain not found as university email"
val exception = shouldThrow<EmailNotUnivException> {
emailCodeSendUseCase.execute(request)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class EmailVerificationUseCaseTest : BehaviorSpec({
val result = emailVerificationUseCase.execute(request)

then("정상적으로 EmailVerificationResponse를 반환해야 한다") {
result.isSucceess shouldBe true
result.isSuccess shouldBe true
}

then("인증 상태가 VERIFIED로 업데이트되어야 한다") {
Expand All @@ -56,12 +56,10 @@ class EmailVerificationUseCaseTest : BehaviorSpec({
coEvery { mockVerificationRepository.findByUnivMailAndStatus(request.univEmail, VerificationStatus.HOLD) } returns null

`when`("EmailVerificationUseCase가 실행되면") {
val exception = shouldThrow<VerifyInfoNotFoundException> {
emailVerificationUseCase.execute(request)
}

then("VerifyInfoNotFoundException 예외가 발생해야 한다") {
exception.message shouldBe "Verification information is not found"
val exception = shouldThrow<VerifyInfoNotFoundException> {
emailVerificationUseCase.execute(request)
}
}
}
}
Expand All @@ -79,12 +77,10 @@ class EmailVerificationUseCaseTest : BehaviorSpec({
coEvery { mockVerificationRepository.findByUnivMailAndStatus(request.univEmail, VerificationStatus.HOLD) } returns mockEntity

`when`("EmailVerificationUseCase가 실행되면") {
val exception = shouldThrow<CodeNotCorrectException> {
emailVerificationUseCase.execute(request)
}

then("CodeNotCorrectException 예외가 발생해야 한다") {
exception.message shouldBe "Verification code is not correct"
val exception = shouldThrow<CodeNotCorrectException> {
emailVerificationUseCase.execute(request)
}
}
}
}
Expand All @@ -102,12 +98,10 @@ class EmailVerificationUseCaseTest : BehaviorSpec({
coEvery { mockVerificationRepository.findByUnivMailAndStatus(request.univEmail, VerificationStatus.HOLD) } returns mockEntity

`when`("EmailVerificationUseCase가 실행되면") {
val exception = shouldThrow<CodeExpiredException> {
emailVerificationUseCase.execute(request)
}

then("CodeExpiredException 예외가 발생해야 한다") {
exception.message shouldBe "Verification code is expired"
val exception = shouldThrow<CodeExpiredException> {
emailVerificationUseCase.execute(request)
}
}
}
}
Expand Down
Loading