소셜 로그인 이메일 미필수 정책 적용

소셜 로그인 시 이메일 동의 없이도 계정 생성이 가능하도록 변경합니다.
Member 엔티티의 email 필드를 선택 사항으로 변경하고, 관련 API 응답 및 인증 로직에서 이메일이 없는 경우에 대한 처리를 추가합니다.
This commit is contained in:
2026-01-26 08:56:05 +09:00
parent 744afd7f45
commit f778f68f1f
18 changed files with 73 additions and 53 deletions

View File

@@ -126,7 +126,7 @@ class AdminMemberService(
GetAdminMemberListResponseItem(
id = it.id!!,
email = it.email,
email = it.email ?: "",
nickname = it.nickname,
profileUrl = if (it.profileImage != null) {
"$cloudFrontHost/${it.profileImage}"
@@ -160,6 +160,7 @@ class AdminMemberService(
val member = repository.findByIdAndActive(memberId = request.memberId)
?: throw SodaException(messageKey = "admin.member.reset_password_invalid")
member.password = passwordEncoder.encode(member.email.split("@")[0])
val email = member.email ?: throw SodaException(message = "이메일이 없는 계정은 비밀번호 재설정이 불가능합니다.")
member.password = passwordEncoder.encode(email.split("@")[0])
}
}

View File

@@ -5,7 +5,6 @@ import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.userdetails.User
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
@@ -29,6 +28,12 @@ class ChargeTempController(private val service: ChargeTempService) {
@PostMapping("/verify")
fun verify(
@RequestBody request: VerifyRequest,
@AuthenticationPrincipal user: User
) = ApiResponse.ok(service.verify(user, request))
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.verify(member, request))
}
}

View File

