feat: PWA FCM 클라이언트 실통합 + 어댑터 키 주입 런처

- firebase SDK 통합: initPush getToken 활성화(동적 import) + isSupported 가드
- public/firebase-messaging-sw.js 백그라운드 수신 SW(웹 config 쿼리스트링 전달)
- run-api-with-adapters.sh: adapter.env.local 로드 후 ga-api bootRun
- 키 파일(adapter.env.local / .env.local)은 git-ignored 템플릿으로 별도 제공

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-06-01 22:56:44 +09:00
parent 5d5c2d53aa
commit 93ac94558f
5 changed files with 1109 additions and 17 deletions
+1037 -4
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -14,6 +14,7 @@
"antd-mobile-icons": "^0.3.0",
"axios": "^1.7.2",
"dayjs": "^1.11.11",
"firebase": "^12.14.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
@@ -0,0 +1,21 @@
/* FCM 백그라운드 메시지 수신 서비스워커 (vite-plugin-pwa SW 와 별개).
* Firebase 웹 config 는 정적 파일에 박지 않고, initPush 등록 시 쿼리스트링으로 전달받는다. */
importScripts('https://www.gstatic.com/firebasejs/12.14.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/12.14.0/firebase-messaging-compat.js');
const p = new URL(self.location).searchParams;
firebase.initializeApp({
apiKey: p.get('apiKey'),
projectId: p.get('projectId'),
appId: p.get('appId'),
messagingSenderId: p.get('messagingSenderId'),
});
firebase.messaging().onBackgroundMessage((payload) => {
const n = payload.notification || {};
self.registration.showNotification(n.title || '알림', {
body: n.body || '',
icon: '/pwa-192x192.png',
data: payload.data || {},
});
});
+36 -13
View File
@@ -6,11 +6,10 @@ import { pushApi } from '@/api/push';
* 활성 조건: 빌드 환경변수 `VITE_FCM_*` 가 모두 설정된 경우에만 동작한다.
* 미설정 시 no-op (개발/미연동 환경에서 안전).
*
* <b>운영 활성화 절차</b>:
* 1) Firebase 프로젝트 생성 → 웹 앱 등록 → 설정값을 .env 의 VITE_FCM_* 로 주입
* 2) `npm i firebase` 후 아래 TODO 블록의 firebase/messaging 초기화 활성화
* 3) getToken(VAPID key)으로 FCM 토큰 발급 → pushApi.register(token) 호출
* 4) public/firebase-messaging-sw.js 서비스워커 배치
* firebase SDK 통합 완료 — 활성 조건은 `.env(.local)` 의 VITE_FCM_* 주입뿐이다.
* 1) Firebase 콘솔 → 웹 앱 등록 → 설정값을 .env.local 의 VITE_FCM_* 로 주입
* 2) getToken(VAPID key)으로 FCM 토큰 발급 → pushApi.register(token) 자동 호출
* 3) public/firebase-messaging-sw.js 가 백그라운드 수신 처리
*
* 서버측(PushAdapter=fcm + FCM_PROJECT_ID/FCM_ACCESS_TOKEN)과 함께 활성화해야 실제 발송된다.
*/
@@ -38,14 +37,38 @@ export async function initPush(): Promise<void> {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
// TODO(운영): `npm i firebase` 후 활성화.
// const { initializeApp } = await import('firebase/app');
// const { getMessaging, getToken } = await import('firebase/messaging');
// const appFb = initializeApp(cfg);
// const messaging = getMessaging(appFb);
// const token = await getToken(messaging, { vapidKey: cfg.vapidKey });
// if (token) await pushApi.register(token, 'WEB');
void pushApi; // 등록 API 는 토큰 발급 후 호출 (위 TODO 활성화 시)
const { isSupported, getMessaging, getToken } = await import('firebase/messaging');
if (!(await isSupported())) {
if (import.meta.env.DEV) console.info('[push] 이 브라우저는 FCM 미지원 — skip.');
return;
}
const { initializeApp, getApps } = await import('firebase/app');
const appFb = getApps().length
? getApps()[0]
: initializeApp({
apiKey: cfg.apiKey,
projectId: cfg.projectId,
appId: cfg.appId,
messagingSenderId: cfg.messagingSenderId,
});
// FCM 백그라운드 수신용 서비스워커 (vite-plugin-pwa SW 와 별개).
// 웹 config 는 정적 SW 파일에 박지 않고 쿼리스트링으로 전달.
const swQuery = new URLSearchParams({
apiKey: cfg.apiKey,
projectId: cfg.projectId,
appId: cfg.appId,
messagingSenderId: cfg.messagingSenderId ?? '',
}).toString();
const swReg = await navigator.serviceWorker.register(`/firebase-messaging-sw.js?${swQuery}`);
const messaging = getMessaging(appFb);
const token = await getToken(messaging, {
vapidKey: cfg.vapidKey,
serviceWorkerRegistration: swReg,
});
if (token) await pushApi.register(token, 'WEB');
} catch (e) {
if (import.meta.env.DEV) console.warn('[push] init 실패', e);
}
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# 외부연동 어댑터 키를 주입하여 ga-api 를 기동한다.
# adapter.env.local 의 값을 환경변수로 로드한 뒤 bootRun.
set -euo pipefail
cd "$(dirname "$0")"
if [[ ! -f adapter.env.local ]]; then
echo "adapter.env.local 이 없습니다. 키를 먼저 채우세요." >&2
exit 1
fi
set -a; source adapter.env.local; set +a
echo "[adapters] bank=$ADAPTER_BANK message=$ADAPTER_MESSAGE push=$ADAPTER_PUSH"
./gradlew :ga-api:bootRun --args='--spring.profiles.active=trading_ai'