import 'package:cloud_firestore/cloud_firestore.dart'; import '../../core/data/backend.dart'; /// 온보딩 6단계(10/3 통합회신 1항 · 해석 확인 Q-OB-02 A · FR-CMN-03~08)가 서버와 주고받는 것. abstract interface class OnboardingRepository { /// ② 약관 — `fnSignupConsent`. 필수 누락은 서버가 거부한다. Future agree(Set agreed, String version); /// ③ 프로필 — 닉네임(2~20 · 중복 허용) · 국가 · 표시 언어. 지금 값(있으면)을 돌려준다. Future<({String nickname, String? country})> currentProfile(); Future saveProfile({ required String nickname, required String country, required String locale, }); /// ③ 추천인 코드(선택) — `fnReferralAttribute`(10/3 7항 「가입 직후 온보딩에 선택 입력」). Future attributeReferral(String code); /// ④ 관심 아티스트 — 팬덤 행성/성운 목록. [query] 가 있으면 이름 앞부분으로 찾는다. Future> fandoms({String query = ''}); /// ⑤ 행성 가입 — 고른 순서대로 `fnFandomJoin`. 대표가 없던 계정은 첫 가입이 대표가 된다(C-15). Future join(List fandomIds); } class OnboardingFandom { const OnboardingFandom({ required this.id, required this.name, required this.members, required this.nebula, }); final String id; final String name; final int members; /// Stage0 성운인가. final bool nebula; } /// 계정이 온보딩을 거쳐야 하는가 — 서버에 가입 약관 동의(`termsAgreedAt`)가 없으면 그렇다. /// 트리거가 아직 사용자 문서를 만들지 않았어도(갓 생성된 계정) 그렇다. Future needsOnboardingFirestore(String uid) async { final snap = await FirebaseFirestore.instance.doc('users/$uid').get(); return snap.data()?['termsAgreedAt'] == null; } class FirestoreOnboardingRepository implements OnboardingRepository { FirestoreOnboardingRepository(this._backend); final Backend _backend; @override Future agree(Set agreed, String version) => _backend.call( 'fnSignupConsent', {'agreed': agreed.toList(), 'version': version}, ); @override Future<({String nickname, String? country})> currentProfile() async { final data = (await _backend.db.doc('users/${_backend.requireUid}').get()).data() ?? const {}; final nickname = data['nickname']; final country = data['countryCode']; return ( nickname: nickname is String ? nickname : '', country: country is String && country.isNotEmpty ? country : null, ); } @override Future saveProfile({ required String nickname, required String country, required String locale, }) => _backend.db.doc('users/${_backend.requireUid}').set({ 'nickname': nickname, 'countryCode': country, 'locale': locale, 'updatedAt': FieldValue.serverTimestamp(), }, SetOptions(merge: true)); @override Future attributeReferral(String code) => _backend.call( 'fnReferralAttribute', {'referralCode': code, 'channel': 'onboarding'}, ); @override Future> fandoms({String query = ''}) async { final col = _backend.db.collection('fandoms'); final q = query.trim(); // 검색은 이름 앞부분 일치(단일 필드 정렬이라 별도 색인이 필요 없다). 목록은 FPI 순. // ponytail: 페이지 50 건 — 팬덤이 늘면 스크롤 이어읽기를 붙인다. final snap = await (q.isEmpty ? col.orderBy('fpiScore', descending: true) : col .orderBy('name') .where('name', isGreaterThanOrEqualTo: q) .where('name', isLessThan: '$q')) .limit(50) .get(); return [ for (final d in snap.docs) OnboardingFandom( id: d.id, name: (d.data()['name'] as String?) ?? d.id, members: (d.data()['memberCount'] as num?)?.toInt() ?? 0, nebula: d.data()['stage'] == 0, ), ]; } @override Future join(List fandomIds) async { for (final id in fandomIds) { await _backend.call('fnFandomJoin', {'fandomId': id}); } } } /// 목 모드 · 테스트용. **정책값이 아니다.** class SampleOnboardingRepository implements OnboardingRepository { SampleOnboardingRepository(); final joined = []; Set? agreed; String? referral; ({String nickname, String country, String locale})? profile; @override Future agree(Set agreed, String version) async => this.agreed = agreed; @override Future<({String nickname, String? country})> currentProfile() async => (nickname: '', country: null); @override Future saveProfile({ required String nickname, required String country, required String locale, }) async => profile = (nickname: nickname, country: country, locale: locale); @override Future attributeReferral(String code) async => referral = code; @override Future> fandoms({String query = ''}) async => [ for (final f in const [ OnboardingFandom(id: 'bts', name: 'BTS', members: 12840, nebula: false), OnboardingFandom( id: 'newjeans', name: 'NewJeans', members: 11230, nebula: false, ), OnboardingFandom(id: 'aespa', name: 'aespa', members: 8, nebula: true), ]) if (f.name.toLowerCase().startsWith(query.trim().toLowerCase())) f, ]; @override Future join(List fandomIds) async => joined.addAll(fandomIds); }