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,11 @@
|
||||
package com.ga.common.adapter.push;
|
||||
|
||||
/**
|
||||
* 모바일 푸시 알림 외부연동 어댑터.
|
||||
* 구현체는 ga-external-adapter 모듈에 위치(Mock / FCM).
|
||||
*/
|
||||
public interface PushAdapter {
|
||||
|
||||
/** 단건 디바이스 토큰으로 푸시 발송. */
|
||||
PushResponse send(PushRequest request);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ga.common.adapter.push;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PushRequest {
|
||||
private String deviceToken; // FCM 등록 토큰
|
||||
private String title;
|
||||
private String body;
|
||||
private String refType; // WITHDRAW/APPROVAL/SETTLE 등 (data payload)
|
||||
private Long refId;
|
||||
private Long refUserId; // 추적용 user_id
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ga.common.adapter.push;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PushResponse {
|
||||
private String messageId; // 외부(FCM) 발급 message name/id
|
||||
private boolean sent;
|
||||
private String resultCode;
|
||||
private String resultMessage;
|
||||
private LocalDateTime sentAt;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
-- V107: user_device_token — 푸시 알림용 디바이스(FCM) 토큰 등록
|
||||
-- 사용자(설계사 PWA 등)가 발급받은 FCM 토큰을 저장. 알림 발생 시 활성 토큰으로 푸시 발송.
|
||||
|
||||
CREATE TABLE user_device_token (
|
||||
token_id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(user_id),
|
||||
token VARCHAR(512) NOT NULL,
|
||||
platform VARCHAR(20) NOT NULL DEFAULT 'WEB', -- WEB / ANDROID / IOS
|
||||
is_active CHAR(1) NOT NULL DEFAULT 'Y',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP,
|
||||
UNIQUE (token),
|
||||
CHECK (platform IN ('WEB','ANDROID','IOS')),
|
||||
CHECK (is_active IN ('Y','N'))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE user_device_token IS '푸시 알림용 디바이스 토큰 — 사용자별 FCM 등록 토큰';
|
||||
COMMENT ON COLUMN user_device_token.user_id IS '소유 사용자 FK';
|
||||
COMMENT ON COLUMN user_device_token.token IS 'FCM 등록 토큰(또는 웹푸시 endpoint)';
|
||||
COMMENT ON COLUMN user_device_token.platform IS '플랫폼 (WEB/ANDROID/IOS)';
|
||||
COMMENT ON COLUMN user_device_token.is_active IS '활성 여부 — 만료/로그아웃 시 N';
|
||||
|
||||
CREATE INDEX idx_udt_user ON user_device_token(user_id, is_active);
|
||||
Reference in New Issue
Block a user