로그인 4종(Google·Apple·Kakao·LINE) 전환 · 이메일 가입 비노출 · 계정 연결 · STATION 탭 비노출 플래그
This commit is contained in:
@@ -1,19 +1,20 @@
|
||||
/// 인증 계약 — 제공자 인터페이스와 결과 타입.
|
||||
///
|
||||
/// 원칙: **"키 미투입"은 예외가 아니라 상태다.** 카카오·네이버는 발주사 사업자 정보로
|
||||
/// 개발자 콘솔에 앱을 등록해야 키가 나오고(Q3), Google·Apple 은 Firebase 설정 파일이
|
||||
/// 있어야 한다(S1·S2). 둘 다 "아직 준비가 안 됐다"이지 "지원하지 않는다"가 아니므로
|
||||
/// 예외를 던지지 않고 [AuthNotConfigured] 로 돌려준다. 화면은 이걸 안내로 보여 준다.
|
||||
/// 원칙: **"키 미투입"은 예외가 아니라 상태다.** Apple·카카오·LINE 은 발주사 사업자
|
||||
/// 정보로 각 개발자 콘솔에 앱을 등록해야 키가 나온다. "아직 준비가 안 됐다"이지
|
||||
/// "지원하지 않는다"가 아니므로 예외를 던지지 않고 [AuthNotConfigured] 로 돌려준다.
|
||||
/// 화면은 이걸 안내 다이얼로그로 보여 준다.
|
||||
library;
|
||||
|
||||
/// 가입 수단 — **정책 설계서 §P-01 확정값 5종**(답변서 v1.1 Q7).
|
||||
/// 로그인 수단 — **별첨 B v1.4 확정값**.
|
||||
///
|
||||
/// 초안 당시 CD-18 이 미확정이라 소셜 4종(카카오·네이버·Apple·Google)만 두었는데,
|
||||
/// 답변서가 **이메일 · Google · Apple · Kakao · LINE** 으로 확정했다.
|
||||
/// **네이버는 확정 목록에 없고, 이메일과 LINE 이 새로 들어왔다.**
|
||||
/// > 1차 로그인은 Google / Apple / Kakao / LINE 4종입니다. 이메일/비밀번호 가입 및
|
||||
/// > X 로그인은 제외합니다. 하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다.
|
||||
///
|
||||
/// 앱 Figma 에는 WeChat · X 버튼도 그려져 있으나 답변서가 5종으로 정했으므로
|
||||
/// 1차는 5종이다. 추가 시 재작업이 없도록 제공자를 인터페이스 뒤에 둔다.
|
||||
/// 답변서 v1.1 Q7 은 이메일을 포함한 5종이었으나 별첨 B v1.4 가 이를 뒤집었다.
|
||||
/// [email] 값이 남아 있는 것은 **QA 계정(qa-test@fanuzu.co.kr) 내부 경로** 하나
|
||||
/// 때문이다 — 사용자에게 노출되는 목록([AuthRepository.providers])에는 들어가지
|
||||
/// 않는다. X(트위터)는 확정 목록에 없어 값 자체를 두지 않는다.
|
||||
enum AuthProviderKind {
|
||||
email('이메일로 시작하기'),
|
||||
kakao('카카오로 시작하기'),
|
||||
@@ -26,7 +27,7 @@ enum AuthProviderKind {
|
||||
/// 버튼 라벨. 임의로 바꾸지 마라 — 화면 대조 대상이다.
|
||||
final String label;
|
||||
|
||||
/// 이메일은 소셜이 아니라 입력 폼이 붙는다. 화면이 이걸로 갈린다.
|
||||
/// 이메일은 소셜이 아니라 입력 폼이 붙는다. QA 내부 경로만 이걸 쓴다.
|
||||
bool get needsForm => this == AuthProviderKind.email;
|
||||
}
|
||||
|
||||
@@ -101,11 +102,12 @@ abstract interface class SocialAuthProvider {
|
||||
Future<void> signOut();
|
||||
}
|
||||
|
||||
/// 이메일 제공자만 쓰는 확장 계약.
|
||||
/// 이메일 제공자 — **QA 계정 전용 내부 경로**.
|
||||
///
|
||||
/// 소셜은 외부 창이 열리고 끝이지만 이메일은 **앱 안에서 값을 받아야 한다.**
|
||||
/// [SocialAuthProvider.signIn] 하나로 묶으면 화면이 제공자 종류를 보고 분기하게
|
||||
/// 되므로, 입력이 필요한 쪽만 별도 계약으로 뺀다.
|
||||
/// 별첨 B v1.4 가 이메일/비밀번호 가입을 1차 범위에서 뺐다. 코드를 지우지 않은 것은
|
||||
/// 발주사 QA 계정(qa-test@fanuzu.co.kr)으로 로그인 이후 화면을 확인해야 하기
|
||||
/// 때문이며, [AuthRepository.qaEmailProvider] 가 **디버그 빌드에서만** 이 구현체를
|
||||
/// 돌려준다. 릴리스 빌드에는 부를 수 있는 경로가 없다.
|
||||
abstract interface class EmailAuthProvider implements SocialAuthProvider {
|
||||
Future<AuthOutcome> signInWithEmail(String email, String password);
|
||||
|
||||
@@ -126,3 +128,72 @@ abstract interface class AuthSession {
|
||||
|
||||
Future<void> signOut();
|
||||
}
|
||||
|
||||
/// 「동일 이메일만 연동 가능」 판정 — 명세 `554:22797` · 안내 문구 `554:23251`.
|
||||
///
|
||||
/// **로컬 파트만 비교한다.** 명세의 예시가 `test123@naver.com` 과
|
||||
/// `test123@gmail.com` 을 같다고 보므로 도메인은 보지 않는다. 전체 문자열을
|
||||
/// 비교하면 명세가 (O) 로 적은 사례가 막힌다.
|
||||
bool sameAccountIdentity(String a, String b) => _localPart(a) == _localPart(b);
|
||||
|
||||
String _localPart(String email) => email.trim().toLowerCase().split('@').first;
|
||||
|
||||
/// 연동 화면에 거는 제공자 — 별첨 B v1.4 확정 4종.
|
||||
/// 이메일은 QA 내부 경로라 목록에 없다.
|
||||
const List<AuthProviderKind> linkableProviders = [
|
||||
AuthProviderKind.google,
|
||||
AuthProviderKind.apple,
|
||||
AuthProviderKind.kakao,
|
||||
AuthProviderKind.line,
|
||||
];
|
||||
|
||||
/// 하나의 FANUZU UID 에 소셜 Provider 를 더 붙이거나 떼는 경계.
|
||||
///
|
||||
/// 별첨 B v1.4 — *하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다.*
|
||||
/// Firebase 는 `User.linkWithCredential` / `User.unlink` 로 이걸 지원한다.
|
||||
/// 저장소와 화면은 이 인터페이스만 알고 SDK 는 어댑터 뒤에 둔다 — 덕분에 설정
|
||||
/// 파일 없이도 연결·해제 규칙을 단위 테스트할 수 있다.
|
||||
abstract interface class AccountLinker {
|
||||
/// 지금 UID 에 붙어 있는 Provider → 그 Provider 가 준 이메일(모르면 null).
|
||||
///
|
||||
/// 설정 화면의 「SNS 연동」 목록이 이걸 그대로 그린다.
|
||||
Map<AuthProviderKind, String?> get linkedAccounts;
|
||||
|
||||
Future<AuthOutcome> link(AuthProviderKind provider);
|
||||
|
||||
/// 마지막 하나 남은 Provider 는 뗄 수 없다 — 떼면 로그인 수단이 사라진다.
|
||||
Future<AuthOutcome> unlink(AuthProviderKind provider);
|
||||
}
|
||||
|
||||
/// 설정 파일·키가 없어 실제로는 붙일 수 없는 상태의 연동 경계.
|
||||
///
|
||||
/// 목 모드에서도 「SNS 연동」 화면 자체는 열려야 한다(별첨2 확인 항목의 화면 동선).
|
||||
/// 상태를 메모리에만 들고 있고 UID 에는 아무 일도 하지 않는다.
|
||||
final class SampleAccountLinker implements AccountLinker {
|
||||
SampleAccountLinker({Map<AuthProviderKind, String?>? seed})
|
||||
: _linked = {...?seed};
|
||||
|
||||
final Map<AuthProviderKind, String?> _linked;
|
||||
|
||||
@override
|
||||
Map<AuthProviderKind, String?> get linkedAccounts => {..._linked};
|
||||
|
||||
@override
|
||||
Future<AuthOutcome> link(AuthProviderKind provider) async {
|
||||
_linked[provider] = 'fan@fanuzu.co.kr';
|
||||
return AuthSucceeded(
|
||||
AuthAccount(uid: 'mock-local', provider: provider, isMock: true),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthOutcome> unlink(AuthProviderKind provider) async {
|
||||
if (_linked.length <= 1) {
|
||||
return const AuthFailed('E-AUTH-13', '마지막 로그인 수단은 연결을 해제할 수 없습니다.');
|
||||
}
|
||||
_linked.remove(provider);
|
||||
return AuthSucceeded(
|
||||
AuthAccount(uid: 'mock-local', provider: provider, isMock: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user