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.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.jwt.TokenProvider 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.notification.MemberNotificationService import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.member.signUp.SignUpRequest 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.utils.generateFileName import org.springframework.beans.factory.annotation.Value 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.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 @Service @Transactional(readOnly = true) class MemberService( private val repository: MemberRepository, private val stipulationRepository: StipulationRepository, private val stipulationAgreeRepository: StipulationAgreeRepository, 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 objectMapper: ObjectMapper, @Value("\${cloud.aws.s3.bucket}") private val s3Bucket: String ) : UserDetailsService { @Transactional fun signUp( profileImage: MultipartFile?, requestString: String ): ApiResponse { val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID) ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") val request = objectMapper.readValue(requestString, SignUpRequest::class.java) if (!request.isAgreePrivacyPolicy || !request.isAgreeTermsOfService) { throw SodaException("약관에 동의하셔야 회원가입이 가능합니다.") } 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 ApiResponse.ok(message = "회원가입을 축하드립니다.", data = login(request.email, request.password)) } fun login(request: LoginRequest): ApiResponse { return ApiResponse.ok( message = "로그인 되었습니다.", data = login(request.email, request.password, request.isAdmin, request.isCreator) ) } fun getMemberInfo(member: Member, container: String): GetMemberInfoResponse { return GetMemberInfoResponse( can = member.getChargeCan(container) + member.getRewardCan(container), isAuth = member.auth != null, role = member.role, messageNotice = member.notification?.message, followingChannelLiveNotice = member.notification?.live, followingChannelUploadContentNotice = member.notification?.uploadContent ) } @Transactional fun updateNotificationSettings(request: UpdateNotificationSettingRequest, member: Member) { memberNotificationService.updateNotification( live = request.live, uploadContent = request.uploadContent, message = request.message, member = member ) } private fun login( email: String, password: String, isAdmin: Boolean = false, isCreator: Boolean = false ): LoginResponse { val member = repository.findByEmail(email = email) ?: throw SodaException("로그인 정보를 확인해주세요.") if (!member.isActive) { throw SodaException("탈퇴한 계정입니다.\n고객센터로 문의해 주시기 바랍니다.") } if (isCreator && member.role != MemberRole.CREATOR) { throw SodaException("로그인 정보를 확인해주세요.") } if (isAdmin && member.role != MemberRole.ADMIN) { throw SodaException("로그인 정보를 확인해주세요.") } 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 = member.profileImage ?: "" ) } 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 ) 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 { validateEmail(email) repository.findByEmail(email)?.let { throw SodaException("이미 사용중인 이메일 입니다.", "email") } return ApiResponse.ok(message = "사용 가능한 이메일 입니다.") } private fun validateEmail(email: String) { val emailValidationMessage = validator.emailValidation(email) if (emailValidationMessage.trim().isNotEmpty()) { throw SodaException(emailValidationMessage, "email") } } fun duplicateCheckNickname(nickname: String): ApiResponse { validateNickname(nickname) repository.findByNickname(nickname)?.let { throw SodaException("이미 사용중인 닉네임 입니다.", "nickname") } return ApiResponse.ok(message = "사용 가능한 닉네임 입니다.") } 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 = repository.findByEmail(email = username) ?: throw UsernameNotFoundException(username) return MemberAdapter(member) } }