fix(flutter): 로그인 전에는 앱을 열 때마다 시작하기 화면을 보여 줌

"시작하기"를 한 번 누르면 기기에 저장해 이후 실행에서 인트로를 건너뛰었다.
그래서 시작하기를 누른 뒤 로그인하지 않고 앱을 다시 열면 스플래시 다음
바로 로그인 화면이 나왔다. 시작하기 여부는 이번 실행 동안만 기억한다.

- 로그아웃은 시안대로 곧바로 로그인 화면으로 간다
- 예전 버전이 저장한 entry.introSeen 값은 더 이상 읽지 않는다

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 21:51:06 +09:00
parent 1dba0c80ab
commit 3e6206630f
7 changed files with 51 additions and 30 deletions
@@ -2,21 +2,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tma_app/core/storage/preferences_provider.dart';
const String _kIntroSeenKey = 'entry.introSeen';
const String _kOnboardingSeenKey = 'entry.onboardingSeen';
/// 설치 후 1회만 보여 주는 화면(인트로·온보딩)의 확인 여부를 기기에 저장한다.
/// 설치 후 1회만 보여 주는 화면(온보딩)의 확인 여부를 기기에 저장한다.
/// (인트로는 로그인 전이면 실행할 때마다 보여 주므로 저장하지 않는다)
class EntryRepository {
EntryRepository(this._prefs);
final SharedPreferences _prefs;
bool get introSeen => _prefs.getBool(_kIntroSeenKey) ?? false;
bool get onboardingSeen => _prefs.getBool(_kOnboardingSeenKey) ?? false;
Future<void> markIntroSeen() => _prefs.setBool(_kIntroSeenKey, true);
Future<void> markOnboardingSeen() => _prefs.setBool(_kOnboardingSeenKey, true);
}
@@ -1,27 +1,29 @@
/// 진입 단계 — 설치 후 첫 진입: 스플래시 → 인트로 → 로그인 → 온보딩 → 앱
/// 진입 단계 — 로그인 전: 스플래시 → 인트로 → 로그인 → (첫 로그인이면) 온보딩 → 앱
enum EntryStage { intro, login, onboarding, app }
/// 진입 흐름 상태. 세 플래그로 지금 보여 줄 단계가 정해진다.
class EntryState {
const EntryState({
required this.introSeen,
required this.introPassed,
required this.signedIn,
required this.onboardingSeen,
});
final bool introSeen;
/// 이번 실행에서 인트로의 "시작하기"를 눌렀는지 — 저장하지 않는다.
/// 로그인하지 않은 채 앱을 다시 열면 인트로부터 다시 보여 준다.
final bool introPassed;
final bool signedIn;
final bool onboardingSeen;
EntryStage get stage {
if (!signedIn) return introSeen ? EntryStage.login : EntryStage.intro;
if (!signedIn) return introPassed ? EntryStage.login : EntryStage.intro;
if (!onboardingSeen) return EntryStage.onboarding;
return EntryStage.app;
}
EntryState copyWith({bool? introSeen, bool? signedIn, bool? onboardingSeen}) {
EntryState copyWith({bool? introPassed, bool? signedIn, bool? onboardingSeen}) {
return EntryState(
introSeen: introSeen ?? this.introSeen,
introPassed: introPassed ?? this.introPassed,
signedIn: signedIn ?? this.signedIn,
onboardingSeen: onboardingSeen ?? this.onboardingSeen,
);
@@ -12,17 +12,14 @@ class EntryNotifier extends Notifier<EntryState> {
@override
EntryState build() {
return EntryState(
introSeen: ref.watch(entryRepositoryProvider).introSeen,
introPassed: false,
signedIn: ref.watch(authRepositoryProvider).hasSession,
onboardingSeen: ref.watch(entryRepositoryProvider).onboardingSeen,
);
}
Future<void> completeIntro() async {
await _entry.markIntroSeen();
if (!ref.mounted) return;
state = state.copyWith(introSeen: true);
}
/// 인트로의 "시작하기" — 이번 실행 동안만 기억한다
void completeIntro() => state = state.copyWith(introPassed: true);
Future<void> signIn(SocialProvider provider) async {
await _auth.signIn(provider);
@@ -36,11 +33,12 @@ class EntryNotifier extends Notifier<EntryState> {
state = state.copyWith(onboardingSeen: true);
}
/// 로그아웃하면 로그인 화면으로 돌아간다 (인트로·온보딩은 다시 보이지 않는다)
/// 로그아웃하면 곧바로 로그인 화면으로 돌아간다 (시안 — 인트로를 거치지 않는다).
/// 이후 앱을 새로 열면 로그인 전이므로 인트로부터 보인다.
Future<void> signOut() async {
await _auth.signOut();
if (!ref.mounted) return;
state = state.copyWith(signedIn: false);
state = state.copyWith(signedIn: false, introPassed: true);
}
}
@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -23,12 +22,12 @@ const Alignment _kArtAlignment = Alignment(.12, .02);
/// 시안: CTA 는 화면 아래에서 80 (홈 인디케이터 34 포함)
const double _kCtaBottomGap = 46;
/// 인트로 — 설치 후 첫 진입(스플래시 다음)에 한 번 보여 준다.
/// 인트로 — 로그인하지 않은 상태로 앱을 열면 스플래시 다음에 보여 준다.
class IntroPage extends ConsumerWidget {
const IntroPage({super.key});
void _start(BuildContext context, WidgetRef ref) {
unawaited(ref.read(entryProvider.notifier).completeIntro());
ref.read(entryProvider.notifier).completeIntro();
context.go(AppRoutes.login);
}