package kr.co.vividnext.sodalive.member import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType import kr.co.vividnext.sodalive.marketing.AdTrackingService import kr.co.vividnext.sodalive.member.block.MemberBlockRequest import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest import kr.co.vividnext.sodalive.member.login.LoginRequest import kr.co.vividnext.sodalive.member.login.LoginResponse import kr.co.vividnext.sodalive.member.login.SocialLoginRequest import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2 import kr.co.vividnext.sodalive.member.social.SocialAuthServiceResolver import kr.co.vividnext.sodalive.useraction.ActionType import kr.co.vividnext.sodalive.useraction.UserActionService import org.springframework.data.domain.Pageable 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.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/member") class MemberController( private val service: MemberService, private val socialAuthServiceResolver: SocialAuthServiceResolver, private val trackingService: AdTrackingService, private val userActionService: UserActionService, private val messageSource: SodaMessageSource, private val langContext: LangContext ) { @GetMapping("/check/email") fun checkEmail(@RequestParam email: String) = service.duplicateCheckEmail(email) @GetMapping("/check/nickname") fun checkNickname(@RequestParam nickname: String) = service.duplicateCheckNickname(nickname) @PutMapping("/change/nickname") fun changeNickname( @RequestBody profileUpdateRequest: ProfileUpdateRequest, @AuthenticationPrincipal user: User ) = ApiResponse.ok(service.updateNickname(profileUpdateRequest, user)) @PostMapping("/signup/v2") fun signupV2(@RequestBody request: SignUpRequestV2): ApiResponse { val response = service.signUpV2(request) if (!response.marketingPid.isNullOrBlank()) { trackingService.saveTrackingHistory( pid = response.marketingPid, type = AdTrackingHistoryType.SIGNUP, memberId = response.memberId ) } userActionService.recordAction( memberId = response.memberId, isAuth = false, actionType = ActionType.SIGN_UP ) val message = messageSource.getMessage("member.signup.success", langContext.lang) return ApiResponse.ok(message = message, data = response.loginResponse) } @PostMapping("/signup") fun signUp( @RequestPart("profileImage", required = false) profileImage: MultipartFile? = null, @RequestPart("request") requestString: String ): ApiResponse { val response = service.signUp(profileImage, requestString) if (!response.marketingPid.isNullOrBlank()) { trackingService.saveTrackingHistory( pid = response.marketingPid, type = AdTrackingHistoryType.SIGNUP, memberId = response.memberId ) } val message = messageSource.getMessage("member.signup.success", langContext.lang) return ApiResponse.ok(message = message, data = response.loginResponse) } @PostMapping("/login") fun login(@RequestBody loginRequest: LoginRequest) = service.login(loginRequest) @PostMapping("/logout") fun logout( @RequestHeader("Authorization") token: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.logout(token.removePrefix("Bearer "), member.id!!)) } @PostMapping("/logout/all") fun logoutAll( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.logoutAll(member.id!!)) } @GetMapping fun getMember( @RequestParam container: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getMember(member.id!!, container)) } @GetMapping("/info") fun getMemberInfo( @RequestParam container: String?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getMemberInfo(member, container ?: "web")) } @PostMapping("/notification") fun updateNotificationSettings( @RequestBody request: UpdateNotificationSettingRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.updateNotificationSettings(request, member)) } @PutMapping("/push-token/update") fun updatePushToken( @RequestBody pushTokenUpdateRequest: PushTokenUpdateRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.updatePushToken( memberId = member.id!!, pushToken = pushTokenUpdateRequest.pushToken, container = pushTokenUpdateRequest.container ) ) } @PutMapping("/marketing-info/update") fun updateMarketingInfo( @RequestBody request: MarketingInfoUpdateRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") val memberId = member.id!! val marketingPid = service.updateMarketingInfo( memberId = memberId, adid = request.adid, pid = request.pid ) if (!marketingPid.isNullOrBlank()) { trackingService.saveTrackingHistory( pid = marketingPid, type = AdTrackingHistoryType.LOGIN, memberId = memberId ) } ApiResponse.ok(Unit) } @PutMapping("/adid/update") fun updateAdid( @RequestBody request: AdidUpdateRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.updateAdid( memberId = member.id!!, adid = request.adid ) ) } @GetMapping("/mypage") fun getMyPage( @RequestParam container: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getMyPage(member, container)) } @PostMapping("/creator/follow") fun creatorFollow( @RequestBody request: CreatorFollowRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.creatorFollow( creatorId = request.creatorId, isNotify = request.isNotify ?: true, isActive = request.isActive ?: true, memberId = member.id!! ) ) } @PostMapping("/creator/unfollow") fun creatorUnFollow( @RequestBody request: CreatorFollowRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.creatorUnFollow(creatorId = request.creatorId, memberId = member.id!!)) } @GetMapping("/block/id") fun getBlockedMemberIdList( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getBlockedMemberIdList(member.id!!)) } @GetMapping("/block") fun getBlockedMemberList( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getBlockedMemberList( member.id!!, offset = pageable.offset, limit = pageable.pageSize.toLong() ) ) } @PostMapping("/block") fun memberBlock( @RequestBody request: MemberBlockRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.memberBlock(request = request, memberId = member.id!!)) } @PostMapping("/unblock") fun memberUnBlock( @RequestBody request: MemberBlockRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.memberUnBlock(request = request, memberId = member.id!!)) } @GetMapping("/search") fun searchMember( @RequestParam nickname: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.searchMember(nickname = nickname, member = member)) } @PostMapping("/sign_out") fun signOut( @RequestBody signOutRequest: SignOutRequest, @AuthenticationPrincipal user: User ) = ApiResponse.ok( service.signOut(signOutRequest, user), messageSource.getMessage("member.signout.success", langContext.lang) ) @GetMapping("/change/nickname/price") fun getChangeNicknamePrice( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getChangeNicknamePrice(memberId = member.id!!)) } @PutMapping fun profileUpdate( @RequestBody profileUpdateRequest: ProfileUpdateRequest, @AuthenticationPrincipal user: User ) = ApiResponse.ok(service.profileUpdate(profileUpdateRequest, user)) @PostMapping("/image") fun profileImageUpdate( @RequestParam("image") multipartFile: MultipartFile, @AuthenticationPrincipal user: User ) = ApiResponse.ok(service.profileImageUpdate(multipartFile, user)) @PostMapping("/forgot-password") fun forgotPassword( @RequestBody request: ForgotPasswordRequest ) = ApiResponse.ok(service.forgotPassword(request)) @GetMapping("/profile/{id}") fun getMemberProfile( @PathVariable id: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getMemberProfile(memberId = id, myMemberId = member.id!!)) } @PostMapping("/login/google") fun loginGoogle( @RequestHeader("Authorization") authHeader: String, @RequestBody request: SocialLoginRequest ): ApiResponse { val token = extractBearerToken(authHeader, MemberProvider.GOOGLE) return processSocialLogin(MemberProvider.GOOGLE, token, request, null) } @PostMapping("/login/kakao") fun loginKakao( @RequestHeader("Authorization") authHeader: String, @RequestBody request: SocialLoginRequest ): ApiResponse { val token = extractBearerToken(authHeader, MemberProvider.KAKAO) return processSocialLogin(MemberProvider.KAKAO, token, request, null) } @PostMapping("/login/apple") fun loginApple( @RequestBody request: SocialLoginRequest ): ApiResponse { val errorKey = socialLoginErrorKey(MemberProvider.APPLE) val token = request.identityToken?.takeIf { it.isNotBlank() } ?: throw SodaException(messageKey = errorKey) val nonce = request.nonce?.takeIf { it.isNotBlank() } ?: throw SodaException(messageKey = errorKey) return processSocialLogin(MemberProvider.APPLE, token, request, nonce) } @PostMapping("/login/line") fun loginLine( @RequestBody request: SocialLoginRequest ): ApiResponse { val errorKey = socialLoginErrorKey(MemberProvider.LINE) val token = request.identityToken?.takeIf { it.isNotBlank() } ?: throw SodaException(messageKey = errorKey) val nonce = request.nonce?.takeIf { it.isNotBlank() } ?: throw SodaException(messageKey = errorKey) return processSocialLogin(MemberProvider.LINE, token, request, nonce) } private fun processSocialLogin( provider: MemberProvider, token: String, request: SocialLoginRequest, nonce: String? ): ApiResponse { val authService = socialAuthServiceResolver.resolve(provider) val response = authService.authenticate( token = token, container = request.container, marketingPid = request.marketingPid, pushToken = request.pushToken, nonce = nonce ) if (!response.marketingPid.isNullOrBlank()) { trackingService.saveTrackingHistory( pid = response.marketingPid, type = AdTrackingHistoryType.SIGNUP, memberId = response.memberId ) } if (response.isNew) { userActionService.recordAction( memberId = response.memberId, isAuth = false, actionType = ActionType.SIGN_UP ) } val message = messageSource.getMessage("member.signup.success", langContext.lang) return ApiResponse.ok(message = message, data = response.loginResponse) } private fun extractBearerToken(authHeader: String, provider: MemberProvider): String { val errorKey = socialLoginErrorKey(provider) if (!authHeader.startsWith("Bearer ")) { throw SodaException(messageKey = errorKey) } return authHeader.substring(7) } private fun socialLoginErrorKey(provider: MemberProvider): String { return when (provider) { MemberProvider.GOOGLE -> "member.social.google_login_failed" MemberProvider.KAKAO -> "member.social.kakao_login_failed" MemberProvider.APPLE -> "member.social.apple_login_failed" MemberProvider.LINE -> "member.social.line_login_failed" else -> "common.error.bad_request" } } }