가입 시 약관 동의 단계 추가 — 필수·선택 항목 구분 · 개인정보처리방침 고지 · 동의 기록 저장

This commit is contained in:
Sn-Kinos
2026-10-11 23:43:25 +09:00
parent 9119b3beaa
commit c40bde2cd0
9 changed files with 564 additions and 51 deletions
+72 -1
View File
@@ -6,21 +6,85 @@ import '../../core/firebase/firebase_boot.dart';
import 'auth.dart';
import 'social_providers.dart';
/// 로그인 상태 보관소. 라우터가 이걸 듣고 로그인/셸을 가른다.
/// 가입 약관 관문 — 로그인한 계정이 약관 동의를 마쳤는가(`users/{uid}.termsAgreedAt`).
enum TermsGate { checking, required, done }
/// 로그인 상태 보관소. 라우터가 이걸 듣고 로그인/약관/셸을 가른다.
class AuthRepository extends ChangeNotifier {
AuthRepository({
List<SocialAuthProvider>? providers,
AuthSession? session,
EmailAuthProvider? qaEmailProvider,
bool? releaseMode,
Future<bool> Function(String uid)? needsTerms,
Future<void> Function(Set<String> agreed)? recordTerms,
}) : providers = providers ?? _defaultProviders(),
_qaEmail = qaEmailProvider ?? const EmailPasswordProvider(),
_session = session,
_needsTerms = needsTerms,
_recordTerms = recordTerms,
_account = session?.currentAccount,
// 기본값은 실제 빌드 모드. 테스트에서만 주입해 릴리스 동작을 재현한다
// (kReleaseMode 는 컴파일 상수라 테스트에서 뒤집을 수 없다).
_releaseMode = releaseMode ?? (kReleaseMode || kProfileMode) {
_sessionSubscription = session?.accountChanges.listen(_replaceAccount);
final initial = _account;
if (initial != null) unawaited(_checkTerms(initial));
}
/// 이 계정이 약관 동의를 거쳐야 하는가. null 이면 판정하지 않는다(목 모드 · 테스트).
///
/// **「로그인」 버튼으로 처음 들어온 소셜 계정도 여기서 걸린다** — 가입/로그인 버튼이나
/// 제공자(카카오 · LINE 커스텀 토큰 포함)와 무관하게, 서버에 동의 기록이 없으면 셸에 들어가지 못한다.
final Future<bool> Function(String uid)? _needsTerms;
/// 동의 기록(`fnSignupConsent`). 필수 누락은 서버가 거부한다.
final Future<void> Function(Set<String> agreed)? _recordTerms;
TermsGate _gate = TermsGate.done;
TermsGate get terms => _gate;
/// 가입 모드에서 제공자 인증 **전에** 동의한 약관. 인증 뒤 동의가 필요한 계정이면
/// 이 값으로 기록하고 약관 화면을 다시 띄우지 않는다.
Set<String>? pendingConsent;
/// 약관 동의를 서버에 기록하고 셸로 보낸다. 실패하면 예외를 그대로 던진다(화면이 안내).
Future<void> agreeTerms(Set<String> agreed) async {
await _recordTerms?.call(agreed);
_gate = TermsGate.done;
pendingConsent = null;
notifyListeners();
}
Future<void> _checkTerms(AuthAccount account) async {
final check = _needsTerms;
if (check == null || account.isMock) {
_gate = TermsGate.done;
return;
}
_gate = TermsGate.checking;
bool required;
try {
required = await check(account.uid);
} catch (e) {
// 판정하지 못하면 약관부터 다시 받는다 — 동의 없는 계정이 셸로 새는 쪽이 더 나쁘다.
debugPrint('[AuthRepository] terms check failed: ${e.runtimeType}');
required = true;
}
if (_account?.uid != account.uid) return;
final pending = pendingConsent;
pendingConsent = null;
if (required && pending != null) {
try {
await agreeTerms(pending);
return;
} catch (e) {
debugPrint('[AuthRepository] pending consent failed: ${e.runtimeType}');
}
if (_account?.uid != account.uid) return;
}
_gate = required ? TermsGate.required : TermsGate.done;
notifyListeners();
}
final bool _releaseMode;
@@ -172,7 +236,14 @@ class AuthRepository extends ChangeNotifier {
void _replaceAccount(AuthAccount? next) {
if (_sameAccount(_account, next)) return;
final uidChanged = _account?.uid != next?.uid;
_account = next;
if (next == null) {
_gate = TermsGate.done;
pendingConsent = null;
} else if (uidChanged) {
unawaited(_checkTerms(next));
}
notifyListeners();
}