135 lines
6.4 KiB
Dart
135 lines
6.4 KiB
Dart
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:cloud_functions/cloud_functions.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../data/backend.dart';
|
|
|
|
/// Firebase 구동 모드.
|
|
enum FirebaseMode {
|
|
/// 설정 파일이 투입돼 Firebase 가 실제로 초기화된 상태.
|
|
live,
|
|
|
|
/// 설정 파일이 없어 로컬 목(mock) 으로 동작하는 상태. 네트워크 호출을 하지 않는다.
|
|
mock,
|
|
}
|
|
|
|
/// Firebase 초기화 게이트.
|
|
///
|
|
/// `google-services.json`(Android) · `GoogleService-Info.plist`(iOS) 가 아직 리포에 없다.
|
|
/// 두 파일은 **스위치 내부 조치 사항 S1·S2**(3주차 빌드 전)이며, 없는 동안에는
|
|
/// 초기화가 실패하는 게 정상이다. 실패를 삼키지 않고 [mode] 를 mock 으로 내려
|
|
/// 앱이 뜨게 하고, 사유를 로그로 남긴다.
|
|
///
|
|
/// 파일이 투입되면 이 클래스는 코드 수정 없이 [FirebaseMode.live] 로 올라간다.
|
|
abstract final class FirebaseBoot {
|
|
static FirebaseMode _mode = FirebaseMode.mock;
|
|
static String _reason = '아직 init() 을 부르지 않았다.';
|
|
static String? _detail;
|
|
|
|
static FirebaseMode get mode => _mode;
|
|
static bool get isLive => _mode == FirebaseMode.live;
|
|
|
|
/// 현재 모드가 된 사유 — 화면에 그대로 띄운다.
|
|
/// **내부 경로·예외 문자열을 넣지 마라.** 사용자에게 보이는 문자열이다.
|
|
static String get reason => _reason;
|
|
|
|
/// 투입 대상 파일 경로와 원시 예외. 로그·진단용이고 화면에는 내보내지 않는다.
|
|
static String? get detail => _detail;
|
|
|
|
/// 설정 파일이 없을 때 투입해야 하는 파일 목록.
|
|
/// **리포 경로다 — 로그·문서 전용이고 화면에 내보내지 마라.** 화면에는 [missingLabels] 를 쓴다.
|
|
static const List<String> requiredConfigFiles = [
|
|
'android/app/google-services.json',
|
|
'ios/Runner/GoogleService-Info.plist',
|
|
];
|
|
|
|
/// 화면에 보여 줄 미투입 항목 이름. 내부 경로를 담지 않는다.
|
|
static const List<String> missingLabels = ['Firebase 설정'];
|
|
|
|
/// 에뮬레이터 호스트. `--dart-define=FANUZU_EMULATOR_HOST=127.0.0.1` 로 켠다.
|
|
///
|
|
/// **왜 두는가** — 실 Firebase 프로젝트가 열리기 전에도 로그인·원장·규칙이
|
|
/// 실제로 도는지 확인할 수 있어야 한다. 설정 파일을 기다리는 동안 이 경로가
|
|
/// 없으면 서버 코드가 한 줄도 실행되지 않은 채 쌓인다.
|
|
///
|
|
/// 릴리스 빌드에서는 열리지 않는다. 배포된 앱이 로컬 호스트를 바라보면
|
|
/// 아무것도 동작하지 않고 원인도 보이지 않는다.
|
|
static const String emulatorHost =
|
|
String.fromEnvironment('FANUZU_EMULATOR_HOST');
|
|
|
|
static bool get useEmulator => emulatorHost.isNotEmpty && !kReleaseMode;
|
|
|
|
/// 에뮬레이터 전용 설정. 실 프로젝트 값이 아니며 비밀값도 아니다.
|
|
/// `demo-` 로 시작하는 프로젝트 ID 는 에뮬레이터가 외부 접속을 막는다.
|
|
static const FirebaseOptions _emulatorOptions = FirebaseOptions(
|
|
apiKey: 'demo-key',
|
|
appId: '1:0:android:0',
|
|
messagingSenderId: '0',
|
|
projectId: 'demo-fanuzu',
|
|
);
|
|
|
|
static Future<void> init() async {
|
|
if (useEmulator) {
|
|
await _initEmulator();
|
|
return;
|
|
}
|
|
try {
|
|
await Firebase.initializeApp();
|
|
_mode = FirebaseMode.live;
|
|
_reason = 'Firebase 설정 파일이 확인되어 실 모드로 초기화했습니다.';
|
|
_detail = null;
|
|
} on Object catch (error) {
|
|
// 설정 파일 미투입이면 여기로 떨어진다 — 예상된 경로다.
|
|
_mode = FirebaseMode.mock;
|
|
_reason = 'Firebase 설정이 아직 투입되지 않아 임시(목) 모드로 동작 중입니다.';
|
|
_detail = '투입 대상: ${requiredConfigFiles.join(' · ')} / 원인: $error';
|
|
}
|
|
debugPrint('[FirebaseBoot] mode=${_mode.name} :: $_reason');
|
|
if (_detail != null) debugPrint('[FirebaseBoot] detail :: $_detail');
|
|
}
|
|
|
|
static Future<void> _initEmulator() async {
|
|
try {
|
|
// **이미 초기화된 앱이 있으면 그것을 쓴다.** Android 는 `google-services.json` 이
|
|
// 있으면 네이티브 SDK 가 앱이 뜰 때 `[DEFAULT]` 를 만들어 둔다. 그 자리에 다시
|
|
// 만들면 `duplicate-app` 으로 떨어지고, 예외를 삼키던 이 경로가 **조용히 목 모드로**
|
|
// 내려간다 — 에뮬레이터를 물린 줄 알고 도는 확인이 전부 무의미해진다.
|
|
//
|
|
// `Firebase.apps` 로는 미리 알 수 없다. 네이티브가 만든 앱은 Dart 쪽 목록에
|
|
// 아직 올라와 있지 않아서, 만들어 보고 걸리면 그때 집는다.
|
|
try {
|
|
await Firebase.initializeApp(options: _emulatorOptions);
|
|
} on FirebaseException catch (error) {
|
|
if (error.code != 'duplicate-app') rethrow;
|
|
}
|
|
FirebaseAuth.instance.useAuthEmulator(emulatorHost, 9099);
|
|
FirebaseFirestore.instance.useFirestoreEmulator(emulatorHost, 8080);
|
|
FirebaseFunctions.instanceFor(region: Backend.region)
|
|
.useFunctionsEmulator(emulatorHost, 5001);
|
|
_mode = FirebaseMode.live;
|
|
_reason = '로컬 에뮬레이터에 연결했습니다. 실 서버가 아닙니다.';
|
|
// 설정 파일이 있는 빌드에서는 프로젝트 ID 가 실 프로젝트 그대로다. 에뮬레이터는
|
|
// 프로젝트 ID 별로 칸을 나누므로, 시드를 넣을 때 같은 ID 를 써야 한다.
|
|
_detail = 'emulator host: $emulatorHost · project: ${Firebase.app().options.projectId}';
|
|
} on Object catch (error) {
|
|
_mode = FirebaseMode.mock;
|
|
_reason = '에뮬레이터에 연결하지 못해 임시(목) 모드로 동작 중입니다.';
|
|
_detail = '원인: $error';
|
|
}
|
|
debugPrint('[FirebaseBoot] mode=${_mode.name} :: $_reason');
|
|
// 사유를 함께 남긴다. 이 경로가 조용히 목 모드로 내려가면 「에뮬레이터를 물렸다고
|
|
// 믿고 도는 확인」이 되므로, 원인이 로그에 없으면 찾는 데만 한참 걸린다.
|
|
if (_detail != null) debugPrint('[FirebaseBoot] detail :: $_detail');
|
|
}
|
|
|
|
/// 테스트에서 모드를 고정할 때만 쓴다.
|
|
@visibleForTesting
|
|
static void debugSetMode(FirebaseMode mode, {String reason = '테스트 지정'}) {
|
|
_mode = mode;
|
|
_reason = reason;
|
|
_detail = null;
|
|
}
|
|
}
|