From 67b909daedfa74398282147bddadf44c82b5b40f Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 23 Dec 2025 13:26:15 +0900 Subject: [PATCH] =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회원/인증 API 응답 메시지를 다국어 키로 분리함. --- .../sodalive/i18n/SodaMessageSource.kt | 199 +++++++++++++++++- .../sodalive/member/MemberController.kt | 43 ++-- .../sodalive/member/MemberService.kt | 161 +++++++------- .../sodalive/member/auth/AuthController.kt | 4 +- .../sodalive/member/auth/AuthService.kt | 28 ++- .../nickname/NicknameGenerateService.kt | 2 +- .../member/social/google/GoogleAuthService.kt | 2 +- .../member/social/google/GoogleService.kt | 2 +- .../member/social/kakao/KakaoAuthService.kt | 2 +- .../member/social/kakao/KakaoService.kt | 2 +- .../member/stipulation/StipulationService.kt | 6 +- .../member/tag/MemberTagController.kt | 2 +- 12 files changed, 338 insertions(+), 115 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt index cf1f9264..49e52017 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt @@ -471,6 +471,197 @@ class SodaMessageSource { ) ) + private val memberAuthMessages = mapOf( + "member.auth.blocked_policy" to mapOf( + Lang.KO to "운영정책을 위반하여 이용을 제한합니다.", + Lang.EN to "Your access is restricted due to policy violations.", + Lang.JA to "運営ポリシー違反のため利用が制限されています。" + ), + "member.auth.already_verified" to mapOf( + Lang.KO to "이미 인증된 계정입니다.", + Lang.EN to "This account is already verified.", + Lang.JA to "既に認証済みのアカウントです。" + ), + "member.auth.certificate_invalid_retry" to mapOf( + Lang.KO to "인증정보에 오류가 있습니다.\n다시 시도해 주세요.", + Lang.EN to "There is an error with the verification information.\nPlease try again.", + Lang.JA to "認証情報にエラーがあります。\nもう一度お試しください。" + ), + "member.auth.max_accounts" to mapOf( + Lang.KO to "이미 본인인증한 계정 %s개 이용중입니다.\n" + + "소다라이브의 본인인증은 최대 3개의 계정만 이용할 수 있습니다.", + Lang.EN to "You are already using %s verified account(s).\n" + + "Identity verification is limited to up to 3 accounts on Sodalive.", + Lang.JA to "本人認証済みのアカウントを%s件利用中です。\n" + + "ソダライブの本人認証は最大3アカウントまでです。" + ), + "member.auth.age_limit" to mapOf( + Lang.KO to "%s년 1월 1일 이전 출생자만 본인인증이 가능합니다.", + Lang.EN to "Only users born on or before January 1, %s can be verified.", + Lang.JA to "%s年1月1日以前に生まれた方のみ本人認証が可能です。" + ) + ) + + private val memberMessages = mapOf( + "member.signup.failed_retry" to mapOf( + Lang.KO to "회원가입을 하지 못했습니다.\n다시 시도해 주세요.", + Lang.EN to "Sign up failed.\nPlease try again.", + Lang.JA to "会員登録に失敗しました。\nもう一度お試しください。" + ), + "member.signup.success" to mapOf( + Lang.KO to "회원가입을 축하드립니다.", + Lang.EN to "Congratulations on your sign up.", + Lang.JA to "ご登録おめでとうございます。" + ), + "member.login.success" to mapOf( + Lang.KO to "로그인 되었습니다.", + Lang.EN to "You are logged in.", + Lang.JA to "ログインしました。" + ), + "member.signout.success" to mapOf( + Lang.KO to "정상적으로 탈퇴 처리되었습니다.", + Lang.EN to "Your account has been successfully deleted.", + Lang.JA to "正常に退会処理されました。" + ) + ) + + private val memberValidationMessages = mapOf( + "member.validation.invalid_request_retry" to mapOf( + Lang.KO to "잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.", + Lang.EN to "Invalid request.\nPlease close the app and try again.", + Lang.JA to "不正なリクエストです。\nアプリを終了して再度お試しください。" + ), + "member.validation.agree_required" to mapOf( + Lang.KO to "약관에 동의하셔야 회원가입이 가능합니다.", + Lang.EN to "You must agree to the terms to sign up.", + Lang.JA to "会員登録には規約への同意が必要です。" + ), + "member.validation.user_not_found" to mapOf( + Lang.KO to "없는 사용자 입니다.", + Lang.EN to "User not found.", + Lang.JA to "ユーザーが見つかりません。" + ), + "member.validation.account_not_found" to mapOf( + Lang.KO to "없는 계정입니다.", + Lang.EN to "Account not found.", + Lang.JA to "アカウントが見つかりません。" + ), + "member.validation.inactive_account" to mapOf( + Lang.KO to "탈퇴한 계정입니다.\n고객센터로 문의해 주시기 바랍니다.", + Lang.EN to "This account has been deleted.\nPlease contact customer support.", + Lang.JA to "退会したアカウントです。\nカスタマーサポートへお問い合わせください。" + ), + "member.validation.creator_not_found" to mapOf( + Lang.KO to "크리에이터 정보를 확인해주세요.", + Lang.EN to "Please check the creator information.", + Lang.JA to "クリエイター情報を確認してください。" + ), + "member.validation.nickname_min_length" to mapOf( + Lang.KO to "두 글자 이상 입력 하셔야 합니다.", + Lang.EN to "Please enter at least 2 characters.", + Lang.JA to "2文字以上入力してください。" + ), + "member.validation.password_mismatch" to mapOf( + Lang.KO to "비밀번호가 일치하지 않습니다.", + Lang.EN to "Password does not match.", + Lang.JA to "パスワードが一致しません。" + ), + "member.validation.signout_reason_required" to mapOf( + Lang.KO to "탈퇴하려는 이유를 입력해 주세요.", + Lang.EN to "Please enter a reason for deleting your account.", + Lang.JA to "退会理由を入力してください。" + ), + "member.validation.email_available" to mapOf( + Lang.KO to "사용 가능한 이메일 입니다.", + Lang.EN to "This email is available.", + Lang.JA to "使用可能なメールアドレスです。" + ), + "member.validation.nickname_available" to mapOf( + Lang.KO to "사용 가능한 닉네임 입니다.", + Lang.EN to "This nickname is available.", + Lang.JA to "使用可能なニックネームです。" + ), + "member.validation.email_in_use" to mapOf( + Lang.KO to "이미 사용중인 이메일 입니다.", + Lang.EN to "This email is already in use.", + Lang.JA to "このメールアドレスは既に使用されています。" + ), + "member.validation.nickname_in_use" to mapOf( + Lang.KO to "이미 사용중인 닉네임 입니다.", + Lang.EN to "This nickname is already in use.", + Lang.JA to "このニックネームは既に使用されています。" + ), + "member.validation.email_registered_with_provider" to mapOf( + Lang.KO to "해당 이메일은 %s 계정으로 가입되어 있습니다. 해당 소셜 로그인을 사용해 주세요.", + Lang.EN to "This email is registered with a %s account. Please use that social login.", + Lang.JA to "このメールアドレスは%sアカウントで登録されています。該当のソーシャルログインをご利用ください。" + ), + "member.validation.email_registered_with_provider_already" to mapOf( + Lang.KO to "해당 이메일은 %s 계정으로 이미 가입되어 있습니다. 해당 소셜 로그인을 사용해 주세요.", + Lang.EN to "This email is already registered with a %s account. Please use that social login.", + Lang.JA to "このメールアドレスは既に%sアカウントで登録されています。該当のソーシャルログインをご利用ください。" + ), + "member.validation.unregistered_account_retry" to mapOf( + Lang.KO to "등록되지 않은 계정입니다.\n확인 후 다시 시도해 주세요.", + Lang.EN to "This account is not registered.\nPlease check and try again.", + Lang.JA to "登録されていないアカウントです。\n確認してもう一度お試しください。" + ) + ) + + private val memberSocialMessages = mapOf( + "member.social.google_login_failed" to mapOf( + Lang.KO to "구글 로그인을 하지 못했습니다. 다시 시도해 주세요", + Lang.EN to "Google login failed. Please try again.", + Lang.JA to "Googleログインに失敗しました。もう一度お試しください。" + ), + "member.social.kakao_login_failed" to mapOf( + Lang.KO to "카카오 로그인을 하지 못했습니다. 다시 시도해 주세요", + Lang.EN to "Kakao login failed. Please try again.", + Lang.JA to "カカオログインに失敗しました。もう一度お試しください。" + ), + "member.social.email_consent_required" to mapOf( + Lang.KO to "이메일 제공에 동의하셔야 서비스 이용이 가능합니다.", + Lang.EN to "You must agree to provide your email to use the service.", + Lang.JA to "サービス利用にはメール提供への同意が必要です。" + ) + ) + + private val memberProviderMessages = mapOf( + "member.provider.email" to mapOf( + Lang.KO to "이메일", + Lang.EN to "Email", + Lang.JA to "メール" + ), + "member.provider.kakao" to mapOf( + Lang.KO to "카카오", + Lang.EN to "Kakao", + Lang.JA to "カカオ" + ), + "member.provider.google" to mapOf( + Lang.KO to "구글", + Lang.EN to "Google", + Lang.JA to "Google" + ), + "member.provider.apple" to mapOf( + Lang.KO to "애플", + Lang.EN to "Apple", + Lang.JA to "Apple" + ) + ) + + private val memberGenderMessages = mapOf( + "member.gender.male" to mapOf( + Lang.KO to "남", + Lang.EN to "Male", + Lang.JA to "男性" + ), + "member.gender.female" to mapOf( + Lang.KO to "여", + Lang.EN to "Female", + Lang.JA to "女性" + ) + ) + fun getMessage(key: String, lang: Lang): String? { val messageGroups = listOf( commonMessages, @@ -496,7 +687,13 @@ class SodaMessageSource { messageMessages, noticeMessages, reportMessages, - imageValidationMessages + imageValidationMessages, + memberAuthMessages, + memberMessages, + memberValidationMessages, + memberSocialMessages, + memberProviderMessages, + memberGenderMessages ) for (messages in messageGroups) { val translations = messages[key] ?: continue diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index e6a094a5..3bc333ae 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -2,6 +2,8 @@ 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 @@ -37,7 +39,9 @@ class MemberController( private val kakaoAuthService: KakaoAuthService, private val googleAuthService: GoogleAuthService, private val trackingService: AdTrackingService, - private val userActionService: UserActionService + private val userActionService: UserActionService, + private val messageSource: SodaMessageSource, + private val langContext: LangContext ) { @GetMapping("/check/email") fun checkEmail(@RequestParam email: String) = service.duplicateCheckEmail(email) @@ -69,7 +73,8 @@ class MemberController( actionType = ActionType.SIGN_UP ) - return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) + val message = messageSource.getMessage("member.signup.success", langContext.lang) + return ApiResponse.ok(message = message, data = response.loginResponse) } @PostMapping("/signup") @@ -87,7 +92,8 @@ class MemberController( ) } - return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) + val message = messageSource.getMessage("member.signup.success", langContext.lang) + return ApiResponse.ok(message = message, data = response.loginResponse) } @PostMapping("/login") @@ -230,7 +236,7 @@ class MemberController( @RequestBody request: CreatorFollowRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.creatorUnFollow(creatorId = request.creatorId, memberId = member.id!!)) } @@ -240,7 +246,7 @@ class MemberController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getBlockedMemberIdList(member.id!!)) } @@ -250,7 +256,7 @@ class MemberController( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok( service.getBlockedMemberList( @@ -266,7 +272,7 @@ class MemberController( @RequestBody request: MemberBlockRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.memberBlock(request = request, memberId = member.id!!)) } @@ -276,7 +282,7 @@ class MemberController( @RequestBody request: MemberBlockRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.memberUnBlock(request = request, memberId = member.id!!)) } @@ -286,7 +292,7 @@ class MemberController( @RequestParam nickname: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.searchMember(nickname = nickname, member = member)) } @@ -295,13 +301,16 @@ class MemberController( fun signOut( @RequestBody signOutRequest: SignOutRequest, @AuthenticationPrincipal user: User - ) = ApiResponse.ok(service.signOut(signOutRequest, 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("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getChangeNicknamePrice(memberId = member.id!!)) } @@ -327,7 +336,7 @@ class MemberController( @PathVariable id: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getMemberProfile(memberId = id, myMemberId = member.id!!)) } @@ -337,7 +346,7 @@ class MemberController( @RequestBody request: SocialLoginRequest ): ApiResponse { if (!authHeader.startsWith("Bearer ")) { - throw SodaException("구글 로그인을 하지 못했습니다. 다시 시도해 주세요") + throw SodaException(messageKey = "member.social.google_login_failed") } val token = authHeader.substring(7) @@ -359,7 +368,8 @@ class MemberController( ) } - return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) + val message = messageSource.getMessage("member.signup.success", langContext.lang) + return ApiResponse.ok(message = message, data = response.loginResponse) } @PostMapping("/login/kakao") @@ -368,7 +378,7 @@ class MemberController( @RequestBody request: SocialLoginRequest ): ApiResponse { if (!authHeader.startsWith("Bearer ")) { - throw SodaException("카카오 로그인을 하지 못했습니다. 다시 시도해 주세요") + throw SodaException(messageKey = "member.social.kakao_login_failed") } val token = authHeader.substring(7) @@ -390,6 +400,7 @@ class MemberController( ) } - return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) + val message = messageSource.getMessage("member.signup.success", langContext.lang) + return ApiResponse.ok(message = message, data = response.loginResponse) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 7b84fac1..f1d34e63 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -11,6 +11,8 @@ 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 @@ -95,6 +97,9 @@ class MemberService( private val passwordEncoder: PasswordEncoder, private val authenticationManagerBuilder: AuthenticationManagerBuilder, + private val messageSource: SodaMessageSource, + private val langContext: LangContext, + private val objectMapper: ObjectMapper, @Value("\${cloud.aws.s3.bucket}") @@ -109,13 +114,13 @@ class MemberService( @Transactional fun signUpV2(request: SignUpRequestV2): SignUpResponse { val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") if (!request.isAgreePrivacyPolicy || !request.isAgreeTermsOfService) { - throw SodaException("약관에 동의하셔야 회원가입이 가능합니다.") + throw SodaException(messageKey = "member.validation.agree_required") } duplicateCheckEmail(request.email) @@ -160,14 +165,14 @@ class MemberService( requestString: String ): SignUpResponse { val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") val request = objectMapper.readValue(requestString, SignUpRequest::class.java) if (!request.isAgreePrivacyPolicy || !request.isAgreeTermsOfService) { - throw SodaException("약관에 동의하셔야 회원가입이 가능합니다.") + throw SodaException(messageKey = "member.validation.agree_required") } validatePassword(request.password) @@ -187,14 +192,14 @@ class MemberService( fun login(request: LoginRequest): ApiResponse { return ApiResponse.ok( - message = "로그인 되었습니다.", + 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("없는 사용자 입니다.") + ?: throw SodaException(messageKey = "member.validation.user_not_found") return ProfileResponse(member, cloudFrontHost, container) } @@ -202,9 +207,9 @@ class MemberService( 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 @@ -260,7 +265,7 @@ class MemberService( @Transactional fun updateAdid(memberId: Long, adid: String) { val member = repository.findByIdOrNull(id = memberId) - ?: throw SodaException("로그인 정보를 확인해주세요.") + ?: throw SodaException(messageKey = "common.error.bad_credentials") member.adid = adid } @@ -305,28 +310,24 @@ class MemberService( isAdmin: Boolean = false, isCreator: Boolean = false ): LoginResponse { - val member = repository.findByEmail(email = email) ?: throw SodaException("없는 계정입니다.") + val member = repository.findByEmail(email = email) + ?: throw SodaException(messageKey = "member.validation.account_not_found") if (!member.isActive) { - throw SodaException("탈퇴한 계정입니다.\n고객센터로 문의해 주시기 바랍니다.") + throw SodaException(messageKey = "member.validation.inactive_account") } if (member.provider != MemberProvider.EMAIL) { - val provider = when (member.provider) { - MemberProvider.APPLE -> "애플" - MemberProvider.GOOGLE -> "구글" - else -> "카카오" - } - - throw SodaException("해당 이메일은 $provider 계정으로 가입되어 있습니다. 해당 소셜 로그인을 사용해 주세요.") + val provider = resolveProviderLabel(member.provider) + throw SodaException(message = formatMessage("member.validation.email_registered_with_provider", provider)) } if (isCreator && member.role != MemberRole.CREATOR) { - throw SodaException("로그인 정보를 확인해주세요.") + throw SodaException(messageKey = "common.error.bad_credentials") } if (isAdmin && member.role != MemberRole.ADMIN) { - throw SodaException("로그인 정보를 확인해주세요.") + throw SodaException(messageKey = "common.error.bad_credentials") } val authenticationToken = UsernamePasswordAuthenticationToken(email, password) @@ -414,22 +415,17 @@ class MemberService( if (findMember != null) { if (findMember.provider == MemberProvider.EMAIL) { - throw SodaException("이미 사용중인 이메일 입니다.", "email") + throw SodaException(messageKey = "member.validation.email_in_use", errorProperty = "email") } else { - val provider = when (findMember.provider) { - MemberProvider.APPLE -> "애플" - MemberProvider.GOOGLE -> "구글" - else -> "카카오" - } - + val provider = resolveProviderLabel(findMember.provider) throw SodaException( - "해당 이메일은 $provider 계정으로 이미 가입되어 있습니다. 해당 소셜 로그인을 사용해 주세요.", - "email" + message = formatMessage("member.validation.email_registered_with_provider_already", provider), + errorProperty = "email" ) } } - return ApiResponse.ok(message = "사용 가능한 이메일 입니다.") + return ApiResponse.ok(message = messageSource.getMessage("member.validation.email_available", langContext.lang)) } private fun validateEmail(email: String) { @@ -441,8 +437,9 @@ class MemberService( fun duplicateCheckNickname(nickname: String): ApiResponse { validateNickname(nickname) - repository.findByNickname(nickname)?.let { throw SodaException("이미 사용중인 닉네임 입니다.", "nickname") } - return ApiResponse.ok(message = "사용 가능한 닉네임 입니다.") + 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) { @@ -469,8 +466,10 @@ class MemberService( ) if (creatorFollowing == null) { - val creator = repository.findByIdOrNull(creatorId) ?: throw SodaException("크리에이터 정보를 확인해주세요.") - val member = repository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") + 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 @@ -514,10 +513,10 @@ class MemberService( if (blockMember == null) { val blockedMember = repository.findByIdOrNull(id = request.blockMemberId) - ?: throw SodaException("잘못된 요청입니다.") + ?: throw SodaException(messageKey = "common.error.invalid_request") val member = repository.findByIdOrNull(id = memberId) - ?: throw SodaException("잘못된 요청입니다.") + ?: throw SodaException(messageKey = "common.error.invalid_request") blockMember = BlockMember() blockMember.member = member @@ -545,7 +544,7 @@ class MemberService( fun searchMember(nickname: String, member: Member): List { if (nickname.length < 2) { - throw SodaException("두 글자 이상 입력 하셔야 합니다.") + throw SodaException(messageKey = "member.validation.nickname_min_length") } return repository.findByNicknameAndOtherCondition(nickname, member) @@ -560,7 +559,7 @@ class MemberService( @Transactional fun logout(token: String, memberId: Long) { val member = repository.findByIdOrNull(memberId) - ?: throw SodaException("로그인 정보를 확인해주세요.") + ?: throw SodaException(messageKey = "common.error.bad_credentials") member.pushToken = null pushTokenService.logout(memberId = memberId) @@ -568,7 +567,7 @@ class MemberService( val lock = getOrCreateLock(memberId = memberId) lock.write { val memberToken = tokenRepository.findByIdOrNull(memberId) - ?: throw SodaException("로그인 정보를 확인해주세요.") + ?: throw SodaException(messageKey = "common.error.bad_credentials") memberToken.tokenSet.remove(token) tokenRepository.save(memberToken) @@ -578,7 +577,7 @@ class MemberService( @Transactional fun logoutAll(memberId: Long) { val member = repository.findByIdOrNull(memberId) - ?: throw SodaException("로그인 정보를 확인해주세요.") + ?: throw SodaException(messageKey = "common.error.bad_credentials") member.pushToken = null pushTokenService.logout(memberId = memberId) @@ -589,16 +588,17 @@ class MemberService( @Transactional fun signOut(signOutRequest: SignOutRequest, user: User) { - val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") + val member = repository.findByEmail(user.username) + ?: throw SodaException(messageKey = "common.error.bad_credentials") if ( member.provider == MemberProvider.EMAIL && !passwordEncoder.matches(signOutRequest.password, member.password) ) { - throw SodaException("비밀번호가 일치하지 않습니다.") + throw SodaException(messageKey = "member.validation.password_mismatch") } if (signOutRequest.reason.isBlank()) { - throw SodaException("탈퇴하려는 이유를 입력해 주세요.") + throw SodaException(messageKey = "member.validation.signout_reason_required") } logoutAll(memberId = member.id!!) @@ -617,15 +617,16 @@ class MemberService( @Transactional fun updateNickname(profileUpdateRequest: ProfileUpdateRequest, user: User) { if (profileUpdateRequest.email != user.username) { - throw SodaException("로그인 정보를 확인해 주세요.") + throw SodaException(messageKey = "common.error.bad_credentials") } - val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") + val member = repository.findByEmail(user.username) + ?: throw SodaException(messageKey = "common.error.bad_credentials") if (profileUpdateRequest.nickname != null) { validateNickname(profileUpdateRequest.nickname) repository.findByNickname(profileUpdateRequest.nickname) - ?.let { throw SodaException("이미 사용중인 닉네임 입니다.") } + ?.let { throw SodaException(messageKey = "member.validation.nickname_in_use") } val price = repository.getChangeNicknamePrice(memberId = member.id!!).price if (price > 0) { @@ -648,17 +649,18 @@ class MemberService( @Transactional fun profileUpdate(profileUpdateRequest: ProfileUpdateRequest, user: User): ProfileResponse { if (profileUpdateRequest.email != user.username) { - throw SodaException("로그인 정보를 확인해 주세요.") + throw SodaException(messageKey = "common.error.bad_credentials") } - val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") + val member = repository.findByEmail(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("비밀번호가 일치하지 않습니다.") + throw SodaException(messageKey = "member.validation.password_mismatch") } } @@ -669,7 +671,7 @@ class MemberService( if (profileUpdateRequest.nickname != null) { validateNickname(profileUpdateRequest.nickname) repository.findByNickname(profileUpdateRequest.nickname) - ?.let { throw SodaException("이미 사용중인 닉네임 입니다.") } + ?.let { throw SodaException(messageKey = "member.validation.nickname_in_use") } member.nickname = profileUpdateRequest.nickname } @@ -723,7 +725,8 @@ class MemberService( @Transactional fun profileImageUpdate(multipartFile: MultipartFile, user: User): String { - val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") + val member = repository.findByEmail(user.username) + ?: throw SodaException(messageKey = "common.error.bad_credentials") val metadata = ObjectMetadata() metadata.contentLength = multipartFile.size @@ -741,17 +744,11 @@ class MemberService( @Transactional fun forgotPassword(request: ForgotPasswordRequest) { val member = repository.getMemberByEmail(email = request.email) - ?: throw SodaException("등록되지 않은 계정입니다.\n확인 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.unregistered_account_retry") - val provider = when (member.provider) { - MemberProvider.EMAIL -> "이메일" - MemberProvider.KAKAO -> "카카오" - MemberProvider.GOOGLE -> "구글" - MemberProvider.APPLE -> "애플" - } - - if (provider != "이메일") { - throw SodaException("해당 계정은 $provider 계정으로 가입되어 있습니다. 해당 소셜 로그인을 사용해 주세요.") + 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) @@ -779,6 +776,21 @@ class MemberService( 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" + } + return messageSource.getMessage(key, langContext.lang) ?: provider.name + } + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } } @@ -786,7 +798,7 @@ class MemberService( @Transactional fun updateMarketingInfo(memberId: Long, adid: String, pid: String): String? { val member = repository.findByIdOrNull(id = memberId) - ?: throw SodaException("로그인 정보를 확인해주세요.") + ?: throw SodaException(messageKey = "common.error.bad_credentials") if (adid != member.adid) { member.adid = adid @@ -814,15 +826,15 @@ class MemberService( if (findMember.isActive) { return MemberResolveResult(member = findMember, isNew = false) } else { - throw SodaException("탈퇴한 계정입니다.\n고객센터로 문의해 주시기 바랍니다.") + throw SodaException(messageKey = "member.validation.inactive_account") } } val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") val email = googleUserInfo.email checkEmail(email) @@ -870,15 +882,15 @@ class MemberService( if (findMember.isActive) { return MemberResolveResult(member = findMember, isNew = false) } else { - throw SodaException("탈퇴한 계정입니다.\n고객센터로 문의해 주시기 바랍니다.") + throw SodaException(messageKey = "member.validation.inactive_account") } } val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") val email = kakaoUserInfo.email checkEmail(email) @@ -918,13 +930,8 @@ class MemberService( val member = repository.findByEmail(email) if (member != null) { - val provider = when (member.provider) { - MemberProvider.APPLE -> "애플" - MemberProvider.GOOGLE -> "구글" - else -> "카카오" - } - - throw SodaException("해당 이메일은 $provider 계정으로 가입되어 있습니다. 해당 소셜 로그인을 사용해 주세요.") + val provider = resolveProviderLabel(member.provider) + throw SodaException(message = formatMessage("member.validation.email_registered_with_provider", provider)) } } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt index b5b92981..8271e953 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt @@ -22,13 +22,13 @@ class AuthController( @RequestBody request: AuthVerifyRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") val authenticateData = service.certificate(request, memberId = member.id!!) if (service.isBlockAuth(authenticateData)) { service.signOut(member.id!!) - throw SodaException("운영정책을 위반하여 이용을 제한합니다.") + throw SodaException(messageKey = "member.auth.blocked_policy") } val authResponse = service.authenticate(authenticateData, member.id!!) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt index e736e1be..16800e38 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt @@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.member.auth import com.fasterxml.jackson.databind.ObjectMapper import kr.co.bootpay.Bootpay 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.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberService import kr.co.vividnext.sodalive.member.SignOut @@ -22,6 +24,8 @@ class AuthService( private val memberService: MemberService, private val memberRepository: MemberRepository, private val signOutRepository: SignOutRepository, + private val messageSource: SodaMessageSource, + private val langContext: LangContext, @Value("\${bootpay.application-id}") private val bootpayApplicationId: String, @@ -32,16 +36,16 @@ class AuthService( val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey) val authId = repository.getAuthIdByMemberId(memberId = memberId) - if (authId != null) throw SodaException("이미 인증된 계정입니다.") + if (authId != null) throw SodaException(messageKey = "member.auth.already_verified") val certificateResult: AuthCertificateResult = try { val token = bootpay.accessToken - if (token["error_code"] != null) throw SodaException("인증정보에 오류가 있습니다.\n다시 시도해 주세요.") + if (token["error_code"] != null) throw SodaException(messageKey = "member.auth.certificate_invalid_retry") val res = bootpay.certificate(request.receiptId) objectMapper.convertValue(res, AuthCertificateResult::class.java) } catch (e: Exception) { - throw SodaException(e.message ?: "인증정보에 오류가 있습니다.\n다시 시도해 주세요.") + throw SodaException(messageKey = "member.auth.certificate_invalid_retry") } if ( @@ -51,7 +55,7 @@ class AuthService( ) { return certificateResult.authenticateData } else { - throw SodaException("인증정보에 오류가 있습니다.\n다시 시도해 주세요.") + throw SodaException(messageKey = "member.auth.certificate_invalid_retry") } } @@ -62,11 +66,13 @@ class AuthService( @Transactional fun signOut(memberId: Long) { - val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") + val member = memberRepository.findByIdOrNull(memberId) + ?: throw SodaException(messageKey = "common.error.bad_credentials") member.isActive = false member.nickname = "deleted_${member.nickname}" - val signOut = SignOut(reason = "운영정책을 위반하여 이용을 제한합니다.") + val signOutReason = messageSource.getMessage("member.auth.blocked_policy", langContext.lang) ?: "" + val signOut = SignOut(reason = signOutReason) signOut.member = member signOutRepository.save(signOut) @@ -77,13 +83,14 @@ class AuthService( fun authenticate(certificate: AuthVerifyCertificate, memberId: Long): AuthResponse { val memberIds = repository.getActiveMemberIdsByDi(di = certificate.di) if (memberIds.size >= 3) { + val message = messageSource.getMessage("member.auth.max_accounts", langContext.lang) ?: "" throw SodaException( - "이미 본인인증한 계정 ${memberIds.size}개 이용중입니다.\n" + - "소다라이브의 본인인증은 최대 3개의 계정만 이용할 수 있습니다." + message = String.format(message, memberIds.size) ) } - val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") + val member = memberRepository.findByIdOrNull(memberId) + ?: throw SodaException(messageKey = "common.error.bad_credentials") val nowYear = LocalDate.now().year val certificateYear = certificate.birth.substring(0, 4).toInt() if (nowYear - certificateYear >= 19) { @@ -99,7 +106,8 @@ class AuthService( repository.save(auth) return AuthResponse(gender = certificate.gender) } else { - throw SodaException("${nowYear - 19}년 1월 1일 이전 출생자만 본인인증이 가능합니다.") + val message = messageSource.getMessage("member.auth.age_limit", langContext.lang) ?: "" + throw SodaException(message = String.format(message, nowYear - 19)) } } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt index 341569cd..77002bca 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt @@ -67,7 +67,7 @@ class NicknameGenerateService(private val repository: MemberRepository) { } } } - throw SodaException("회원가입을 하지 못했습니다.\n다시 시도해 주세요.") + throw SodaException(messageKey = "member.signup.failed_retry") } fun generateUniqueNickname(): String { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleAuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleAuthService.kt index 46548bc6..99530a6a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleAuthService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleAuthService.kt @@ -26,7 +26,7 @@ class GoogleAuthService( pushToken: String? ): SocialLoginResponse { val googleUserInfo = googleService.getUserInfo(idToken) - ?: throw SodaException("구글 로그인을 하지 못했습니다. 다시 시도해 주세요") + ?: throw SodaException(messageKey = "member.social.google_login_failed") val memberResolveResult = memberService.findOrRegister(googleUserInfo, container, marketingPid, pushToken) val member = memberResolveResult.member val principal = MemberAdapter(member) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleService.kt index fa313951..e6ac4088 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/google/GoogleService.kt @@ -27,7 +27,7 @@ class GoogleService( if (token != null) { val payload = token.payload - val email = payload.email ?: throw SodaException("이메일 제공에 동의하셔야 서비스 이용이 가능합니다.") + val email = payload.email ?: throw SodaException(messageKey = "member.social.email_consent_required") GoogleUserInfo( sub = payload.subject, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoAuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoAuthService.kt index f8e66ba1..ed4cf0c5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoAuthService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoAuthService.kt @@ -26,7 +26,7 @@ class KakaoAuthService( pushToken: String? ): SocialLoginResponse { val kakaoUserInfo = kakaoService.getUserInfo(accessToken) - ?: throw SodaException("카카오 로그인을 하지 못했습니다. 다시 시도해 주세요") + ?: throw SodaException(messageKey = "member.social.kakao_login_failed") val memberResolveResult = memberService.findOrRegister(kakaoUserInfo, container, marketingPid, pushToken) val member = memberResolveResult.member val principal = MemberAdapter(member) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoService.kt index ffd12049..8ad65c56 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/social/kakao/KakaoService.kt @@ -37,7 +37,7 @@ class KakaoService( val id = jsonNode.get("id").asLong() val kakaoAccount = jsonNode.get("kakao_account") val email = kakaoAccount?.get("email")?.asText() - ?: throw SodaException("카카오 로그인을 하지 못했습니다. 다시 시도해 주세요") + ?: throw SodaException(messageKey = "member.social.kakao_login_failed") val properties = jsonNode.get("properties") val nickname = properties?.get("nickname")?.asText() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationService.kt index 617b0719..19280080 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationService.kt @@ -16,14 +16,14 @@ class StipulationService(private val repository: StipulationRepository) { fun getTermsOfService(): StipulationDto { val stipulation = repository.findByIdOrNull(TERMS_OF_SERVICE_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") return StipulationDto(stipulation) } fun getPrivacyPolicy(): StipulationDto { val stipulation = repository.findByIdOrNull(PRIVACY_POLICY_ID) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") return StipulationDto(stipulation) } @@ -31,7 +31,7 @@ class StipulationService(private val repository: StipulationRepository) { @Transactional fun modify(request: StipulationModifyRequest) { val stipulation = repository.findByIdOrNull(request.id) - ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + ?: throw SodaException(messageKey = "member.validation.invalid_request_retry") stipulation.description = request.description } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagController.kt index e46a27ca..20380c7c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagController.kt @@ -15,7 +15,7 @@ class MemberTagController(private val service: MemberTagService) { fun getTags( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") ApiResponse.ok(service.getTags(member)) }