Flow: AuthGate (인증 게이트)
비로그인 상태에서 보호된 액션을 호출하면 뜨는 로그인 유도 bottom sheet. App 전용 (Web 은 비로그인 피드라 게이트 개념 없음). lazy auth 모델의 핵심 — 진입 자체는 막지 않고, 보호 액션 시점에만 인증을 요구한다.
- Flutter (App):
mobile/lib/features/auth/presentation/widgets/auth_gate_modal.dart - 진입점:
requireAuth(context, ref, action, onAuthenticated)— 인증돼 있으면 콜백 즉시 실행, 아니면 게이트 표시 후 로그인 성공 시 콜백 실행 - 소셜 버튼 primitive:
app_social_button.dart(catalog) - bottom sheet primitive:
showAppBottomSheet
💡 시각 미리보기: design site / auth-gate
트리거 액션 (AuthGateAction)
게이트는 호출한 보호 액션에 따라 title/subtitle 카피가 달라진다. 4종:
| action | 트리거 위치 | title | subtitle |
|---|---|---|---|
reveal | 공시 상세 — AI 분석 보기 (screen-disclosure-detail) | "AI 분석을 보려면" | "AI 한입 분석 (주 10회 무료)" |
watchlist | 대시보드 — 관심 종목 추가 (screen-dashboard) | "관심 종목에 추가하려면" | "새 공시를 푸시로 알려드려요" |
feedback | 공시 상세 — 분석 피드백 | "피드백을 남기려면" | "분석 품질 개선에 도움이 돼요" |
signIn | 더보기 — 로그인 메뉴 (screen-settings) | "공시한입에 로그인" | "관심 종목 알림 · AI 분석 · 피드백 등 더 많은 기능" |
subtitle 의 quota 숫자("주 10회")는
WeeklyQuotaCopy.valuePropsingle source 를 통해서만 — 하드코딩 금지.
구성
bottom sheet (showAppBottomSheet, transparent chrome) 한 장:
| 영역 | 내용 |
|---|---|
| drag handle | 36×4 라운드 바 (상단 중앙) |
| title | action 별 카피 (xl, w800) |
| 닫기(X) | 우상단 PhosphorIcons.x IconButton — dismiss |
| subtitle | action 별 가치 카피 (sm, textSecondary) |
| 에러 배너 | 로그인 실패 시 errorBg 박스 (조건부) |
| 소셜 버튼 | AppSocialButton kakao / google / apple (플랫폼별 순서) |
소셜 버튼 순서 (플랫폼 분기)
정의 순서는 kakao → google → apple. 런타임에 플랫폼별로 재배열:
- iOS — apple 을 맨 위로 이동 (
apple, kakao, google). Apple 로그인 노출 (App Store 가이드라인) - Android — apple 제외 (
kakao, google). Kakao 가 메인 마켓이라 최상단 유지
게이트 동작 (requireAuth)
- bootstrapped 가드 — 앱 cold start 직후
AuthController.refetch()미완료 구간(저장 토큰 유효하지만user == null)이면 로그인 사용자에게도 게이트가 잘못 노출됨. 첫/auth/me응답까지 최대 ~2초 폴링(50ms 간격) 후 판정. timeout 시 그냥 진행(네트워크 장애 무한 대기 방지) - 인증됨 →
onAuthenticated()즉시 실행, 게이트 안 띄움 - 비로그인 → 게이트 표시 → 로그인 성공(
true) 시context.mounted+isLoggedIn재확인 후onAuthenticated()실행. dismiss(false) 시 무동작
로그인 처리 (소셜 버튼 탭)
각 provider 동일 패턴 (_loginKakao / _loginGoogle / _loginApple):
- 해당 버튼 spinner + 나머지 버튼 disabled (
_loading) {provider}LoginService.authenticate()→ 토큰 →authController.handleAuthToken(token, provider:)handleAuthToken은/auth/me실패 시 throw 없이 logged-out 으로 반환 가능 → 결과 재확인:isLoggedIn이면 게이트 닫고 성공 반환, 아니면 에러 배너 ("로그인 처리에 실패했어요. 잠시 후 다시 시도해주세요.")- provider 별 취소 예외(
*LoginCancelled)는 침묵, 실패 예외(*LoginFailed)는 에러 배너
Dismiss 정책
- scrim(배경) 탭으로 안 닫힘 (
isDismissible: false) — scrim 탭이 swipe 와 구분 안 돼auth_gate_dismissed.reason지표를 왜곡(panel 결정). dismiss 는 X 아이콘 + swipe down 만 - 로그인 처리 중(
_loading != null) dismiss 차단 —PopScope(canPop:)가 swipe-to-dismiss + system back 가로챔. OAuth 콜백 후handleAuthToken진행 중 닫히면 "로그인은 됐는데 onAuthenticated 누락" race 발생 useRootNavigator: true—_MainShell의 floating glass nav bar 위로 덮기 위해
Analytics
| 이벤트 | 시점 | payload |
|---|---|---|
auth_gate_shown | 게이트 노출 (initState) | action |
auth_gate_dismissed | X 클릭 → reason: close / swipe·back → reason: swipe | action, reason |
dispose 시 종결 이벤트 미송출이면
swipe로 보충 송출. 풀스크린 vs 모달 A/B 데이터 수집용.
상태 분기
- A1. idle — 소셜 버튼 노출, 에러 없음 (기본)
- A2. loading — 탭한 provider 버튼 spinner, 나머지 disabled, dismiss 차단
- A3. error — 에러 배너 + 버튼 재활성 (재시도 가능)
Edge cases
- cold start race — bootstrapped 가드가 흡수 (위 동작 1)
- 로그인 성공 직후 logout 동시 발생 —
onAuthenticated직전isLoggedIn재확인으로 차단 - handleAuthToken 부분 실패(
/auth/me단절) — 게이트 안 닫고 에러 배너 (버튼만 눌리고 무반응 방지)
관련 docs
- screen-disclosure-detail.md — reveal / feedback 트리거
- screen-dashboard.md — watchlist 트리거
- screen-settings.md — signIn 트리거
- social-login — 소셜 버튼 SSOT