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

feat: 유저 회원가입 플로우 (TEMP_USER -> USER) #10

Merged
merged 9 commits into from
Mar 20, 2024
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package com.vacgom.backend.application.auth

import com.vacgom.backend.application.auth.dto.LoginResponse
import com.vacgom.backend.application.auth.dto.AuthResponse
import com.vacgom.backend.application.auth.dto.MemberResponse
import com.vacgom.backend.application.auth.dto.TokenResponse
import com.vacgom.backend.domain.auth.constants.Role.ROLE_TEMP_USER
import com.vacgom.backend.domain.auth.oauth.constants.ProviderType
import com.vacgom.backend.domain.member.Member
import com.vacgom.backend.global.security.jwt.JwtService
import com.vacgom.backend.global.security.jwt.JwtFactory
import com.vacgom.backend.infrastructure.member.persistence.MemberRepository
import jakarta.transaction.Transactional
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Component
import java.net.URI

@Component
@Transactional
class AuthService(
private val authFactory: AuthFactory,
private val jwtService: JwtService,
private val jwtFactory: JwtFactory,
private val memberRepository: MemberRepository
) {
fun createRedirectHeaders(redirectUri: URI): HttpHeaders {
Expand All @@ -29,20 +30,19 @@ class AuthService(
return authFactory.getAuthUriGenerator(provider).generate()
}

@Transactional
fun login(
providerType: String,
code: String
): LoginResponse {
): AuthResponse {
val authConnector = authFactory.getAuthConnector(providerType)
val oauthToken = authConnector.fetchOauthToken(code)
val memberInfo = authConnector.fetchMemberInfo(oauthToken.accessToken)
val member = findOrCreateMember(memberInfo.id, ProviderType.from(providerType))

val memberResponse = MemberResponse(member.id!!, member.role)
val tokenResponse = TokenResponse(jwtService.createAccessToken(member))
val tokenResponse = TokenResponse(jwtFactory.createAccessToken(member))

return LoginResponse(memberResponse, tokenResponse)
return AuthResponse(memberResponse, tokenResponse)
}

private fun findOrCreateMember(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.vacgom.backend.application.auth.dto

data class LoginResponse(
data class AuthResponse(
val member: MemberResponse,
val token: TokenResponse
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.vacgom.backend.application.auth.dto

data class ResourceIdResponse(
var id: Long
val id: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.vacgom.backend.application.member

import com.vacgom.backend.application.auth.dto.AuthResponse
import com.vacgom.backend.application.auth.dto.MemberResponse
import com.vacgom.backend.application.auth.dto.TokenResponse
import com.vacgom.backend.application.member.dto.request.SignUpRequest
import com.vacgom.backend.domain.auth.constants.Role
import com.vacgom.backend.domain.member.HealthProfile
import com.vacgom.backend.domain.member.MemberDetails
import com.vacgom.backend.domain.member.Nickname
import com.vacgom.backend.exception.member.MemberError
import com.vacgom.backend.exception.member.NicknameError
import com.vacgom.backend.global.exception.error.BusinessException
import com.vacgom.backend.global.security.jwt.JwtFactory
import com.vacgom.backend.infrastructure.member.persistence.HealthProfileRepository
import com.vacgom.backend.infrastructure.member.persistence.MemberRepository
import jakarta.transaction.Transactional
import org.springframework.stereotype.Service
import java.util.*

@Service
@Transactional
class MemberService(
private val memberRepository: MemberRepository,
private val healthProfileRepository: HealthProfileRepository,
private val jwtFactory: JwtFactory
) {
fun validateNickname(id: String) {
val nickname = Nickname(id)
if (memberRepository.existsMemberByNickname(nickname))
throw BusinessException(NicknameError.DUPLICATED)
}

fun signUpVacgom(
memberId: UUID,
request: SignUpRequest
): AuthResponse {
val member = memberRepository.findById(memberId).orElseThrow { BusinessException(MemberError.NOT_FOUND) }
val nickname = Nickname(request.nickname)
val memberDetails = MemberDetails(request.name, request.birthday, request.sex)

member.updateNickname(nickname)
member.updateMemberDetails(memberDetails)
member.updateRole(Role.ROLE_USER)

Copy link
Collaborator

Choose a reason for hiding this comment

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

여기서 나중엔 백신접종내역 추가되는ㄱ게 맞나요?

val healthConditions = request.healthConditions.stream()
.map { condition ->
HealthProfile(member, condition)
}.toList()
healthProfileRepository.saveAll(healthConditions)

val memberResponse = MemberResponse(member.id!!, member.role)
val tokenResponse = TokenResponse(jwtFactory.createAccessToken(member))

return AuthResponse(memberResponse, tokenResponse)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.vacgom.backend.application.member.dto.request

import com.vacgom.backend.domain.member.constants.HealthCondition

data class SignUpRequest(
val name: String,
val birthday: String,
val sex: String,
val healthConditions: MutableList<HealthCondition>,
val nickname: String
)
24 changes: 0 additions & 24 deletions src/main/kotlin/com/vacgom/backend/domain/auth/RefreshToken.kt

This file was deleted.

12 changes: 0 additions & 12 deletions src/main/kotlin/com/vacgom/backend/domain/auth/constants/Role.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,4 @@ enum class Role {
ROLE_GUEST,
ROLE_TEMP_USER,
ROLE_USER;

fun isGuest(role: Role): Boolean {
return role == ROLE_GUEST
}

fun isTempUser(role: Role): Boolean {
return role == ROLE_TEMP_USER
}

fun isUser(role: Role): Boolean {
return role == ROLE_USER
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/com/vacgom/backend/domain/member/HealthProfile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.vacgom.backend.domain.member

import com.vacgom.backend.domain.member.constants.HealthCondition
import jakarta.persistence.*
import org.hibernate.annotations.GenericGenerator
import java.util.*

@Entity
@Table(name = "t_health_profile")
class HealthProfile(
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
var member: Member,

@Enumerated(value = EnumType.STRING)
var healthCondition: HealthCondition
) {
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@Column(columnDefinition = "BINARY(16)", name = "health_profile_id")
val id: UUID? = null
}
21 changes: 18 additions & 3 deletions src/main/kotlin/com/vacgom/backend/domain/member/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,25 @@ class Member(
) : BaseEntity() {

@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@Column(columnDefinition = "BINARY(16)", name = "member_id")
val id: UUID? = null
val id: UUID? = UUID.randomUUID()

var name: String? = null
@Embedded
var memberDetails: MemberDetails? = null

@Embedded
var nickname: Nickname? = null

fun updateMemberDetails(memberDetails: MemberDetails) {
this.memberDetails = memberDetails
}

fun updateNickname(nickname: Nickname) {
this.nickname = nickname
}

fun updateRole(role: Role) {
this.role = role
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/com/vacgom/backend/domain/member/MemberDetails.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.vacgom.backend.domain.member

import com.fasterxml.jackson.annotation.JsonFormat
import com.vacgom.backend.domain.member.constants.Sex
import jakarta.persistence.Embeddable
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@Embeddable
class MemberDetails(
var name: String,

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
var birthday: LocalDate,

@Enumerated(EnumType.STRING)
var sex: Sex
) {
constructor(name: String, birthday: String, sex: String) : this(
name,
LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyy-MM-dd")),
Sex.valueOf(sex)
)
}
43 changes: 43 additions & 0 deletions src/main/kotlin/com/vacgom/backend/domain/member/Nickname.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.vacgom.backend.domain.member

import com.vacgom.backend.exception.member.NicknameError
import com.vacgom.backend.global.exception.error.BusinessException
import jakarta.persistence.Column
import jakarta.persistence.Embeddable

@Embeddable
class Nickname(
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 객체에 제약 거는거 좋아하는건 또 어케 하시고 ..
근데 이거 코틀린에서도 자바처럼 생성자를 만들 수 있거든여 그걸 활용하는게 좋을것 같습니다.

@Column(unique = true) var nickname: String
) {
companion object {
private const val VACGOM_ID_REGEX = "[a-z0-9_]+"
private const val START_WITH_LOWER_CASE_REGEX = "^[a-z].*"
private const val CONSECUTIVE_UNDERSCORES = "____"
private const val MINIMUM_LENGTH = 4
private const val MAXIMUM_LENGTH = 10
}

init {
validatePattern(nickname)
}

private fun validatePattern(id: String) {
require(id.matches(START_WITH_LOWER_CASE_REGEX.toRegex())) {
throw BusinessException(NicknameError.NOT_START_WITH_LOWER_CASE)
}
require(id.matches(VACGOM_ID_REGEX.toRegex())) {
throw BusinessException(NicknameError.INVALID_VACGOM_ID_PATTERN)
}
require(CONSECUTIVE_UNDERSCORES !in id) {
throw BusinessException(NicknameError.CONSECUTIVE_UNDERSCORES)
}
require(id.length in MINIMUM_LENGTH..MAXIMUM_LENGTH) {
throw BusinessException(NicknameError.INVALID_LENGTH)
}

val lowerCaseCount = id.count { it.isLowerCase() }
require(lowerCaseCount >= 4) {
throw BusinessException(NicknameError.MINIMUM_LOWERCASE_REQUIRED)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.vacgom.backend.domain.member.constants

enum class HealthCondition(
val code: Long,
val description: String
) {
DIABETES(1, "당뇨병"),
CHRONIC_CARDIOVASCULAR_DISEASE(2, "만성 심혈관질환"),
CHRONIC_PULMONARY_DISEASE(3, "만성 폐질환"),
CHRONIC_RENAL_DISEASE(4, "만성 신질환"),
CHRONIC_LIVER_DISEASE(5, "만성 간질환"),
SOLID_TUMOR_UNDERGOING_ANTINEOPLASTIC_THERAPY(6, "항암치료중인 고형암"),
IMMUNOSUPPRESSIVE_AGENTS_EXCLUDING_TRANSPLANT(7, "이식 이외 면역 억제제 사용"),
HEMATOPOIETIC_STEM_CELL_TRANSPLANTATION(8, "조혈모"),
CELL_TRANSPLANTATION(9, "세포이식"),
SICKLE_CELL_DISEASE(10, "무비증"),
HIV_INFECTION_CD4_ABOVE_200(11, "HIV 감염:CD4>=200/mm3"),
HIV_INFECTION_CD4_BELOW_200(11, "HIV 감염:CD4<200/mm3"),
PREGNANCY(12, "임신"),
MEDICAL_WORKER(13, "의료기관 종사자"),
ORGAN_TRANSPLANTATION(14, "장기 이식 경험")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.vacgom.backend.domain.member.constants

enum class Sex {
MALE,
FEMALE
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ enum class MemberError(
override val status: HttpStatus,
override val code: String
) : ErrorCode {
NOT_FOUND("사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND, "M_001"),
NOT_FOUND("사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND, "M_001")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.vacgom.backend.exception.member

import com.vacgom.backend.global.exception.error.ErrorCode
import org.springframework.http.HttpStatus


enum class NicknameError(
override val message: String,
override val status: HttpStatus,
override val code: String
) : ErrorCode {
INVALID_VACGOM_ID_PATTERN("닉네임은 영문 소문자, 숫자, 언더바(_)로만 구성되어야 합니다.", HttpStatus.BAD_REQUEST, "VI_001"),
NOT_START_WITH_LOWER_CASE("닉네임은 영문 소문자로 시작해야 합니다", HttpStatus.BAD_REQUEST, "VI_002"),
CONSECUTIVE_UNDERSCORES("언더바(_)는 최대 3개 까지 연속으로 사용할 수 있습니다.", HttpStatus.BAD_REQUEST, "VI_003"),
DUPLICATED("이미 사용중인 닉네임입니다.", HttpStatus.BAD_REQUEST, "VI_004"),
INVALID_LENGTH("닉네임은 4 ~ 20자 이하여야 합니다.", HttpStatus.BAD_REQUEST, "VI_005"),
MINIMUM_LOWERCASE_REQUIRED("최소 4개의 소문자 영어를 포함해야 합니다.", HttpStatus.BAD_REQUEST, "VI_006"),
}

Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ class SecurityConfig(
@Order(1)
fun anyRequestFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeRequests { auth ->
auth.requestMatchers(customRequestMatcher.tempUserEndpoints()).hasRole("TEMP_USER")
.anyRequest().hasRole("USER")
auth.requestMatchers(customRequestMatcher.tempUserEndpoints()).hasRole("TEMP_USER").anyRequest().hasRole("USER")
}
.addFilterAfter(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
.addFilterBefore(apiExceptionHandlingFilter, UsernamePasswordAuthenticationFilter::class.java)

return commonHttpSecurity(http).build()
}

Expand Down
Loading
Loading