import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.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 requiredConfigFiles = [ 'app/android/app/google-services.json', 'app/ios/Runner/GoogleService-Info.plist', ]; /// 화면에 보여 줄 미투입 항목 이름. 내부 경로를 담지 않는다. static const List 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 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 _initEmulator() async { try { await Firebase.initializeApp(options: _emulatorOptions); FirebaseAuth.instance.useAuthEmulator(emulatorHost, 9099); _mode = FirebaseMode.live; _reason = '로컬 에뮬레이터에 연결했습니다. 실 서버가 아닙니다.'; _detail = 'emulator host: $emulatorHost'; } on Object catch (error) { _mode = FirebaseMode.mock; _reason = '에뮬레이터에 연결하지 못해 임시(목) 모드로 동작 중입니다.'; _detail = '원인: $error'; } debugPrint('[FirebaseBoot] mode=${_mode.name} :: $_reason'); } /// 테스트에서 모드를 고정할 때만 쓴다. @visibleForTesting static void debugSetMode(FirebaseMode mode, {String reason = '테스트 지정'}) { _mode = mode; _reason = reason; _detail = null; } }