From 9a04bf56d98270cace53c80d836fc9c4af57e4ee Mon Sep 17 00:00:00 2001 From: Sn-Kinos Date: Mon, 14 Sep 2026 15:05:35 +0900 Subject: [PATCH] =?UTF-8?q?Callable=20=EB=A6=AC=EC=A0=84=EC=9D=84=20?= =?UTF-8?q?=EC=84=9C=EC=9A=B8=EB=A1=9C=20=EA=B3=A0=EC=A0=95=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=B6=80=ED=8A=B8=EC=8A=A4=ED=8A=B8=EB=9E=A9?= =?UTF-8?q?=EC=9D=84=20=ED=95=A8=EC=88=98=20=EC=A0=95=EC=9D=98=EB=B3=B4?= =?UTF-8?q?=EB=8B=A4=20=EB=A8=BC=EC=A0=80=20=C2=B7=20=EC=97=90=EB=AE=AC?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=84=B0=20=EC=97=B0=EA=B2=B0=EC=9D=B4=20?= =?UTF-8?q?=EB=AA=A9=20=EB=AA=A8=EB=93=9C=EB=A1=9C=20=EB=82=B4=EB=A0=A4?= =?UTF-8?q?=EA=B0=80=EC=A7=80=20=EC=95=8A=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/firebase/functions/src/core/bootstrap.ts | 19 +++++++++++++++++++ app/firebase/functions/src/index.ts | 13 ++++--------- app/lib/core/firebase/firebase_boot.dart | 20 ++++++++++++++++++-- 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 app/firebase/functions/src/core/bootstrap.ts diff --git a/app/firebase/functions/src/core/bootstrap.ts b/app/firebase/functions/src/core/bootstrap.ts new file mode 100644 index 0000000..1a84e80 --- /dev/null +++ b/app/firebase/functions/src/core/bootstrap.ts @@ -0,0 +1,19 @@ +import { initializeApp } from 'firebase-admin/app'; +import { setGlobalOptions } from 'firebase-functions/v2'; + +/** + * 런타임 전역 설정 — **함수 정의보다 먼저 서야 한다.** + * + * 예전에는 `index.ts` 본문에서 `setGlobalOptions()` 를 불렀는데, ES 모듈은 `export … + * from` 을 **본문보다 먼저** 평가한다. 그래서 모든 `onCall` 이 만들어진 뒤에야 리전이 + * 정해졌고, 함수는 전부 기본 리전(`us-central1`)에 실렸다. 앱은 서울(`asia-northeast3`)로 + * 부르므로 **모든 Callable 이 `NOT_FOUND`** 로 떨어진다 — 원인이 「함수가 없다」처럼 + * 보여 찾기 어려운 자리다(에뮬레이터 왕복에서 실측). + * + * 이 파일을 `index.ts` 의 **첫 import** 로 두면 같은 규칙이 반대로 작동한다. import 는 + * 선언 순서대로 평가되므로, 함수 모듈보다 먼저 여기가 돈다. + */ +initializeApp(); + +// 리전을 서울로 고정한다. 기본값(us-central1)이면 왕복 지연이 그대로 체감된다. +setGlobalOptions({ region: 'asia-northeast3', maxInstances: 20 }); diff --git a/app/firebase/functions/src/index.ts b/app/firebase/functions/src/index.ts index 373334a..71df7df 100644 --- a/app/firebase/functions/src/index.ts +++ b/app/firebase/functions/src/index.ts @@ -1,12 +1,7 @@ -import { initializeApp } from 'firebase-admin/app'; -import { setGlobalOptions } from 'firebase-functions/v2'; - -// Admin SDK 는 보안 규칙을 우회한다. 그래서 재화를 건드리는 경로가 전부 -// 이 패키지 안에만 있어야 하고, 규칙 층은 클라이언트 쓰기를 전면 차단한다. -initializeApp(); - -// 리전을 서울로 고정한다. 기본값(us-central1)이면 왕복 지연이 그대로 체감된다. -setGlobalOptions({ region: 'asia-northeast3', maxInstances: 20 }); +// **첫 줄이어야 한다.** Admin SDK 초기화와 리전 고정이 함수 정의보다 먼저 서야 하는데, +// ES 모듈은 아래 `export … from` 을 본문보다 먼저 평가한다. 본문에서 부르면 모든 함수가 +// 기본 리전에 실려 앱의 호출이 `NOT_FOUND` 로 떨어진다 — `core/bootstrap.ts` 참고. +import './core/bootstrap.js'; export { fnAuthOnCreate, fnConsentRecord, fnAdminSetRole, diff --git a/app/lib/core/firebase/firebase_boot.dart b/app/lib/core/firebase/firebase_boot.dart index c304801..a88c56f 100644 --- a/app/lib/core/firebase/firebase_boot.dart +++ b/app/lib/core/firebase/firebase_boot.dart @@ -92,20 +92,36 @@ abstract final class FirebaseBoot { static Future _initEmulator() async { try { - await Firebase.initializeApp(options: _emulatorOptions); + // **이미 초기화된 앱이 있으면 그것을 쓴다.** 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 = '로컬 에뮬레이터에 연결했습니다. 실 서버가 아닙니다.'; - _detail = 'emulator host: $emulatorHost'; + // 설정 파일이 있는 빌드에서는 프로젝트 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'); } /// 테스트에서 모드를 고정할 때만 쓴다.