가입 약관 동의 기록 · 서비스 탈퇴 · 온보딩 6단계 · Coach Mark
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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<void> agree(Set<String> agreed, String version);
|
||||
|
||||
/// ③ 프로필 — 닉네임(2~20 · 중복 허용) · 국가 · 표시 언어. 지금 값(있으면)을 돌려준다.
|
||||
Future<({String nickname, String? country})> currentProfile();
|
||||
Future<void> saveProfile({
|
||||
required String nickname,
|
||||
required String country,
|
||||
required String locale,
|
||||
});
|
||||
|
||||
/// ③ 추천인 코드(선택) — `fnReferralAttribute`(10/3 7항 「가입 직후 온보딩에 선택 입력」).
|
||||
Future<void> attributeReferral(String code);
|
||||
|
||||
/// ④ 관심 아티스트 — 팬덤 행성/성운 목록. [query] 가 있으면 이름 앞부분으로 찾는다.
|
||||
Future<List<OnboardingFandom>> fandoms({String query = ''});
|
||||
|
||||
/// ⑤ 행성 가입 — 고른 순서대로 `fnFandomJoin`. 대표가 없던 계정은 첫 가입이 대표가 된다(C-15).
|
||||
Future<void> join(List<String> 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<bool> 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<void> agree(Set<String> 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 <String, dynamic>{};
|
||||
final nickname = data['nickname'];
|
||||
final country = data['countryCode'];
|
||||
return (
|
||||
nickname: nickname is String ? nickname : '',
|
||||
country: country is String && country.isNotEmpty ? country : null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<void> attributeReferral(String code) => _backend.call(
|
||||
'fnReferralAttribute',
|
||||
{'referralCode': code, 'channel': 'onboarding'},
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<OnboardingFandom>> 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<void> join(List<String> fandomIds) async {
|
||||
for (final id in fandomIds) {
|
||||
await _backend.call('fnFandomJoin', {'fandomId': id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 목 모드 · 테스트용. **정책값이 아니다.**
|
||||
class SampleOnboardingRepository implements OnboardingRepository {
|
||||
SampleOnboardingRepository();
|
||||
|
||||
final joined = <String>[];
|
||||
Set<String>? agreed;
|
||||
String? referral;
|
||||
({String nickname, String country, String locale})? profile;
|
||||
|
||||
@override
|
||||
Future<void> agree(Set<String> agreed, String version) async =>
|
||||
this.agreed = agreed;
|
||||
|
||||
@override
|
||||
Future<({String nickname, String? country})> currentProfile() async =>
|
||||
(nickname: '', country: null);
|
||||
|
||||
@override
|
||||
Future<void> saveProfile({
|
||||
required String nickname,
|
||||
required String country,
|
||||
required String locale,
|
||||
}) async => profile = (nickname: nickname, country: country, locale: locale);
|
||||
|
||||
@override
|
||||
Future<void> attributeReferral(String code) async => referral = code;
|
||||
|
||||
@override
|
||||
Future<List<OnboardingFandom>> 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<void> join(List<String> fandomIds) async => joined.addAll(fandomIds);
|
||||
}
|
||||
Reference in New Issue
Block a user