diff --git a/app/lib/core/data/backend.dart b/app/lib/core/data/backend.dart index f6a9006..3c1b42a 100644 --- a/app/lib/core/data/backend.dart +++ b/app/lib/core/data/backend.dart @@ -52,6 +52,9 @@ class Backend { /// 로그인 계정 이메일. 닉네임이 없을 때 화면이 부르는 이름의 재료다. String? get email => _auth.currentUser?.email; + /// 로그인 계정의 제공자 표시 이름(Kakao 닉네임 · Google/LINE 이름 · Apple fullName). 온보딩 닉네임 기본값 재료다. + String? get providerDisplayName => _auth.currentUser?.displayName; + /// 로그인 필수 경로에서 UID 를 강제로 얻는다. String get requireUid { final value = uid; diff --git a/app/lib/features/auth/onboarding_data.dart b/app/lib/features/auth/onboarding_data.dart index de2c2a0..bf8cd47 100644 --- a/app/lib/features/auth/onboarding_data.dart +++ b/app/lib/features/auth/onboarding_data.dart @@ -1,6 +1,9 @@ +import 'package:flutter/widgets.dart' show StringCharacters; import 'package:cloud_firestore/cloud_firestore.dart'; import '../../core/data/backend.dart'; +import '../my/my_data.dart' + show nicknameMaxLength, nicknameMinLength, nicknameReserved; /// 온보딩 6단계(10/3 통합회신 1항 · 해석 확인 Q-OB-02 A · FR-CMN-03~08)가 서버와 주고받는 것. abstract interface class OnboardingRepository { @@ -41,6 +44,16 @@ class OnboardingFandom { final bool nebula; } +/// 소셜 계정 표시 이름을 닉네임 칸 기본값으로 — 해석 확인 Q-E11 임시값. +/// 닉네임 규칙(2~20자 · 사칭 금지어)에 걸리면 빈칸이다. 길이는 글자(grapheme) 기준. +String suggestedNickname(String? providerName) { + final n = (providerName ?? '').trim(); + final len = n.characters.length; + return len < nicknameMinLength || len > nicknameMaxLength || nicknameReserved(n) + ? '' + : n; +} + /// 계정이 온보딩을 거쳐야 하는가 — 서버에 가입 약관 동의(`termsAgreedAt`)가 없으면 그렇다. /// 트리거가 아직 사용자 문서를 만들지 않았어도(갓 생성된 계정) 그렇다. Future needsOnboardingFirestore(String uid) async { @@ -67,7 +80,9 @@ class FirestoreOnboardingRepository implements OnboardingRepository { final nickname = data['nickname']; final country = data['countryCode']; return ( - nickname: nickname is String ? nickname : '', + nickname: nickname is String && nickname.isNotEmpty + ? nickname + : suggestedNickname(_backend.providerDisplayName), country: country is String && country.isNotEmpty ? country : null, ); } diff --git a/app/lib/features/auth/social_providers.dart b/app/lib/features/auth/social_providers.dart index 0c78f70..9bddf9c 100644 --- a/app/lib/features/auth/social_providers.dart +++ b/app/lib/features/auth/social_providers.dart @@ -32,7 +32,14 @@ abstract interface class CredentialProvider { } /// 자격증명 한 벌과, 판정에 쓸 그 계정의 이메일. -typedef SocialCredential = ({fb.AuthCredential credential, String? email}); +/// +/// [name] 은 제공자가 자격증명과 함께만 주는 표시 이름(Apple fullName — 최초 인가 때만). Firebase 가 +/// 이 값을 계정 이름으로 옮기지 않아, 로그인 직후 우리가 계정에 써 둔다(온보딩 닉네임 기본값). +typedef SocialCredential = ({ + fb.AuthCredential credential, + String? email, + String? name, +}); /// Google 로그인 — 실 코드 경로. /// @@ -67,6 +74,7 @@ final class GoogleSignInProvider return ( credential: fb.GoogleAuthProvider.credential(idToken: idToken), email: account.email, + name: null, ); } @@ -160,6 +168,10 @@ final class AppleSignInProvider // Apple 은 **최초 인가 때만** 이메일을 준다. 두 번째부터는 null 이라 // 「동일 이메일만 연동」 판정을 할 수 없다 — 그때는 통과시킨다. email: credential.email, + name: [credential.givenName, credential.familyName] + .whereType() + .where((part) => part.isNotEmpty) + .join(' '), ); } @@ -179,6 +191,10 @@ final class AppleSignInProvider final result = await fb.FirebaseAuth.instance.signInWithCredential( obtained.credential, ); + final name = obtained.name; + if (name != null && name.isNotEmpty && (result.user?.displayName ?? '').isEmpty) { + await result.user?.updateDisplayName(name); + } return AuthSucceeded(_toAccount(result.user, provider)); } on apple.SignInWithAppleAuthorizationException catch (error) { if (error.code == apple.AuthorizationErrorCode.canceled) { diff --git a/app/test/suggested_nickname_test.dart b/app/test/suggested_nickname_test.dart new file mode 100644 index 0000000..95fd904 --- /dev/null +++ b/app/test/suggested_nickname_test.dart @@ -0,0 +1,14 @@ +import 'package:fancake/features/auth/onboarding_data.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('소셜 표시 이름은 닉네임 규칙을 통과할 때만 기본값이 된다', () { + expect(suggestedNickname(' 홍길동 '), '홍길동'); + expect(suggestedNickname('a' * 20), 'a' * 20); + expect(suggestedNickname('a' * 21), ''); + expect(suggestedNickname('나'), ''); + expect(suggestedNickname('팬'), ''); + expect(suggestedNickname('FANUZU 운영팀'), ''); + expect(suggestedNickname(null), ''); + }); +}