diff --git a/app/lib/features/auth/auth.dart b/app/lib/features/auth/auth.dart index 6c4196f..244a415 100644 --- a/app/lib/features/auth/auth.dart +++ b/app/lib/features/auth/auth.dart @@ -6,6 +6,8 @@ /// 화면은 이걸 안내 다이얼로그로 보여 준다. library; +import '../../core/i18n/app_locale.dart'; + /// 로그인 수단 — **별첨 B v1.4 확정값**. /// /// > 1차 로그인은 Google / Apple / Kakao / LINE 4종입니다. 이메일/비밀번호 가입 및 @@ -16,20 +18,18 @@ library; /// 때문이다 — 사용자에게 노출되는 목록([AuthRepository.providers])에는 들어가지 /// 않는다. X(트위터)는 확정 목록에 없어 값 자체를 두지 않는다. enum AuthProviderKind { - email('이메일로 시작하기', '이메일'), - kakao('카카오로 시작하기', '카카오'), - line('LINE으로 시작하기', 'LINE'), - apple('Apple로 시작하기', 'Apple'), - google('Google로 시작하기', 'Google'); - - const AuthProviderKind(this.label, this.shortName); + email, + kakao, + line, + apple, + google; /// 버튼 라벨. 임의로 바꾸지 마라 — 화면 대조 대상이다. - final String label; + String get label => appL10n.authProviderStart(name); /// 제공자 이름만 — 설정 「SNS 연동」·안내 문구용. [label] 에서 「로 시작하기」를 /// 떼어 만들면 「LINE으로」가 「LINE으」로 잘린다(인계 결함). - final String shortName; + String get shortName => appL10n.authProviderName(name); /// 이메일은 소셜이 아니라 입력 폼이 붙는다. QA 내부 경로만 이걸 쓴다. bool get needsForm => this == AuthProviderKind.email; @@ -194,7 +194,7 @@ final class SampleAccountLinker implements AccountLinker { @override Future unlink(AuthProviderKind provider) async { if (_linked.length <= 1) { - return const AuthFailed('E-AUTH-13', '마지막 로그인 수단은 연결을 해제할 수 없습니다.'); + return AuthFailed('E-AUTH-13', appL10n.authLastProvider); } _linked.remove(provider); return AuthSucceeded( diff --git a/app/lib/features/auth/auth_repository.dart b/app/lib/features/auth/auth_repository.dart index fe4cbdf..5c0559d 100644 --- a/app/lib/features/auth/auth_repository.dart +++ b/app/lib/features/auth/auth_repository.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import '../../core/firebase/firebase_boot.dart'; import 'auth.dart'; import 'social_providers.dart'; +import '../../core/i18n/app_locale.dart'; /// 로그인 상태 보관소. 라우터가 이걸 듣고 로그인/셸을 가른다. class AuthRepository extends ChangeNotifier { @@ -35,6 +36,17 @@ class AuthRepository extends ChangeNotifier { AuthAccount? get account => _account; bool get isSignedIn => _account != null; + /// 간편 로그인 「회원가입」 모드로 막 인증을 마쳤다. 라우터가 이 값을 보고 가입 완료 + /// 화면(`182:29344`)을 한 번 끼운다. [finishSignup] 이 내린다. + bool get justSignedUp => _justSignedUp; + bool _justSignedUp = false; + + void finishSignup() { + if (!_justSignedUp) return; + _justSignedUp = false; + notifyListeners(); + } + /// **별첨 B v1.4 확정 4종.** 순서는 회신 문구 순서다(화면 버튼은 Figma 순서로 다시 줄 세운다). /// /// > 1차 로그인은 Google / Apple / Kakao / LINE 4종입니다. 이메일/비밀번호 가입 및 @@ -76,7 +88,7 @@ class AuthRepository extends ChangeNotifier { }) async { final impl = qaEmailProvider; if (impl == null) { - return const AuthFailed('E-AUTH-03', '이메일 로그인을 쓸 수 없습니다.'); + return AuthFailed('E-AUTH-03', appL10n.authEmailUnavailable); } late final AuthOutcome outcome; try { @@ -85,13 +97,16 @@ class AuthRepository extends ChangeNotifier { : await impl.signInWithEmail(email, password); } on Object catch (error) { debugPrint('[AuthRepository] email sign-in failed: ${error.runtimeType}'); - return const AuthFailed('E-AUTH-01', '이메일 또는 비밀번호를 확인해 주세요.'); + return AuthFailed('E-AUTH-01', appL10n.authCheckCredentials); } if (outcome is AuthSucceeded) _replaceAccount(outcome.account); return outcome; } - Future signIn(SocialProvider provider) async { + Future signIn( + SocialProvider provider, { + bool signUp = false, + }) async { final impl = providers.firstWhere( (p) => p.provider == provider, orElse: () => throw ArgumentError.value( @@ -109,9 +124,10 @@ class AuthRepository extends ChangeNotifier { debugPrint( '[AuthRepository] ${provider.name} sign-in failed: ${error.runtimeType}', ); - outcome = const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.'); + outcome = AuthFailed('E-AUTH-03', appL10n.authSocialFailed); } if (outcome is AuthSucceeded) { + _justSignedUp = signUp; _replaceAccount(outcome.account); } return outcome; @@ -165,6 +181,7 @@ class AuthRepository extends ChangeNotifier { } void _replaceAccount(AuthAccount? next) { + if (next == null) _justSignedUp = false; if (_sameAccount(_account, next)) return; _account = next; notifyListeners(); diff --git a/app/lib/features/auth/firebase_account_linker.dart b/app/lib/features/auth/firebase_account_linker.dart index d3b4f11..98c9ea2 100644 --- a/app/lib/features/auth/firebase_account_linker.dart +++ b/app/lib/features/auth/firebase_account_linker.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'auth.dart'; import 'social_providers.dart'; +import '../../core/i18n/app_locale.dart'; /// 하나의 FANUZU UID 에 소셜 Provider 를 붙이고 떼는 Firebase 어댑터. /// @@ -47,7 +48,7 @@ final class FirebaseAccountLinker implements AccountLinker { Future link(AuthProviderKind provider) async { final user = _firebaseAuth.currentUser; if (user == null) { - return const AuthFailed('E-AUTH-03', '로그인이 필요합니다.'); + return AuthFailed('E-AUTH-03', appL10n.authLoginRequired); } final impl = _implOf(provider); if (impl == null) { @@ -77,7 +78,7 @@ final class FirebaseAccountLinker implements AccountLinker { if (mine != null && theirs != null && !sameAccountIdentity(mine, theirs)) { - return const AuthFailed('E-AUTH-08', '가입하신 이메일과 다른 계정입니다.'); + return AuthFailed('E-AUTH-08', appL10n.authDifferentEmail); } final result = await user.linkWithCredential(obtained.credential); @@ -90,7 +91,7 @@ final class FirebaseAccountLinker implements AccountLinker { } on Object catch (error) { // SDK 가 문서화하지 않은 플랫폼 예외를 던져도 화면이 busy 에 고착되지 않게 닫는다. debugPrint('[FirebaseAccountLinker] link failed: ${error.runtimeType}'); - return const AuthFailed('E-AUTH-03', '계정 연결에 실패했습니다.'); + return AuthFailed('E-AUTH-03', appL10n.authLinkFailed); } } @@ -98,16 +99,16 @@ final class FirebaseAccountLinker implements AccountLinker { Future unlink(AuthProviderKind provider) async { final user = _firebaseAuth.currentUser; if (user == null) { - return const AuthFailed('E-AUTH-03', '로그인이 필요합니다.'); + return AuthFailed('E-AUTH-03', appL10n.authLoginRequired); } // **마지막 하나는 떼지 않는다.** 떼는 순간 로그인 수단이 0개가 되어 계정에 // 다시 들어올 방법이 사라진다 — Firebase 는 이걸 막아 주지 않는다. if (linkedAccounts.length <= 1) { - return const AuthFailed('E-AUTH-13', '마지막 로그인 수단은 연결을 해제할 수 없습니다.'); + return AuthFailed('E-AUTH-13', appL10n.authLastProvider); } final id = providerIds[provider]; if (id == null) { - return const AuthFailed('E-AUTH-10', '연결되어 있지 않은 계정입니다.'); + return AuthFailed('E-AUTH-10', appL10n.authNotLinked); } try { await user.unlink(id); diff --git a/app/lib/features/auth/login_screen.dart b/app/lib/features/auth/login_screen.dart index f714f75..275653c 100644 --- a/app/lib/features/auth/login_screen.dart +++ b/app/lib/features/auth/login_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/firebase/firebase_boot.dart'; +import '../../core/i18n/app_locale.dart'; import '../../core/theme/tokens.dart'; import '../../core/ui/app_icons.dart'; import 'auth.dart'; @@ -62,14 +63,14 @@ class _LoginScreenState extends State { if (!mounted) return; setState(() { _busy = false; - _notice = authNotice(outcome); + _notice = authNotice(outcome, context.l10n); }); } Future _resetPassword() async { final email = _email.text.trim(); if (email.isEmpty) { - setState(() => _notice = '재설정 메일을 받으실 이메일을 먼저 입력해 주세요.'); + setState(() => _notice = context.l10n.authResetNeedEmail); return; } final impl = widget.auth.qaEmailProvider; @@ -81,8 +82,8 @@ class _LoginScreenState extends State { _busy = false; // 성공해도 계정 존재 여부를 알려 주지 않는다. 가입된 이메일을 훑을 수 있다. _notice = outcome is AuthFailed - ? authNotice(outcome) - : '입력하신 주소로 재설정 안내를 보냈습니다.'; + ? authNotice(outcome, context.l10n) + : context.l10n.authResetSent; }); } @@ -171,12 +172,12 @@ class _LoginScreenState extends State { backgroundColor: AppColors.cosmicAccent, foregroundColor: AppColors.onCosmicAccent, ), - child: const Text('로그인'), + child: Text(context.l10n.authLogin), ), const SizedBox(height: AppSpace.s4), AuthSwitchRow( - question: '팬우주는 처음이세요?', - action: '회원가입', + question: context.l10n.authFirstTime, + action: context.l10n.authSignup, onTap: () => _open(signUp: true), ), ], @@ -216,7 +217,7 @@ class _SimpleLoginScreenState extends State { if (agreed != true || !mounted) return; } setState(() => _busy = true); - final outcome = await widget.auth.signIn(provider); + final outcome = await widget.auth.signIn(provider, signUp: widget.signUp); if (!mounted) return; setState(() => _busy = false); // 키 대기 제공자는 안내 다이얼로그로 끊는다 — 눌렀는데 아무 일도 안 일어나면 @@ -226,7 +227,7 @@ class _SimpleLoginScreenState extends State { return; } if (!mounted) return; - setState(() => _notice = authNotice(outcome)); + setState(() => _notice = authNotice(outcome, context.l10n)); } Future _showNotConfigured(AuthNotConfigured outcome) async { @@ -234,15 +235,17 @@ class _SimpleLoginScreenState extends State { await showDialog( context: context, builder: (context) => AlertDialog( - title: Text('$name 로그인 준비 중'), + title: Text(context.l10n.authNotConfiguredTitle(name)), content: Text( - '$name 로그인은 발주사 키가 들어오면 열립니다.\n\n' - '필요한 항목\n· ${outcome.missing.join('\n· ')}', + context.l10n.authNotConfiguredBody( + name, + outcome.missing.join('\n· '), + ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('확인'), + child: Text(context.l10n.commonConfirm), ), ], ), @@ -294,8 +297,8 @@ class _SimpleLoginScreenState extends State { const SizedBox(height: AppSpace.s5), Text( signUp - ? '가장 편한 방법으로\n팬우주에 회원가입 하세요!' - : '가장 편한 방법으로\n팬우주에 로그인하세요!', + ? context.l10n.authSignupHeadline + : context.l10n.authLoginHeadline, textAlign: TextAlign.center, style: const TextStyle( fontFamily: AppFonts.body, @@ -317,7 +320,11 @@ class _SimpleLoginScreenState extends State { for (final p in providers) ...[ _ProviderButton( provider: p.provider, - label: providerButtonLabel(p.provider, signUp: signUp), + label: providerButtonLabel( + p.provider, + signUp: signUp, + l10n: context.l10n, + ), enabled: !_busy, onPressed: () => _signIn(p.provider), ), @@ -329,13 +336,13 @@ class _SimpleLoginScreenState extends State { ], signUp ? AuthSwitchRow( - question: '이미 가입하셨나요?', - action: '로그인', + question: context.l10n.authAlreadyMember, + action: context.l10n.authLogin, onTap: _switchMode, ) : AuthSwitchRow( - question: '팬우주는 처음이세요?', - action: '회원가입', + question: context.l10n.authFirstTime, + action: context.l10n.authSignup, onTap: _switchMode, ), ], @@ -357,26 +364,29 @@ const _figmaOrder = [ /// 간편 로그인 · 회원가입 버튼 문구 — 시안 「Kakao로 로그인」 · 「Kakao로 가입하기」. /// [AuthProviderKind.label](「카카오로 시작하기」)은 설정 · 안내 문구가 쓰므로 따로 둔다. -String providerButtonLabel(AuthProviderKind kind, {bool signUp = false}) => - '${_names[kind]} ${signUp ? '가입하기' : '로그인'}'; +/// 제공자별 조사(ko 「LINE으로」)는 arb 의 select 가 맡는다. [l10n] 이 없으면 ko. +String providerButtonLabel( + AuthProviderKind kind, { + bool signUp = false, + AppLocalizations? l10n, +}) { + final t = l10n ?? koL10n; + return signUp + ? t.authProviderSignup(kind.name) + : t.authProviderLogin(kind.name); +} -/// 시안 버튼 문구의 제공자 표기. LINE 은 조사가 「으로」다. -const _names = { - AuthProviderKind.apple: 'Apple로', - AuthProviderKind.google: 'Google로', - AuthProviderKind.kakao: 'Kakao로', - AuthProviderKind.line: 'LINE으로', - AuthProviderKind.email: '이메일로', -}; - -String? authNotice(AuthOutcome outcome) => switch (outcome) { - AuthSucceeded() => null, - AuthCancelled() => null, - AuthFailed(:final code, :final message) => '[$code] $message', - AuthNotConfigured(:final provider, :final missing) => - '${provider.shortName} 로그인은 아직 준비 중입니다.\n' - '필요한 항목: ${missing.join(' · ')}', -}; +String? authNotice(AuthOutcome outcome, [AppLocalizations? l10n]) => + switch (outcome) { + AuthSucceeded() => null, + AuthCancelled() => null, + AuthFailed(:final code, :final message) => '[$code] $message', + AuthNotConfigured(:final provider, :final missing) => + (l10n ?? koL10n).authNotConfiguredNotice( + provider.shortName, + missing.join(' · '), + ), + }; TextStyle _abz(double size, Color color) => TextStyle( fontFamily: AppFonts.display, diff --git a/app/lib/features/auth/signup_screens.dart b/app/lib/features/auth/signup_screens.dart index 07120fe..8ae2533 100644 --- a/app/lib/features/auth/signup_screens.dart +++ b/app/lib/features/auth/signup_screens.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../core/i18n/app_locale.dart'; import '../../core/theme/tokens.dart'; /// 회원가입 약관 — Figma `179:24230`(미동의) · `179:24324`(전체 동의). @@ -16,16 +17,22 @@ class TermsScreen extends StatefulWidget { } enum _Term { - service('서비스 이용약관 (필수)', required: true, body: true), - location('위치기반 서비스 이용약관 (필수)', required: true, body: true), - age('만 14세 이상입니다. (필수)', required: true, body: false), - marketing('마케팅 정보 및 이벤트 알림 수신 동의 (선택)', required: false, body: true); + service(required: true, body: true), + location(required: true, body: true), + age(required: true, body: false), + marketing(required: false, body: true); - const _Term(this.label, {required this.required, required this.body}); + const _Term({required this.required, required this.body}); - final String label; final bool required; final bool body; + + String label(AppLocalizations l10n) => switch (this) { + service => l10n.termsService, + location => l10n.termsLocation, + age => l10n.termsAge, + marketing => l10n.termsMarketing, + }; } const _body = @@ -47,13 +54,13 @@ class _TermsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('이용 약관')), + appBar: AppBar(title: Text(context.l10n.termsTitle)), body: ListView( padding: const EdgeInsets.all(AppSpace.s4), children: [ _Box( child: _CheckRow( - label: '전체 동의', + label: context.l10n.termsAgreeAll, checked: _all, size: 16, onTap: () => setState( @@ -74,7 +81,7 @@ class _TermsScreenState extends State { border: Border(bottom: BorderSide(color: _stroke)), ), child: _CheckRow( - label: t.label, + label: t.label(context.l10n), checked: _agreed.contains(t), onTap: () => _toggle(t), ), @@ -102,7 +109,7 @@ class _TermsScreenState extends State { ], ) : _CheckRow( - label: t.label, + label: t.label(context.l10n), checked: _agreed.contains(t), onTap: () => _toggle(t), ), @@ -118,7 +125,7 @@ class _TermsScreenState extends State { backgroundColor: Colors.black, foregroundColor: AppColors.onCosmic, ), - child: const Text('이전'), + child: Text(context.l10n.commonPrev), ), ), const SizedBox(width: 10), @@ -133,7 +140,7 @@ class _TermsScreenState extends State { disabledBackgroundColor: const Color(0xFF333333), disabledForegroundColor: const Color(0xFF777777), ), - child: const Text('완료'), + child: Text(context.l10n.commonDone), ), ), ], @@ -206,12 +213,12 @@ class _CheckRow extends StatelessWidget { ); } -/// 회원가입 완료 — Figma `182:29344`. 「시작」은 이 화면을 닫는다. -/// -/// 아직 가입 동선에 걸려 있지 않다. 제공자 인증이 성공하면 라우터 redirect 가 곧장 셸로 -/// 보내므로, 이 화면을 끼우려면 redirect 가 「가입 직후」를 알아야 한다(app_router 공유 변경). +/// 회원가입 완료 — Figma `182:29344`. 가입 직후 라우터 redirect 가 한 번 띄우고 +/// ([AuthRepository.justSignedUp]), 「시작」은 [onStart] 로 셸에 들어간다. class SignupCompleteScreen extends StatelessWidget { - const SignupCompleteScreen({super.key}); + const SignupCompleteScreen({super.key, this.onStart}); + + final VoidCallback? onStart; @override Widget build(BuildContext context) { @@ -238,8 +245,8 @@ class SignupCompleteScreen extends StatelessWidget { ), ), const SizedBox(height: AppSpace.s6), - const Text( - '회원가입이 완료되었습니다', + Text( + context.l10n.signupDoneTitle, style: TextStyle( fontFamily: AppFonts.body, fontSize: 24, @@ -250,7 +257,7 @@ class SignupCompleteScreen extends StatelessWidget { ), const SizedBox(height: AppSpace.s3), Text( - '팬덤 활동을 시작할 수 있습니다.\n다양한 활동을 통해 내 팬덤 행성을 키워보세요.', + context.l10n.signupDoneBody, textAlign: TextAlign.center, style: TextStyle( fontFamily: AppFonts.display, @@ -267,12 +274,12 @@ class SignupCompleteScreen extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4), child: FilledButton( - onPressed: () => Navigator.of(context).maybePop(), + onPressed: onStart ?? () => Navigator.of(context).maybePop(), style: FilledButton.styleFrom( backgroundColor: AppColors.cosmicAccent, foregroundColor: AppColors.onCosmicAccent, ), - child: const Text('시작'), + child: Text(context.l10n.commonStart), ), ), ], diff --git a/app/lib/features/auth/social_providers.dart b/app/lib/features/auth/social_providers.dart index 4bb0041..638d0e6 100644 --- a/app/lib/features/auth/social_providers.dart +++ b/app/lib/features/auth/social_providers.dart @@ -8,6 +8,7 @@ import 'package:sign_in_with_apple/sign_in_with_apple.dart' as apple; import '../../core/firebase/firebase_boot.dart'; import 'auth.dart'; +import '../../core/i18n/app_locale.dart'; /// Firebase 자격증명을 만들 수 있는 제공자. /// @@ -71,7 +72,7 @@ final class GoogleSignInProvider try { final obtained = await obtainCredential(); if (obtained == null) { - return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.'); + return AuthFailed('E-AUTH-03', appL10n.authSocialFailed); } final result = await fb.FirebaseAuth.instance.signInWithCredential( obtained.credential, @@ -162,12 +163,12 @@ final class AppleSignInProvider if (missing.isNotEmpty) return AuthNotConfigured(provider, missing); if (!await apple.SignInWithApple.isAvailable()) { - return const AuthFailed('E-AUTH-03', '이 기기에서는 Apple 로그인을 쓸 수 없습니다.'); + return AuthFailed('E-AUTH-03', appL10n.authAppleUnavailable); } try { final obtained = await obtainCredential(); if (obtained == null) { - return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.'); + return AuthFailed('E-AUTH-03', appL10n.authSocialFailed); } final result = await fb.FirebaseAuth.instance.signInWithCredential( obtained.credential, @@ -180,7 +181,7 @@ final class AppleSignInProvider return AuthFailed('E-AUTH-03', error.message); } on apple.SignInWithAppleException catch (error) { debugPrint('[AppleSignInProvider] failed: ${error.runtimeType}'); - return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.'); + return AuthFailed('E-AUTH-03', appL10n.authSocialFailed); } on fb.FirebaseAuthException catch (error) { return AuthFailed( authCatalogCode(error), @@ -389,21 +390,21 @@ enum AuthFlow { /// 사용자가 자기가 하지도 않은 계정 연결을 설명받고, 무엇을 해야 하는지 모른다. String authMessage(fb.FirebaseAuthException error, {required AuthFlow flow}) => switch (error.code) { - 'email-already-in-use' => '이미 가입된 이메일입니다.', - 'weak-password' => '비밀번호가 너무 짧습니다. 8자 이상으로 정해 주세요.', - 'invalid-email' => '이메일 형식이 올바르지 않습니다.', - 'too-many-requests' => '잠시 후 다시 시도해 주세요.', + 'email-already-in-use' => appL10n.authEmailInUse, + 'weak-password' => appL10n.authWeakPassword, + 'invalid-email' => appL10n.authInvalidEmail, + 'too-many-requests' => appL10n.commonTryLater, 'account-exists-with-different-credential' => switch (flow) { AuthFlow.signIn => _otherProviderSignInGuide(error.email), - AuthFlow.link => '다른 FANUZU 계정에 이미 연결된 소셜 계정입니다.', + AuthFlow.link => appL10n.authCredentialInUse, }, // 붙이려는 자격증명이 남의 UID 에 이미 달려 있다. 연결 경로에서만 난다. - 'credential-already-in-use' => '다른 FANUZU 계정에 이미 연결된 소셜 계정입니다.', - 'provider-already-linked' => '이미 연결된 계정입니다.', - 'no-such-provider' => '연결되어 있지 않은 계정입니다.', - 'operation-not-allowed' => '이 로그인 수단은 아직 사용할 수 없습니다.', - 'requires-recent-login' => '보안을 위해 다시 로그인한 뒤 시도해 주세요.', - _ => '이메일 또는 비밀번호를 확인해 주세요.', + 'credential-already-in-use' => appL10n.authCredentialInUse, + 'provider-already-linked' => appL10n.authAlreadyLinked, + 'no-such-provider' => appL10n.authNotLinked, + 'operation-not-allowed' => appL10n.authMethodUnavailable, + 'requires-recent-login' => appL10n.authRecentLogin, + _ => appL10n.authCheckCredentials, }; /// 「먼저 쓰던 수단으로 로그인한 뒤 SNS 연동에서 붙이라」는 안내. @@ -422,6 +423,4 @@ String authMessage(fb.FirebaseAuthException error, {required AuthFlow flow}) => /// 따라 「은/는」이 갈려 `example.co은` 같은 문구가 나간다. 「계정」을 사이에 두면 /// 조사가 항상 맞는다. String _otherProviderSignInGuide(String? email) => - '${email ?? '이'} 계정은 다른 로그인 수단으로 가입돼 있습니다. ' - '처음 가입에 쓰신 수단으로 로그인한 뒤 MY > 설정 > SNS 연동에서 ' - '이 수단을 연결해 주세요.'; + appL10n.authOtherProviderGuide(email ?? appL10n.authThisAccount);