Files
sodalive-backend-spring-boot/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt

1119 lines
43 KiB
Kotlin

package kr.co.vividnext.sodalive.member
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.order.OrderService
import kr.co.vividnext.sodalive.email.SendEmailService
import kr.co.vividnext.sodalive.fcm.PushTokenService
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.jwt.TokenProvider
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.member.auth.AuthRepository
import kr.co.vividnext.sodalive.member.block.BlockMember
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.block.GetBlockedMemberListResponse
import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse
import kr.co.vividnext.sodalive.member.login.LoginRequest
import kr.co.vividnext.sodalive.member.login.LoginResponse
import kr.co.vividnext.sodalive.member.myPage.MyPageResponse
import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLog
import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLogRepository
import kr.co.vividnext.sodalive.member.nickname.NicknameGenerateService
import kr.co.vividnext.sodalive.member.notification.MemberNotificationService
import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest
import kr.co.vividnext.sodalive.member.signUp.SignUpRequest
import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
import kr.co.vividnext.sodalive.member.signUp.SignUpResponse
import kr.co.vividnext.sodalive.member.signUp.SignUpValidator
import kr.co.vividnext.sodalive.member.social.MemberResolveResult
import kr.co.vividnext.sodalive.member.social.apple.AppleUserInfo
import kr.co.vividnext.sodalive.member.social.google.GoogleUserInfo
import kr.co.vividnext.sodalive.member.social.kakao.KakaoUserInfo
import kr.co.vividnext.sodalive.member.social.line.LineUserInfo
import kr.co.vividnext.sodalive.member.stipulation.Stipulation
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
import kr.co.vividnext.sodalive.member.stipulation.StipulationIds
import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository
import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag
import kr.co.vividnext.sodalive.member.tag.MemberTagRepository
import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
import kr.co.vividnext.sodalive.point.MemberPointRepository
import kr.co.vividnext.sodalive.utils.generateFileName
import kr.co.vividnext.sodalive.utils.generatePassword
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.CacheManager
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
@Service
@Transactional(readOnly = true)
class MemberService(
private val repository: MemberRepository,
private val tokenRepository: MemberTokenRepository,
private val stipulationRepository: StipulationRepository,
private val stipulationAgreeRepository: StipulationAgreeRepository,
private val creatorFollowingRepository: CreatorFollowingRepository,
private val blockMemberRepository: BlockMemberRepository,
private val authRepository: AuthRepository,
private val signOutRepository: SignOutRepository,
private val nicknameChangeLogRepository: NicknameChangeLogRepository,
private val memberTagRepository: MemberTagRepository,
private val liveReservationRepository: LiveReservationRepository,
private val chargeRepository: ChargeRepository,
private val memberPointRepository: MemberPointRepository,
private val orderService: OrderService,
private val emailService: SendEmailService,
private val pushTokenService: PushTokenService,
private val canPaymentService: CanPaymentService,
private val nicknameGenerateService: NicknameGenerateService,
private val memberNotificationService: MemberNotificationService,
private val s3Uploader: S3Uploader,
private val validator: SignUpValidator,
private val tokenProvider: TokenProvider,
private val passwordEncoder: PasswordEncoder,
private val authenticationManagerBuilder: AuthenticationManagerBuilder,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
private val countryContext: CountryContext,
private val objectMapper: ObjectMapper,
private val cacheManager: CacheManager,
@Value("\${cloud.aws.s3.bucket}")
private val s3Bucket: String,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : UserDetailsService {
private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = mutableMapOf()
private val recommendLiveCacheKeyPrefix = "getRecommendLive:"
@Transactional
fun signUpV2(request: SignUpRequestV2): SignUpResponse {
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
if (!request.isAgreePrivacyPolicy || !request.isAgreeTermsOfService) {
throw SodaException(messageKey = "member.validation.agree_required")
}
duplicateCheckEmail(request.email)
validatePassword(request.password)
val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member(
email = request.email,
password = passwordEncoder.encode(request.password),
nickname = nickname,
profileImage = "profile/default-profile.png",
gender = Gender.NONE,
container = request.container,
countryCode = countryContext.countryCode
)
if (!request.marketingPid.isNullOrBlank()) {
member.activePid = request.marketingPid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
}
repository.save(member)
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
if (request.pushToken != null) {
pushTokenService.registerToken(
memberId = member.id!!,
token = request.pushToken,
deviceType = request.container
)
}
return SignUpResponse(
memberId = member.id!!,
marketingPid = request.marketingPid,
loginResponse = login(request.email, request.password)
)
}
@Transactional
fun signUp(
profileImage: MultipartFile?,
requestString: String
): SignUpResponse {
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val request = objectMapper.readValue(requestString, SignUpRequest::class.java)
if (!request.isAgreePrivacyPolicy || !request.isAgreeTermsOfService) {
throw SodaException(messageKey = "member.validation.agree_required")
}
validatePassword(request.password)
duplicateCheckEmail(request.email)
duplicateCheckNickname(request.nickname)
val member = createMember(request)
member.profileImage = uploadProfileImage(profileImage = profileImage, memberId = member.id!!)
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
return SignUpResponse(
memberId = member.id!!,
marketingPid = request.marketingPid,
loginResponse = login(request.email, request.password)
)
}
fun login(request: LoginRequest): ApiResponse<LoginResponse> {
return ApiResponse.ok(
message = messageSource.getMessage("member.login.success", langContext.lang),
data = login(request.email, request.password, request.isAdmin, request.isCreator)
)
}
fun getMember(id: Long, container: String): ProfileResponse {
val member = repository.findByIdOrNull(id)
?: throw SodaException(messageKey = "member.validation.user_not_found")
return ProfileResponse(member, cloudFrontHost, container)
}
fun getMemberInfo(member: Member, container: String): GetMemberInfoResponse {
val gender = if (member.auth != null) {
if (member.auth!!.gender == 1) {
messageSource.getMessage("member.gender.male", langContext.lang)
} else {
messageSource.getMessage("member.gender.female", langContext.lang)
}
} else {
null
}
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val signUpDate = member.createdAt!!
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.format(dateTimeFormatter)
val chargeCount = chargeRepository.getChargeCount(memberId = member.id!!)
val point = memberPointRepository.findByMemberIdAndExpiresAtAfterOrderByExpiresAtAsc(
memberId = member.id!!,
expiresAt = LocalDateTime.now()
).sumOf { it.point }
return GetMemberInfoResponse(
can = member.getChargeCan(container) + member.getRewardCan(container),
point = point,
isAuth = member.auth != null,
gender = gender,
signupDate = signUpDate,
chargeCount = chargeCount,
role = member.role,
messageNotice = member.notification?.message,
followingChannelLiveNotice = member.notification?.live,
followingChannelUploadContentNotice = member.notification?.uploadContent,
auditionNotice = member.notification?.audition
)
}
@Transactional
fun updateNotificationSettings(request: UpdateNotificationSettingRequest, member: Member) {
memberNotificationService.updateNotification(
live = request.live,
uploadContent = request.uploadContent,
message = request.message,
audition = request.audition,
member = member
)
}
@Transactional
fun updatePushToken(memberId: Long, pushToken: String, container: String) {
pushTokenService.registerToken(
memberId = memberId,
token = pushToken,
deviceType = container
)
}
@Transactional
fun updateAdid(memberId: Long, adid: String) {
val member = repository.findByIdOrNull(id = memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials")
member.adid = adid
}
fun getMyPage(member: Member, container: String): MyPageResponse {
val liveReservationCount = liveReservationRepository.getReservationCount(memberId = member.id!!)
val orderList = orderService.getAudioContentOrderList(
member = member,
offset = 0,
limit = 4
)
val totalPoint = memberPointRepository.findByMemberIdAndExpiresAtAfterOrderByExpiresAtAsc(
memberId = member.id!!,
expiresAt = LocalDateTime.now()
).sumOf { it.point }
return MyPageResponse(
nickname = member.nickname,
profileUrl = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
},
chargeCan = member.getChargeCan(container = container),
rewardCan = member.getRewardCan(container = container),
point = totalPoint,
youtubeUrl = member.youtubeUrl,
instagramUrl = member.instagramUrl,
fancimmUrl = member.fancimmUrl,
xUrl = member.xUrl,
kakaoOpenChatUrl = member.websiteUrl,
liveReservationCount = liveReservationCount,
isAuth = member.auth != null,
orderList = orderList
)
}
private fun login(
email: String,
password: String,
isAdmin: Boolean = false,
isCreator: Boolean = false
): LoginResponse {
val member = repository.findByEmail(email = email)
?: throw SodaException(messageKey = "member.validation.account_not_found")
if (!member.isActive) {
throw SodaException(messageKey = "member.validation.inactive_account")
}
if (member.provider != MemberProvider.EMAIL) {
val provider = resolveProviderLabel(member.provider)
throw SodaException(message = formatMessage("member.validation.email_registered_with_provider", provider))
}
if (isCreator && member.role != MemberRole.CREATOR) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
if (isAdmin && member.role != MemberRole.ADMIN) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
val authenticationToken = UsernamePasswordAuthenticationToken(email, password)
val authentication = authenticationManagerBuilder.`object`.authenticate(authenticationToken)
SecurityContextHolder.getContext().authentication = authentication
val jwt = tokenProvider.createToken(
authentication = authentication,
memberId = member.id!!
)
return LoginResponse(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
}
)
}
private fun uploadProfileImage(profileImage: MultipartFile?, memberId: Long): String {
return if (profileImage != null) {
val metadata = ObjectMetadata()
metadata.contentLength = profileImage.size
s3Uploader.upload(
inputStream = profileImage.inputStream,
bucket = s3Bucket,
filePath = "profile/$memberId/${generateFileName(prefix = "$memberId-profile")}",
metadata = metadata
)
} else {
"profile/default-profile.png"
}
}
private fun agreeTermsOfServiceAndPrivacyPolicy(
member: Member,
stipulationTermsOfService: Stipulation,
stipulationPrivacyPolicy: Stipulation
) {
val termsOfServiceAgree = StipulationAgree(true)
termsOfServiceAgree.member = member
termsOfServiceAgree.stipulation = stipulationTermsOfService
stipulationAgreeRepository.save(termsOfServiceAgree)
val privacyPolicyAgree = StipulationAgree(true)
privacyPolicyAgree.member = member
privacyPolicyAgree.stipulation = stipulationPrivacyPolicy
stipulationAgreeRepository.save(privacyPolicyAgree)
}
private fun createMember(request: SignUpRequest): Member {
val member = Member(
email = request.email,
password = passwordEncoder.encode(request.password),
nickname = request.nickname,
gender = request.gender,
container = request.container,
countryCode = countryContext.countryCode
)
if (!request.marketingPid.isNullOrBlank()) {
member.activePid = request.marketingPid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
}
return repository.save(member)
}
private fun validatePassword(password: String) {
val passwordValidationMessage = validator.passwordValidation(password)
if (passwordValidationMessage.trim().isNotEmpty()) {
throw SodaException(passwordValidationMessage)
}
}
fun duplicateCheckEmail(email: String): ApiResponse<Any> {
validateEmail(email)
val findMember = repository.findByEmail(email)
if (findMember != null) {
if (findMember.provider == MemberProvider.EMAIL) {
throw SodaException(messageKey = "member.validation.email_in_use", errorProperty = "email")
} else {
val provider = resolveProviderLabel(findMember.provider)
throw SodaException(
message = formatMessage("member.validation.email_registered_with_provider_already", provider),
errorProperty = "email"
)
}
}
return ApiResponse.ok(message = messageSource.getMessage("member.validation.email_available", langContext.lang))
}
private fun validateEmail(email: String) {
val emailValidationMessage = validator.emailValidation(email)
if (emailValidationMessage.trim().isNotEmpty()) {
throw SodaException(emailValidationMessage, "email")
}
}
fun duplicateCheckNickname(nickname: String): ApiResponse<Any> {
validateNickname(nickname)
repository.findByNickname(nickname)
?.let { throw SodaException(messageKey = "member.validation.nickname_in_use", errorProperty = "nickname") }
return ApiResponse.ok(message = messageSource.getMessage("member.validation.nickname_available", langContext.lang))
}
private fun validateNickname(nickname: String) {
val nicknameValidationMessage = validator.nicknameValidation(nickname)
if (nicknameValidationMessage.trim().isNotEmpty()) {
throw SodaException(nicknameValidationMessage, "nickname")
}
}
override fun loadUserByUsername(username: String): UserDetails {
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)
}
@Transactional
fun creatorFollow(creatorId: Long, isNotify: Boolean, isActive: Boolean, memberId: Long) {
if (creatorId == memberId) return
val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberId(
creatorId = creatorId,
memberId = memberId
)
if (creatorFollowing == null) {
val creator = repository.findByIdOrNull(creatorId)
?: throw SodaException(messageKey = "member.validation.creator_not_found")
val member = repository.findByIdOrNull(memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials")
val newCreatorFollowing = CreatorFollowing()
newCreatorFollowing.member = member
newCreatorFollowing.creator = creator
creatorFollowingRepository.save(newCreatorFollowing)
} else {
creatorFollowing.isNotify = isNotify && isActive
creatorFollowing.isActive = isActive
}
}
@Transactional
fun creatorUnFollow(creatorId: Long, memberId: Long) {
val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberId(
creatorId = creatorId,
memberId = memberId
)
if (creatorFollowing != null) {
creatorFollowing.isActive = false
}
}
fun getBlockedMemberIdList(memberId: Long): List<Long> {
return blockMemberRepository.getBlockedMemberIdList(memberId)
}
fun getBlockedMemberList(memberId: Long, offset: Long, limit: Long): GetBlockedMemberListResponse {
val totalCount = blockMemberRepository.getBlockedMemberTotalCount(memberId)
val items = blockMemberRepository.getBlockedMemberList(offset = offset, limit = limit, memberId = memberId)
return GetBlockedMemberListResponse(totalCount = totalCount, items = items)
}
@Transactional
fun memberBlock(request: MemberBlockRequest, memberId: Long) {
val member = repository.findByIdOrNull(id = memberId)
?: throw SodaException(messageKey = "common.error.invalid_request")
val blockedMember = repository.findByIdOrNull(id = request.blockMemberId)
?: throw SodaException(messageKey = "common.error.invalid_request")
val blockTargetMemberIds = mutableSetOf(request.blockMemberId)
blockedMember.auth?.let { auth ->
val verifiedMemberIds = authRepository.getMemberIdsByNameAndBirthAndDiAndGender(
name = auth.name,
birth = auth.birth,
di = auth.di,
gender = auth.gender
)
blockTargetMemberIds.addAll(verifiedMemberIds)
}
blockTargetMemberIds.remove(memberId)
blockTargetMemberIds.forEach { targetMemberId ->
val targetMember = repository.findByIdOrNull(id = targetMemberId) ?: return@forEach
var blockMember = blockMemberRepository.getBlockAccount(
blockedMemberId = targetMemberId,
memberId = memberId
)
if (blockMember == null) {
blockMember = BlockMember()
blockMember.member = member
blockMember.blockedMember = targetMember
blockMemberRepository.save(blockMember)
} else {
blockMember.isActive = true
}
}
evictRecommendLiveCache(memberId)
blockTargetMemberIds.forEach { evictRecommendLiveCache(it) }
}
@Transactional
fun memberUnBlock(request: MemberBlockRequest, memberId: Long) {
val blockMember = blockMemberRepository.getBlockAccount(
blockedMemberId = request.blockMemberId,
memberId = memberId
)
if (blockMember != null) {
blockMember.isActive = false
}
evictRecommendLiveCache(memberId)
evictRecommendLiveCache(request.blockMemberId)
}
fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId)
fun searchMember(nickname: String, member: Member): List<GetRoomDetailUser> {
if (nickname.length < 2) {
throw SodaException(messageKey = "member.validation.nickname_min_length")
}
return repository.findByNicknameAndOtherCondition(nickname, member)
.filter {
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) &&
!blockMemberRepository.isBlocked(blockedMemberId = it.id!!, memberId = member.id!!)
}
.map {
GetRoomDetailUser(it, cloudFrontHost)
}
}
@Transactional
fun logout(token: String, memberId: Long) {
val member = repository.findByIdOrNull(memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials")
member.pushToken = null
pushTokenService.logout(memberId = memberId)
val lock = getOrCreateLock(memberId = memberId)
lock.write {
val memberToken = tokenRepository.findByIdOrNull(memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials")
memberToken.tokenSet.remove(token)
tokenRepository.save(memberToken)
}
}
@Transactional
fun logoutAll(memberId: Long) {
val member = repository.findByIdOrNull(memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials")
member.pushToken = null
pushTokenService.logout(memberId = memberId)
val lock = getOrCreateLock(memberId = memberId)
lock.write { tokenRepository.deleteById(memberId) }
}
@Transactional
fun signOut(signOutRequest: SignOutRequest, user: User) {
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (
member.provider == MemberProvider.EMAIL &&
!passwordEncoder.matches(signOutRequest.password, member.password)
) {
throw SodaException(messageKey = "member.validation.password_mismatch")
}
if (signOutRequest.reason.isBlank()) {
throw SodaException(messageKey = "member.validation.signout_reason_required")
}
logoutAll(memberId = member.id!!)
member.isActive = false
member.nickname = "deleted_${member.nickname}"
val signOut = SignOut(reason = signOutRequest.reason)
signOut.member = member
signOutRepository.save(signOut)
}
fun getChangeNicknamePrice(memberId: Long): GetChangeNicknamePriceResponse {
return repository.getChangeNicknamePrice(memberId = memberId)
}
@Transactional
fun updateNickname(profileUpdateRequest: ProfileUpdateRequest, user: User) {
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (profileUpdateRequest.nickname != null) {
validateNickname(profileUpdateRequest.nickname)
repository.findByNickname(profileUpdateRequest.nickname)
?.let { throw SodaException(messageKey = "member.validation.nickname_in_use") }
val price = repository.getChangeNicknamePrice(memberId = member.id!!).price
if (price > 0) {
canPaymentService.spendCan(
memberId = member.id!!,
needCan = price,
canUsage = CanUsage.CHANGE_NICKNAME,
container = profileUpdateRequest.container
)
}
val nicknameChangeLog = NicknameChangeLog(prevNickname = member.nickname)
nicknameChangeLog.member = member
nicknameChangeLogRepository.save(nicknameChangeLog)
member.nickname = profileUpdateRequest.nickname
}
}
@Transactional
fun profileUpdate(profileUpdateRequest: ProfileUpdateRequest, user: User): ProfileResponse {
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (profileUpdateRequest.modifyPassword != null) {
if (passwordEncoder.matches(profileUpdateRequest.password, member.password)) {
validatePassword(profileUpdateRequest.modifyPassword)
member.password = passwordEncoder.encode(profileUpdateRequest.modifyPassword)
} else {
throw SodaException(messageKey = "member.validation.password_mismatch")
}
}
if (profileUpdateRequest.gender != null) {
member.gender = profileUpdateRequest.gender
}
if (profileUpdateRequest.nickname != null) {
validateNickname(profileUpdateRequest.nickname)
repository.findByNickname(profileUpdateRequest.nickname)
?.let { throw SodaException(messageKey = "member.validation.nickname_in_use") }
member.nickname = profileUpdateRequest.nickname
}
val tags = if (!profileUpdateRequest.removeTags.isNullOrEmpty()) {
member.tags.filter { !profileUpdateRequest.removeTags.contains(it.tag.tag) }
} else {
member.tags
}.toMutableList()
if (!profileUpdateRequest.insertTags.isNullOrEmpty()) {
val accountCounselorTags = memberTagRepository.findByMember(member).map { it.tag }
profileUpdateRequest.insertTags.forEach {
val tag = memberTagRepository.findByTag(it)
if (tag != null && !accountCounselorTags.contains(tag)) {
tags.add(MemberCreatorTag(member, tag))
}
}
}
if (tags != member.tags) {
member.tags.clear()
member.tags.addAll(tags)
}
if (profileUpdateRequest.introduce != null) {
member.introduce = profileUpdateRequest.introduce
}
if (profileUpdateRequest.youtubeUrl != null) {
member.youtubeUrl = profileUpdateRequest.youtubeUrl
}
if (profileUpdateRequest.instagramUrl != null) {
member.instagramUrl = profileUpdateRequest.instagramUrl
}
if (profileUpdateRequest.fancimmUrl != null) {
member.fancimmUrl = profileUpdateRequest.fancimmUrl
}
if (profileUpdateRequest.xUrl != null) {
member.xUrl = profileUpdateRequest.xUrl
}
if (profileUpdateRequest.kakaoOpenChatUrl != null) {
member.websiteUrl = profileUpdateRequest.kakaoOpenChatUrl
}
if (profileUpdateRequest.isVisibleDonationRank != null) {
member.isVisibleDonationRank = profileUpdateRequest.isVisibleDonationRank
}
if (profileUpdateRequest.donationRankingPeriod != null) {
member.donationRankingPeriod = profileUpdateRequest.donationRankingPeriod
}
return ProfileResponse(member, cloudFrontHost, profileUpdateRequest.container)
}
@Transactional
fun profileImageUpdate(multipartFile: MultipartFile, user: User): String {
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
val metadata = ObjectMetadata()
metadata.contentLength = multipartFile.size
member.profileImage = s3Uploader.upload(
inputStream = multipartFile.inputStream,
bucket = s3Bucket,
filePath = "profile/${member.id}/${generateFileName(prefix = "${member.id}-profile")}",
metadata = metadata
)
return "$cloudFrontHost/${member.profileImage!!}"
}
@Transactional
fun forgotPassword(request: ForgotPasswordRequest) {
val member = repository.getMemberByEmail(email = request.email)
?: throw SodaException(messageKey = "member.validation.unregistered_account_retry")
if (member.provider != MemberProvider.EMAIL) {
val provider = resolveProviderLabel(member.provider)
throw SodaException(message = formatMessage("member.validation.email_registered_with_provider", provider))
}
val password = generatePassword(12)
member.password = passwordEncoder.encode(password)
val date = LocalDateTime.now()
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일, HH:mm"))
val data = HashMap<String, String>()
data["password"] = password
data["date"] = date
val templateData = objectMapper.writeValueAsString(data)
emailService.sendTemplatedEmail(
template = "sodalive-find-password",
templateData = templateData,
receiver = request.email
)
}
fun getMemberProfile(memberId: Long, myMemberId: Long): GetMemberProfileResponse {
return repository.getMemberProfile(memberId, myMemberId)
}
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang) ?: ""
return if (args.isEmpty()) template else String.format(template, *args)
}
private fun resolveProviderLabel(provider: MemberProvider): String {
val key = when (provider) {
MemberProvider.EMAIL -> "member.provider.email"
MemberProvider.KAKAO -> "member.provider.kakao"
MemberProvider.GOOGLE -> "member.provider.google"
MemberProvider.APPLE -> "member.provider.apple"
MemberProvider.LINE -> "member.provider.line"
}
return messageSource.getMessage(key, langContext.lang) ?: provider.name
}
private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock {
return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() }
}
private fun evictRecommendLiveCache(memberId: Long) {
cacheManager.getCache("cache_ttl_3_hours")?.evict(recommendLiveCacheKeyPrefix + memberId)
}
@Transactional
fun updateMarketingInfo(memberId: Long, adid: String, pid: String): String? {
val member = repository.findByIdOrNull(id = memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (adid != member.adid) {
member.adid = adid
}
if (pid != member.activePid && pid.isNotBlank()) {
member.activePid = pid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
return pid
}
return member.activePid
}
@Transactional
fun findOrRegister(
googleUserInfo: GoogleUserInfo,
container: String,
marketingPid: String?,
pushToken: String?
): MemberResolveResult {
val findMember = repository.findByGoogleId(googleUserInfo.sub)
if (findMember != null) {
if (findMember.isActive) {
return MemberResolveResult(member = findMember, isNew = false)
} else {
throw SodaException(messageKey = "member.validation.inactive_account")
}
}
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val email = googleUserInfo.email
checkEmail(email)
val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member(
googleId = googleUserInfo.sub,
email = email,
password = "",
nickname = nickname,
profileImage = "profile/default-profile.png",
gender = Gender.NONE,
provider = MemberProvider.GOOGLE,
container = container,
countryCode = countryContext.countryCode
)
if (!marketingPid.isNullOrBlank()) {
member.activePid = marketingPid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
}
repository.save(member)
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
if (pushToken != null) {
pushTokenService.registerToken(
memberId = member.id!!,
token = pushToken,
deviceType = container
)
}
return MemberResolveResult(member = member, isNew = true)
}
@Transactional
fun findOrRegister(
kakaoUserInfo: KakaoUserInfo,
container: String,
marketingPid: String?,
pushToken: String?
): MemberResolveResult {
val findMember = repository.findByKakaoId(kakaoUserInfo.id)
if (findMember != null) {
if (findMember.isActive) {
return MemberResolveResult(member = findMember, isNew = false)
} else {
throw SodaException(messageKey = "member.validation.inactive_account")
}
}
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val email = kakaoUserInfo.email
checkEmail(email)
val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member(
kakaoId = kakaoUserInfo.id,
email = email,
password = "",
nickname = nickname,
profileImage = "profile/default-profile.png",
gender = Gender.NONE,
provider = MemberProvider.KAKAO,
container = container,
countryCode = countryContext.countryCode
)
if (!marketingPid.isNullOrBlank()) {
member.activePid = marketingPid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
}
repository.save(member)
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
if (pushToken != null) {
pushTokenService.registerToken(
memberId = member.id!!,
token = pushToken,
deviceType = container
)
}
return MemberResolveResult(member = member, isNew = true)
}
@Transactional
fun findOrRegister(
appleUserInfo: AppleUserInfo,
container: String,
marketingPid: String?,
pushToken: String?
): MemberResolveResult {
val findMember = repository.findByAppleId(appleUserInfo.sub)
if (findMember != null) {
if (findMember.isActive) {
return MemberResolveResult(member = findMember, isNew = false)
} else {
throw SodaException(messageKey = "member.validation.inactive_account")
}
}
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val email = appleUserInfo.email
checkEmail(email)
val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member(
appleId = appleUserInfo.sub,
email = email,
password = "",
nickname = nickname,
profileImage = "profile/default-profile.png",
gender = Gender.NONE,
provider = MemberProvider.APPLE,
container = container,
countryCode = countryContext.countryCode
)
if (!marketingPid.isNullOrBlank()) {
member.activePid = marketingPid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
}
repository.save(member)
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
if (pushToken != null) {
pushTokenService.registerToken(
memberId = member.id!!,
token = pushToken,
deviceType = container
)
}
return MemberResolveResult(member = member, isNew = true)
}
@Transactional
fun findOrRegister(
lineUserInfo: LineUserInfo,
container: String,
marketingPid: String?,
pushToken: String?
): MemberResolveResult {
val findMember = repository.findByLineId(lineUserInfo.sub)
if (findMember != null) {
if (findMember.isActive) {
return MemberResolveResult(member = findMember, isNew = false)
} else {
throw SodaException(messageKey = "member.validation.inactive_account")
}
}
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
val email = lineUserInfo.email
checkEmail(email)
val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member(
lineId = lineUserInfo.sub,
email = email,
password = "",
nickname = nickname,
profileImage = "profile/default-profile.png",
gender = Gender.NONE,
provider = MemberProvider.LINE,
container = container,
countryCode = countryContext.countryCode
)
if (!marketingPid.isNullOrBlank()) {
member.activePid = marketingPid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
}
repository.save(member)
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
if (pushToken != null) {
pushTokenService.registerToken(
memberId = member.id!!,
token = pushToken,
deviceType = container
)
}
return MemberResolveResult(member = member, isNew = true)
}
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) {
val provider = resolveProviderLabel(member.provider)
throw SodaException(message = formatMessage("member.validation.email_registered_with_provider", provider))
}
}
}