/// 인증 계약 — 제공자 인터페이스와 결과 타입. /// /// 원칙: **"키 미투입"은 예외가 아니라 상태다.** 카카오·네이버는 발주사 사업자 정보로 /// 개발자 콘솔에 앱을 등록해야 키가 나오고(Q3), Google·Apple 은 Firebase 설정 파일이 /// 있어야 한다(S1·S2). 둘 다 "아직 준비가 안 됐다"이지 "지원하지 않는다"가 아니므로 /// 예외를 던지지 않고 [AuthNotConfigured] 로 돌려준다. 화면은 이걸 안내로 보여 준다. library; /// 가입 수단 — **정책 설계서 §P-01 확정값 5종**(답변서 v1.1 Q7). /// /// 초안 당시 CD-18 이 미확정이라 소셜 4종(카카오·네이버·Apple·Google)만 두었는데, /// 답변서가 **이메일 · Google · Apple · Kakao · LINE** 으로 확정했다. /// **네이버는 확정 목록에 없고, 이메일과 LINE 이 새로 들어왔다.** /// /// 앱 Figma 에는 WeChat · X 버튼도 그려져 있으나 답변서가 5종으로 정했으므로 /// 1차는 5종이다. 추가 시 재작업이 없도록 제공자를 인터페이스 뒤에 둔다. enum AuthProviderKind { email('이메일로 시작하기'), kakao('카카오로 시작하기'), line('LINE으로 시작하기'), apple('Apple로 시작하기'), google('Google로 시작하기'); const AuthProviderKind(this.label); /// 버튼 라벨. 임의로 바꾸지 마라 — 화면 대조 대상이다. final String label; /// 이메일은 소셜이 아니라 입력 폼이 붙는다. 화면이 이걸로 갈린다. bool get needsForm => this == AuthProviderKind.email; } /// 이전 이름. 한 번에 다 바꾸지 않아도 되게 남겨 둔다. typedef SocialProvider = AuthProviderKind; /// 로그인된 계정. class AuthAccount { const AuthAccount({ required this.uid, required this.provider, this.displayName, this.email, this.isMock = false, }); final String uid; final AuthProviderKind? provider; final String? displayName; final String? email; /// Firebase 미설정 상태에서 만든 임시 세션인지. 실 세션과 절대 섞이면 안 된다. final bool isMock; } /// 로그인 시도 결과. sealed class AuthOutcome { const AuthOutcome(); } final class AuthSucceeded extends AuthOutcome { const AuthSucceeded(this.account); final AuthAccount account; } /// 사용자가 취소함. 에러로 취급하지 않는다. final class AuthCancelled extends AuthOutcome { const AuthCancelled(); } /// 실패. [code] 는 `wireframe/planning/error-catalog.yaml` 의 ID 다(소셜 실패 = E-AUTH-03). final class AuthFailed extends AuthOutcome { const AuthFailed(this.code, this.message); final String code; final String message; } /// 아직 쓸 수 없음 — 키·설정 파일 미투입. final class AuthNotConfigured extends AuthOutcome { const AuthNotConfigured(this.provider, this.missing); final AuthProviderKind provider; /// 투입해야 하는 것들(파일명·키명). 화면과 README 에 그대로 노출한다. final List missing; } /// 로그인 제공자 인터페이스. 이메일도 같은 인터페이스를 쓴다. abstract interface class SocialAuthProvider { AuthProviderKind get provider; /// 실 연동 코드 경로를 가지고 있는지. 스텁이면 false. bool get isLive; /// 비어 있지 않으면 지금은 로그인할 수 없다. [signIn] 은 [AuthNotConfigured] 를 낸다. List get missingConfig; Future signIn(); Future signOut(); } /// 이메일 제공자만 쓰는 확장 계약. /// /// 소셜은 외부 창이 열리고 끝이지만 이메일은 **앱 안에서 값을 받아야 한다.** /// [SocialAuthProvider.signIn] 하나로 묶으면 화면이 제공자 종류를 보고 분기하게 /// 되므로, 입력이 필요한 쪽만 별도 계약으로 뺀다. abstract interface class EmailAuthProvider implements SocialAuthProvider { Future signInWithEmail(String email, String password); Future signUpWithEmail(String email, String password); /// 비밀번호 재설정 메일 발송. Figma 「비밀번호 재설정」 화면이 이걸 부른다. Future sendPasswordReset(String email); } /// 앱 재실행·토큰 만료·다른 기기 로그아웃을 인증 상태에 반영하는 세션 경계. /// /// 저장소는 이 인터페이스만 알며 Firebase SDK 자체는 어댑터 뒤에 둔다. 덕분에 /// 외부 설정 파일 없이도 세션 복원과 만료 전이를 단위 테스트할 수 있다. abstract interface class AuthSession { AuthAccount? get currentAccount; Stream get accountChanges; Future signOut(); }