@@ -15,10 +15,8 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.core.userdetails.User
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@@ -26,7 +24,6 @@ import org.springframework.transaction.annotation.Transactional
@Transactional(readOnly = true)
class ChargeTempService(
private val chargeRepository: ChargeRepository,
private val memberRepository: MemberRepository,
private val objectMapper: ObjectMapper,
private val messageSource: SodaMessageSource,
@@ -54,11 +51,9 @@ class ChargeTempService(
}
@Transactional
fun verify(user: User, verifyRequest: VerifyRequest) {
fun verify(member: Member, verifyRequest: VerifyRequest) {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByEmail(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)

View File

@@ -87,7 +87,7 @@ class CreatorAdminMemberService(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {

View File

@@ -19,7 +19,7 @@ import javax.persistence.OneToOne
@Entity
data class Member(
val email: String,
var email: String? = null,
var password: String,
var nickname: String,
var profileImage: String? = null,

View File

@@ -4,7 +4,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
class MemberAdapter(val member: Member) : User(
member.email,
member.email ?: "member:${member.id}",
member.password,
listOf(SimpleGrantedAuthority("ROLE_${member.role.name}"))
)

View File

@@ -20,7 +20,7 @@ import org.springframework.stereotype.Repository
@Repository
interface MemberRepository : JpaRepository<Member, Long>, MemberQueryRepository {
fun findByEmail(email: String): Member?
fun findByEmail(email: String?): Member?
fun findByNickname(nickname: String): Member?
fun findByGoogleId(googleId: String): Member?
fun findByKakaoId(kakaoId: Long): Member?
@@ -51,7 +51,7 @@ interface MemberQueryRepository {
fun getMessageRecipientPushToken(messageId: Long): PushTokenInfo?
fun getIndividualRecipientPushTokens(recipients: List<Long>, isAuth: Boolean?): List<PushTokenInfo>
fun getChangeNicknamePrice(memberId: Long): GetChangeNicknamePriceResponse
fun getMemberByEmail(email: String): Member?
fun getMemberByEmail(email: String?): Member?
fun getChangeNoticeRecipientPushTokens(creatorId: Long): List<PushTokenInfo>
fun getPushTokenFromReservationList(roomId: Long): List<PushTokenInfo>
@@ -363,7 +363,8 @@ class MemberQueryRepositoryImpl(
)
}
override fun getMemberByEmail(email: String): Member? {
override fun getMemberByEmail(email: String?): Member? {
if (email == null) return null
return queryFactory
.selectFrom(member)
.where(member.email.eq(email))

View File

@@ -346,7 +346,7 @@ class MemberService(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {
@@ -454,8 +454,16 @@ class MemberService(
}
override fun loadUserByUsername(username: String): UserDetails {
val member = repository.findByEmail(email = username)
?: throw UsernameNotFoundException(username)
val member = if (username.startsWith("member:")) {
val id = username.substringAfter("member:").toLongOrNull()
if (id != null) {
repository.findByIdOrNull(id)
} else {
null
}
} else {
repository.findByEmail(email = username)
} ?: throw UsernameNotFoundException(username)
return MemberAdapter(member)
}
@@ -592,7 +600,7 @@ class MemberService(
@Transactional
fun signOut(signOutRequest: SignOutRequest, user: User) {
val member = repository.findByEmail(user.username)
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (
member.provider == MemberProvider.EMAIL &&
@@ -620,11 +628,7 @@ class MemberService(
@Transactional
fun updateNickname(profileUpdateRequest: ProfileUpdateRequest, user: User) {
if (profileUpdateRequest.email != user.username) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
val member = repository.findByEmail(user.username)
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (profileUpdateRequest.nickname != null) {
@@ -652,11 +656,7 @@ class MemberService(
@Transactional
fun profileUpdate(profileUpdateRequest: ProfileUpdateRequest, user: User): ProfileResponse {
if (profileUpdateRequest.email != user.username) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
val member = repository.findByEmail(user.username)
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (profileUpdateRequest.modifyPassword != null) {
@@ -729,7 +729,7 @@ class MemberService(
@Transactional
fun profileImageUpdate(multipartFile: MultipartFile, user: User): String {
val member = repository.findByEmail(user.username)
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
val metadata = ObjectMetadata()
@@ -932,7 +932,24 @@ class MemberService(
return MemberResolveResult(member = member, isNew = true)
}
private fun checkEmail(email: String) {
private fun findMemberByUsername(username: String): Member? {
return if (username.startsWith("member:")) {
val id = username.substringAfter("member:").toLongOrNull()
if (id != null) {
repository.findByIdOrNull(id)
} else {
null
}
} else {
repository.findByEmail(email = username)
}
}
private fun checkEmail(email: String?) {
if (email.isNullOrBlank()) {
return
}
val member = repository.findByEmail(email)
if (member != null) {

View File

@@ -17,7 +17,7 @@ data class ProfileResponse(
) {
constructor(member: Member, cloudFrontHost: String, container: String) : this(
userId = member.id!!,
email = member.email,
email = member.email ?: "",
nickname = member.nickname,
gender = member.gender,
profileUrl = if (member.profileImage != null) {

View File

@@ -1,7 +1,7 @@
package kr.co.vividnext.sodalive.member
data class ProfileUpdateRequest(
val email: String,
val email: String? = null,
val password: String? = null,
val modifyPassword: String? = null,
val nickname: String? = null,

View File

@@ -47,7 +47,7 @@ class GoogleAuthService(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {

View File

@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.member.social.google
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
@@ -27,7 +26,7 @@ class GoogleService(
if (token != null) {
val payload = token.payload
val email = payload.email ?: throw SodaException(messageKey = "member.social.email_consent_required")
val email = payload.email
GoogleUserInfo(
sub = payload.subject,

View File

@@ -2,6 +2,6 @@ package kr.co.vividnext.sodalive.member.social.google
data class GoogleUserInfo(
val sub: String,
val email: String,
val email: String?,
val name: String?
)

View File

@@ -47,7 +47,7 @@ class KakaoAuthService(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {

View File

@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.member.social.kakao
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
@@ -37,7 +36,6 @@ class KakaoService(
val id = jsonNode.get("id").asLong()
val kakaoAccount = jsonNode.get("kakao_account")
val email = kakaoAccount?.get("email")?.asText()
?: throw SodaException(messageKey = "member.social.kakao_login_failed")
val properties = jsonNode.get("properties")
val nickname = properties?.get("nickname")?.asText()

View File

@@ -2,6 +2,6 @@ package kr.co.vividnext.sodalive.member.social.kakao
data class KakaoUserInfo(
val id: Long,
val email: String,
val email: String?,
val nickname: String?
)

View File

@@ -1,9 +1,10 @@
package kr.co.vividnext.sodalive.menu
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.userdetails.User
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@@ -13,5 +14,13 @@ import org.springframework.web.bind.annotation.RestController
class MenuController(private val service: MenuService) {
@GetMapping
@PreAuthorize("hasAnyRole('AGENT', 'ADMIN', 'CREATOR')")
fun getMenus(@AuthenticationPrincipal user: User) = ApiResponse.ok(service.getMenus(user))
fun getMenus(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.getMenus(member))
}
}

View File

@@ -1,18 +1,13 @@
package kr.co.vividnext.sodalive.menu
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.security.core.userdetails.User
import kr.co.vividnext.sodalive.member.Member
import org.springframework.stereotype.Service
@Service
class MenuService(
private val repository: MenuRepository,
private val memberRepository: MemberRepository
private val repository: MenuRepository
) {
fun getMenus(user: User): List<GetMenuResponse> {
val member = memberRepository.findByEmail(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
fun getMenus(member: Member): List<GetMenuResponse> {
return repository.getMenu(member.role)
}
}