앱 공통 계층 — 데이터 저장소 · 서버 시계 · 푸시 · 결제·광고 · 다국어 5종 · 행성 표현 · 공통 UI · 화면 자산
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:cloud_functions/cloud_functions.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../i18n/app_locale.dart';
|
||||
|
||||
/// 서버 경계 — 앱에서 Firestore·Cloud Functions 로 나가는 유일한 통로.
|
||||
///
|
||||
@@ -19,9 +21,11 @@ class Backend {
|
||||
FirebaseFirestore? firestore,
|
||||
FirebaseFunctions? functions,
|
||||
FirebaseAuth? auth,
|
||||
FirebaseStorage? storage,
|
||||
}) : _firestore = firestore ?? FirebaseFirestore.instance,
|
||||
_functions = functions,
|
||||
_auth = auth ?? FirebaseAuth.instance;
|
||||
_auth = auth ?? FirebaseAuth.instance,
|
||||
_storage = storage;
|
||||
|
||||
static const String region = 'asia-northeast3';
|
||||
|
||||
@@ -35,15 +39,24 @@ class Backend {
|
||||
FirebaseFunctions get functions =>
|
||||
_functions ??= FirebaseFunctions.instanceFor(region: region);
|
||||
|
||||
/// 사용자가 올리는 파일(프로필 사진 등)이 나가는 곳. Functions 와 같은 이유로
|
||||
/// 실제로 쓰기 전까지 만들지 않는다.
|
||||
FirebaseStorage? _storage;
|
||||
|
||||
FirebaseStorage get storage => _storage ??= FirebaseStorage.instance;
|
||||
|
||||
FirebaseFirestore get db => _firestore;
|
||||
|
||||
String? get uid => _auth.currentUser?.uid;
|
||||
|
||||
/// 로그인 계정 이메일. 닉네임이 없을 때 화면이 부르는 이름의 재료다.
|
||||
String? get email => _auth.currentUser?.email;
|
||||
|
||||
/// 로그인 필수 경로에서 UID 를 강제로 얻는다.
|
||||
String get requireUid {
|
||||
final value = uid;
|
||||
if (value == null) {
|
||||
throw const BackendException('E-AUTH-03', '로그인이 필요합니다.');
|
||||
throw BackendException('E-AUTH-03', appL10n.authLoginRequired);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -63,13 +76,34 @@ class Backend {
|
||||
if (data is Map) return Map<String, dynamic>.from(data);
|
||||
return const {};
|
||||
} on FirebaseFunctionsException catch (error) {
|
||||
throw BackendException(
|
||||
_catalogCode(error),
|
||||
error.message ?? '요청을 처리하지 못했습니다.',
|
||||
);
|
||||
final code = _catalogCode(error);
|
||||
throw BackendException(code, _message(code, error));
|
||||
}
|
||||
}
|
||||
|
||||
/// 앱이 문구를 가진 카탈로그 코드는 표시 언어로 바꾼다. 나머지는 서버 사유 그대로.
|
||||
static String _message(String code, FirebaseFunctionsException error) {
|
||||
final details = error.details;
|
||||
return switch (code) {
|
||||
// 성운 창립자 탄생 전 탈퇴(Q-PN-03).
|
||||
'E-FDM-05' => appL10n.fandomFounderLeaveBlocked,
|
||||
// 검증 활동(Quest 완료) 미달 — 서버가 `done` · `need` 를 싣는다(Campaign 개설자격 A안).
|
||||
'E-CMP-04'
|
||||
when details is Map &&
|
||||
details['done'] is int &&
|
||||
details['need'] is int =>
|
||||
appL10n.campaignNeedsQuests(
|
||||
'${details['done']}',
|
||||
'${details['need']}',
|
||||
'${(details['need'] as int) - (details['done'] as int)}',
|
||||
),
|
||||
// 보상 없는 사용자 생성 Quest 하루 상한 — 서버가 `cap` 을 싣는다(Q-UQ-02 보완).
|
||||
'E-QST-04' when details is Map && details['cap'] is int =>
|
||||
appL10n.questFreeDailyCap('${details['cap']}'),
|
||||
_ => error.message ?? appL10n.commonRequestFailed,
|
||||
};
|
||||
}
|
||||
|
||||
static String _catalogCode(FirebaseFunctionsException error) {
|
||||
final details = error.details;
|
||||
if (details is Map && details['code'] is String) {
|
||||
@@ -97,6 +131,29 @@ class BackendException implements Exception {
|
||||
String toString() => 'BackendException($code): $message';
|
||||
}
|
||||
|
||||
/// 다른 사용자에게도 보이는 이름 — 정책 원장 B-52 · G-07
|
||||
///
|
||||
/// 닉네임이 비었거나 **이메일 형태(`@` 포함)**면 [fallback] 을 쓴다. 예전에는 닉네임이
|
||||
/// 없을 때 이메일 앞부분을 이름으로 썼고, 그 값이 닉네임으로 저장돼 홈 기여자명에
|
||||
/// 노출됐다. UID 도 이름 자리에 넣지 않는다.
|
||||
///
|
||||
/// 서버 · 규칙은 이름 없는 작성자를 「팬」([storedFanName])으로 저장한다 — 읽을 때는 그 값도
|
||||
/// 지금 언어의 「팬」으로 바꿔 보여 준다.
|
||||
String publicName(Object? raw, [String? fallback]) {
|
||||
final v = raw is String ? raw.trim() : '';
|
||||
return v.isEmpty || v.contains('@') || v == storedFanName
|
||||
? (fallback ?? appL10n.commonFan)
|
||||
: v;
|
||||
}
|
||||
|
||||
/// 문서에 싣는 이름 없는 작성자 표기. 규칙 `publicNameOf` · 서버 `publicNameOf` 와 같은 값이라
|
||||
/// 언어와 무관하게 고정이다(번역된 「Fan」을 실으면 규칙이 사칭으로 거절한다).
|
||||
const String storedFanName = '팬';
|
||||
|
||||
/// 내 이름을 문서에 실을 때 — [publicName] 과 같되 대체값은 언어 무관 [storedFanName].
|
||||
String storedPublicName(Object? nickname) =>
|
||||
publicName(nickname, storedFanName);
|
||||
|
||||
/// Firestore 문서에서 값을 꺼내는 도우미.
|
||||
///
|
||||
/// 서버 스키마가 앞서 나가거나 필드가 아직 채워지지 않은 문서가 섞일 수 있어,
|
||||
|
||||
Reference in New Issue
Block a user