앱 푸시 알림 체계
Capacitor 앱(novato_react)의 FCM 푸시 알림 + 알림함 시스템. 2026-07-25 정식 개통.
한눈에 보기 — 알림 종류
| 알림 | 시점 (KST) | 내용 | 딥링크 |
|---|---|---|---|
| ☕ 커피타임 | 허브 등록 즉시 (트리거) | 별명(실명 금지, 없으면 “팀원”) + 랜덤 농담 10종 | /lounge |
| 📅 주간 일정 | 매주 월 09:00 | 이번 주(월~일) 전체 경기 목록 | /schedule |
| 🏀 오늘 경기 | 경기 당일 09:00 | 그날 경기 목록 | /schedule |
| 🏀 개인 경기 예고 | 경기 전날 22:00 | ”OO님, 내일 7/29(수) 18:30 3M vs 야호…” + 우리 팀 유니폼 색 — 본인 팀 경기가 있는 사람만 | /schedule |
| 💬 댓글 멘션 | 즉시 (트리거) | 경기 댓글 “@선수이름” → 그 선수의 연결 계정에만 (작성자 제외) | /records |
| 🙋 가입 요청 | 신청 즉시 (트리거) | super_admin 에게만 — 탭 시 브라우저로 허브 회원관리 | https://hub.novato.kr/members |
| 📊 경기 결과 | 결과 등록 즉시 (트리거, 점수 첫 기록 시 1회) | 해당 경기 결과 + 팀 순위 전체 | /ranking |
| 🤖 AI 프리뷰/리뷰 | 생성 즉시 (트리거) | 매치업 + 헤드라인 | /predict/<gameId>, /records |
| (임시) 테스트 | 9~17시 2시간 간격 | ”별명님, N시 알림 테스트입니다” | /dashboard |
수신 대상 공통 규칙: 승인(approved) 회원 + player 연결 + 활성 푸시 토큰 (push_audience() RPC).
player 미연결/미승인/익명 토큰은 어떤 알림도 받지 않는다.
알림함 (앱 상단 🔔)
- 최근 7일 수신 알림 최신순. 항목 탭 → 해당 알림의 route(딥링크)로 인앱 이동.
- 안 읽은 알림이 있으면 종 아이콘에 빨간 점. 알림함 열면 전체 읽음 처리.
- 데이터:
app_notifications(RLS: 본인 행만 조회/읽음). send-push 가 발송할 때 유저별로 자동 기록하므로 별도 처리 불필요. - 7일 지난 행은 크론
purge-app-notifications-7d(매일 KST 04:00)가 삭제. - 앱 코드:
src/pages/NotificationInboxPage.tsx,src/services/notificationService.ts
아키텍처
크론(pg_cron) / DB 트리거
│ net.http_post (service key = Vault 'service_role_key')
▼
notify-events (Edge Function) ← 문구 조합 전담 (농담/별명/일정/순위)
│ HTTP (target: user ids)
▼
send-push (Edge Function) ← FCM v1 발송 + 알림함(app_notifications) 기록
│ + 무효 토큰(404/403) 자동 비활성화
▼
FCM → 단말 (탭 시 data.route 로 인앱 라우팅 — src/native/pushNotifications.ts)
- notify-events
supabase/functions/notify-events/index.ts— kind:morning(주간/오늘/개인 예고 통합) ·results·coffee·ai.dry: true로 발송 없이 문구만 확인 가능. - send-push
supabase/functions/send-push/index.ts— target:all|user(uuid 배열) |token(테스트). 개인별 다른 내용 = 유저별 호출. - 배선 SQL(원격 적용 완료):
novato_react/supabase/notify_events_wiring.sql,app_notifications_inbox.sql,push_test_cron.sql
서버 오브젝트 (Supabase)
| 종류 | 이름 | 역할 |
|---|---|---|
| cron (jobid 7) | notify-morning-kst9 | UTC 0시 = KST 09:00 → kind morning |
| trigger | trg_notify_result on game_league | 점수 null→값 UPDATE → 결과+순위 즉시 |
| trigger | trg_notify_mention on game_comments | ’@’ 포함 INSERT → 멘션 알림 |
| cron (jobid 10) | notify-game-eve-kst22 | UTC 13시 = KST 22:00 → kind game-eve (전날 개인+유니폼) |
| cron (jobid 5) | purge-app-notifications-7d | 알림함 7일 정리 |
| cron (jobid 3) | push-test-2h-kst-9-18 | (임시) 개인화 테스트 알림 |
| trigger | trg_notify_coffee on coffee_times | INSERT → 커피 알림 |
| trigger | trg_notify_preview on game_previews | INSERT → AI 프리뷰 알림 |
| trigger | trg_notify_review on game_reviews | INSERT → AI 리뷰 알림 |
| RPC | push_audience() | 수신 대상 (service_role 전용) |
| table | app_notifications | 알림함 (7일 보관) |
| table | push_tokens | FCM 토큰 (무효 시 자동 비활성) |
운영 방법
- 알림 끄기:
SELECT cron.unschedule('<jobname>');/ 트리거는DROP TRIGGER <이름> ON <테이블>; - 문구 수정:
notify-events/index.ts수정 후supabase functions deploy notify-events(농담 목록COFFEE_JOKES포함) - 테스트(발송 없이): notify-events 에
{"kind":"morning","dry":true}등으로 POST (Authorization: service key) - 테스트 크론 제거:
SELECT cron.unschedule('push-test-2h-kst-9-18');
함정 노트 (겪은 것들)
- OTA·manifest 는 CapacitorHttp 필수 — R2 는 CORS 헤더가 없어 웹뷰 fetch 는 조용히 실패 → 아키텍처 및 index 변경 이력(2026-07-23) 참조
- Firebase 재등록(패키지명 변경) 시:
google-services.json은 네이티브 리소스라 APK 재설치 필수(OTA 불가). 옛 클라이언트 토큰은 FCM 404/403 → send-push 가 자동 비활성화. - capgo OTA 번들은 앱이 백그라운드로 갈 때 적용 — 강제 종료(force-stop)는 적용 단계를 건너뜀.