INSIDE · STATION · 참여형 번역 · 알림 · MY 화면
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
import '../../core/data/backend.dart';
|
||||
import 'inside_data.dart';
|
||||
|
||||
/// INSIDE 목록의 실 서버 구현 — 화면 대조 원장 F3.
|
||||
///
|
||||
/// 정렬 셋(`최신순` · `인기순` · `POP순`)이 Figma 에서는 별도 화면이지만 여기서는
|
||||
/// orderBy 필드만 다른 같은 질의다. 세 화면을 따로 두면 페이지네이션 커서도 셋이
|
||||
/// 되고, 한쪽만 고치는 사고가 난다.
|
||||
///
|
||||
/// 커서는 `startAfterDocument` 로 잡는다. `limit` 을 커서보다 먼저 걸면 둘째 장이
|
||||
/// 항상 비어 돌아온다 — 라운지에서 한 번 겪은 결함이라 순서를 지킨다.
|
||||
class FirestoreInsideRepository implements InsideRepository {
|
||||
FirestoreInsideRepository(this._backend);
|
||||
|
||||
final Backend _backend;
|
||||
|
||||
/// 사용자 문서(소속 팬덤 · 표시명). 라우터는 이 값을 모르므로 한 번 읽어 들고
|
||||
/// 있는다. 탭을 옮길 때마다 다시 읽으면 왕복이 그대로 쌓인다.
|
||||
Map<String, dynamic>? _user;
|
||||
|
||||
Future<Map<String, dynamic>> _me() async => _user ??=
|
||||
(await _backend.db.doc('users/${_backend.requireUid}').get()).data() ??
|
||||
const {};
|
||||
|
||||
Future<String> _resolveFandomId() async => (await _me()).strOr('fandomId');
|
||||
|
||||
DocumentSnapshot<Map<String, dynamic>>? _cursor;
|
||||
InsideSort? _cursorSort;
|
||||
|
||||
static const int pageSize = 5;
|
||||
|
||||
Query<Map<String, dynamic>> _query(InsideSort sort, String fandomId) {
|
||||
Query<Map<String, dynamic>> q = _backend.db.collection('questions');
|
||||
if (fandomId.isNotEmpty) {
|
||||
q = q.where('fandomId', isEqualTo: fandomId);
|
||||
}
|
||||
return q.orderBy(sort.field, descending: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InsideSnapshot> load({InsideSort sort = InsideSort.latest}) async {
|
||||
final fandomId = await _resolveFandomId();
|
||||
final snap = await _query(sort, fandomId).limit(pageSize).get();
|
||||
_cursor = snap.docs.isEmpty ? null : snap.docs.last;
|
||||
_cursorSort = sort;
|
||||
return InsideSnapshot(
|
||||
questions: [for (final doc in snap.docs) _question(doc)],
|
||||
sort: sort,
|
||||
// 작성 버튼은 팬덤에 가입한 사용자에게만 보인다(명세 2.70-3).
|
||||
canWrite: fandomId.isNotEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<InsideQuestion>> more({
|
||||
required InsideSort sort,
|
||||
int limit = pageSize,
|
||||
}) async {
|
||||
// 정렬이 바뀌면 앞 장의 커서는 다른 순서의 위치라 의미가 없다. 처음부터 읽는다.
|
||||
if (_cursorSort != sort) _cursor = null;
|
||||
|
||||
var query = _query(sort, await _resolveFandomId());
|
||||
final cursor = _cursor;
|
||||
if (cursor != null) query = query.startAfterDocument(cursor);
|
||||
|
||||
final snap = await query.limit(limit).get();
|
||||
if (snap.docs.isNotEmpty) {
|
||||
_cursor = snap.docs.last;
|
||||
_cursorSort = sort;
|
||||
}
|
||||
return [for (final doc in snap.docs) _question(doc)];
|
||||
}
|
||||
|
||||
InsideQuestion _question(QueryDocumentSnapshot<Map<String, dynamic>> doc) {
|
||||
final data = doc.data();
|
||||
return InsideQuestion(
|
||||
id: doc.id,
|
||||
title: data.strOr('title'),
|
||||
body: data.strOr('body'),
|
||||
authorName: publicName(data['authorName']),
|
||||
createdAt: data.dateOr('createdAt', DateTime.now()),
|
||||
bountyPop: data.intOr('bountyPop'),
|
||||
answerCount: data.intOr('answerCount'),
|
||||
viewCount: data.intOr('viewCount'),
|
||||
// 질문 문서에 아직 없는 필드다(fn-question-create 가 쓰지 않음). 없으면 0 · 빈 목록.
|
||||
likeCount: data.intOr('likeCount'),
|
||||
imageUrls: [
|
||||
for (final u in (data['imageUrls'] as List?) ?? const []) '$u',
|
||||
],
|
||||
isMine: data.strOr('authorUid') == _backend.uid,
|
||||
isSolved: data.strOr('status') == 'accepted',
|
||||
refund: InsideRefund.fromStatus(data.strOr('status')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> balance() async {
|
||||
final wallet =
|
||||
(await _backend.db
|
||||
.doc('users/${_backend.requireUid}/wallet/main')
|
||||
.get())
|
||||
.data() ??
|
||||
const {};
|
||||
return wallet.intOr('activityBalance') + wallet.intOr('supportBalance');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> minBounty() async {
|
||||
final policy = await _backend.db.doc('config/policy').get();
|
||||
final value = (policy.data() ?? const {}).intOr('qnaMinBountyPop');
|
||||
return value > 0 ? value : 100;
|
||||
}
|
||||
|
||||
/// 실패는 [BackendException] 으로 그대로 올린다 — 화면이 성공처럼 그리지 않게.
|
||||
@override
|
||||
Future<String> create({
|
||||
required String title,
|
||||
required String body,
|
||||
required int bountyPop,
|
||||
required String clientSeq,
|
||||
}) async {
|
||||
final result = await _backend.call('fnQuestionCreate', {
|
||||
'title': title,
|
||||
'body': body,
|
||||
'bountyPop': bountyPop,
|
||||
'clientSeq': clientSeq,
|
||||
});
|
||||
return result.strOr('questionId');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<InsideAnswer>> answers(String questionId) async {
|
||||
final snap = await _backend.db
|
||||
.collection('questions/$questionId/answers')
|
||||
.orderBy('createdAt', descending: true)
|
||||
.get();
|
||||
return [
|
||||
for (final doc in snap.docs)
|
||||
if (doc.data().strOr('visibility', 'visible') == 'visible')
|
||||
InsideAnswer(
|
||||
id: doc.id,
|
||||
authorName: publicName(doc.data()['authorName']),
|
||||
body: doc.data().strOr('body'),
|
||||
createdAt: doc.data().dateOr('createdAt', DateTime.now()),
|
||||
isMine: doc.data().strOr('authorUid') == _backend.uid,
|
||||
isAccepted: doc.data().boolOr('accepted'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// 표시명 = 공개 닉네임(`publicName`, 없으면 「팬」). 공급자 실명(`displayName`)은
|
||||
/// 쓰지 않는다(M-7). 규칙 `authorNameIsPublic` 이 같은 계산으로 대조한다.
|
||||
@override
|
||||
Future<void> answer(String questionId, String body) async {
|
||||
final me = await _me();
|
||||
await _backend.db.collection('questions/$questionId/answers').add({
|
||||
'authorUid': _backend.requireUid,
|
||||
'authorName': storedPublicName(me['nickname']),
|
||||
'body': body,
|
||||
'accepted': false,
|
||||
'visibility': 'visible',
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> accept(String questionId, String answerId) => _backend.call(
|
||||
'fnAnswerAccept',
|
||||
{'questionId': questionId, 'answerId': answerId},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user