알림함 항목 생성 · 알림 이동 · 브리핑 항목 승인·상세 · 푸시 토픽을 가입 팬덤 전체로

This commit is contained in:
Sn-Kinos
2026-10-05 22:48:10 +09:00
parent fbf9a2c3d7
commit cf608a1d99
27 changed files with 714 additions and 69 deletions
+28 -20
View File
@@ -32,8 +32,8 @@ class PushMessaging {
StreamSubscription<RemoteMessage>? _opened;
StreamSubscription<String>? _tokenRefresh;
/// 소속 팬덤. 토픽 이름의 일부라 매번 사용자 문서를 읽지 않도록 기억한다.
String? _fandomId;
/// 가입한 팬덤 전부. 토픽 이름의 일부라 매번 읽지 않도록 기억한다.
Set<String>? _fandomIds;
/// 알림 권한 상태. 화면이 「거부됨」 안내와 재요청 버튼을 그릴 근거다.
AuthorizationStatus _status = AuthorizationStatus.notDetermined;
@@ -80,16 +80,25 @@ class PushMessaging {
}, SetOptions(merge: true));
}
/// 소속 팬덤 ID. 한 번 읽어 두고 다시 읽지 않는다.
Future<String> _resolveFandomId() async {
final cached = _fandomId;
/// 가입한 팬덤 ID 전부 — 대표 팬덤(`users.fandomId`) + 가입 목록(`users/{uid}/memberships`).
///
/// PRD-06 「관심 아티스트를 등록한 사용자」 · 별첨 1 ③-5 「FCM 푸시 수신」 — 대표 팬덤
/// 하나만 구독하면 다른 행성의 브리핑 · 이클립스 예고를 받지 못한다.
Future<Set<String>> _resolveFandomIds() async {
final cached = _fandomIds;
if (cached != null) return cached;
final uid = _backend.uid;
if (uid == null) return '';
if (uid == null) return const {};
final user = await _backend.db.doc('users/$uid').get();
final id = (user.data() ?? const <String, dynamic>{}).strOr('fandomId');
if (id.isNotEmpty) _fandomId = id;
return id;
final memberships = await _backend.db
.collection('users/$uid/memberships')
.get();
final ids = {
(user.data() ?? const <String, dynamic>{}).strOr('fandomId'),
for (final d in memberships.docs) d.id,
}..remove('');
if (ids.isNotEmpty) _fandomIds = ids;
return ids;
}
/// 설정대로 토픽을 맞춘다.
@@ -102,14 +111,14 @@ class PushMessaging {
/// 팬덤 ID 는 사용자 문서에서 스스로 읽는다. 호출자가 넘기게 하면 설정 화면이
/// 그 값을 알아야 하고, 화면이 알 이유가 없다.
Future<void> syncTopics(Map<PushTopic, bool> prefs) async {
final fandomId = await _resolveFandomId();
if (fandomId.isEmpty) return;
final plan = topicPlan(fandomId, prefs);
for (final topic in plan.subscribe) {
await _transport.subscribeToTopic(topic);
}
for (final topic in plan.unsubscribe) {
await _transport.unsubscribeFromTopic(topic);
for (final fandomId in await _resolveFandomIds()) {
final plan = topicPlan(fandomId, prefs);
for (final topic in plan.subscribe) {
await _transport.subscribeToTopic(topic);
}
for (final topic in plan.unsubscribe) {
await _transport.unsubscribeFromTopic(topic);
}
}
}
@@ -142,8 +151,7 @@ class PushMessaging {
/// 토픽 해지를 먼저 하는 이유는 실패 순서다. 토큰 삭제가 실패해도 구독은 끊겨
/// 알림이 멈춘다. 반대로 두면 토큰만 지워진 채 토픽 알림이 계속 온다.
Future<void> clearOnSignOut() async {
final fandomId = await _resolveFandomId();
if (fandomId.isNotEmpty) {
for (final fandomId in await _resolveFandomIds()) {
for (final topic in PushTopic.values) {
await _transport.unsubscribeFromTopic(topic.topicFor(fandomId));
}
@@ -158,7 +166,7 @@ class PushMessaging {
}
// 다음 로그인은 다른 계정일 수 있다. 팬덤을 들고 있으면 그 계정의 토픽에
// 구독해 버린다.
_fandomId = null;
_fandomIds = null;
}
Future<void> dispose() async {