feat: 푸시 알림 파이프라인 — PushAdapter(FCM HTTP v1) + 디바이스토큰 등록 + 디스패치
마지막 외부연동 영역(푸시)을 채널=FCM HTTP v1 로 확정해 서버측 완전 구현.
Toast/KFTC 와 동일 패턴: 실 API·config-gated(app.adapter.push, 기본 mock)·fail-fast.
- ga-common: PushAdapter 인터페이스 + PushRequest/PushResponse
- ga-external-adapter: MockPushAdapter(기본) + FcmPushAdapter(POST /v1/projects/{id}/messages:send,
Bearer, notification+data 매핑, name→messageId) + FcmProperties. 의존성 추가 없음.
- V107 user_device_token(user_id/token/platform/is_active, UNIQUE token) + UserDeviceTokenVO/Mapper(upsert/selectActiveByUser/deactivate)
- ga-api:
- PushDispatchService.sendToUser — 사용자 활성 토큰 전부에 best-effort 푸시(예외 흡수)
- MobileMe: POST /api/mobile/me/device-tokens (등록 upsert) + /unregister (본인검증 deactivate)
- WithdrawBankSettleService.notify 에 푸시 디스패치 연동(출금 SETTLED/FAILED 시 인앱+푸시)
- ExternalCallLoggingAspect 에 PushAdapter 포인트컷 추가(외부호출 감사로그)
- application.yml app.adapter.push + app.adapter.fcm.* 설정
- ga-mobile-pwa: api/push.ts(register/unregister) + push/initPush.ts(config-gated, VITE_FCM_* 미설정 시 no-op) + 로그인 성공 시 initPush() 훅
- AdapterFailFastTest 에 FCM fail-fast 케이스 추가
검증: 전체 ./gradlew build(test) SUCCESSFUL, PWA 빌드 OK, Flyway V107 success,
디바이스토큰 등록/해제/검증 라이브 e2e PASS(등록200·해제200·is_active=N·빈토큰400).
실 푸시 발송은 Firebase 프로젝트(서버 FCM_PROJECT_ID/ACCESS_TOKEN + PWA VITE_FCM_* + firebase SDK) 주입 후 가능.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import api, { unwrap } from './request';
|
||||
|
||||
/**
|
||||
* 푸시 디바이스 토큰 등록/해제 API.
|
||||
* 서버측 푸시 파이프라인(PushAdapter/PushDispatchService)과 연동.
|
||||
*/
|
||||
export const pushApi = {
|
||||
register: (token: string, platform = 'WEB') =>
|
||||
unwrap<void>(api.post('/api/mobile/me/device-tokens', { token, platform })),
|
||||
unregister: (token: string) =>
|
||||
unwrap<void>(api.post('/api/mobile/me/device-tokens/unregister', { token })),
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { Button, Form, Input, NavBar, Toast } from 'antd-mobile';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { authApi } from '@/api/auth';
|
||||
import { initPush } from '@/push/initPush';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
|
||||
export default function Login() {
|
||||
@@ -19,6 +20,7 @@ export default function Login() {
|
||||
userId: 0, loginId: values.loginId,
|
||||
});
|
||||
Toast.show({ icon: 'success', content: '로그인 성공' });
|
||||
void initPush(); // 푸시 토큰 등록 (config-gated, 미설정 시 no-op)
|
||||
navigate(location.state?.from ?? '/', { replace: true });
|
||||
} catch {
|
||||
// request interceptor 가 토스트 처리
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { pushApi } from '@/api/push';
|
||||
|
||||
/**
|
||||
* 푸시 알림 초기화 (config-gated).
|
||||
*
|
||||
* 활성 조건: 빌드 환경변수 `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 서비스워커 배치
|
||||
*
|
||||
* 서버측(PushAdapter=fcm + FCM_PROJECT_ID/FCM_ACCESS_TOKEN)과 함께 활성화해야 실제 발송된다.
|
||||
*/
|
||||
export async function initPush(): Promise<void> {
|
||||
const cfg = {
|
||||
apiKey: import.meta.env.VITE_FCM_API_KEY,
|
||||
projectId: import.meta.env.VITE_FCM_PROJECT_ID,
|
||||
appId: import.meta.env.VITE_FCM_APP_ID,
|
||||
messagingSenderId: import.meta.env.VITE_FCM_SENDER_ID,
|
||||
vapidKey: import.meta.env.VITE_FCM_VAPID_KEY,
|
||||
};
|
||||
|
||||
// 환경변수 미설정 → 푸시 비활성 (no-op)
|
||||
if (!cfg.apiKey || !cfg.projectId || !cfg.appId || !cfg.vapidKey) {
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[push] VITE_FCM_* 미설정 — 푸시 비활성(서버는 mock).');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('serviceWorker' in navigator) || !('Notification' in window)) return;
|
||||
|
||||
try {
|
||||
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 활성화 시)
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('[push] init 실패', e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user