Skip to content

[fix/MAT-920] 앱 내 반영#359

Merged
sterdsterd merged 13 commits into
developfrom
fix/mat-920-fcm-badge
May 27, 2026
Merged

[fix/MAT-920] 앱 내 반영#359
sterdsterd merged 13 commits into
developfrom
fix/mat-920-fcm-badge

Conversation

@sterdsterd
Copy link
Copy Markdown
Collaborator

@sterdsterd sterdsterd commented May 27, 2026

Summary

앱 내 알림 목록/count/OS 앱 아이콘 뱃지가 모두 서버의 최근 7일 unread count를 기준으로 동작하도록 정리했습니다.
푸시 알림 탭 시 payload의 notificationId로 해당 알림을 읽음 처리하고, 딥링크 이동은 read API에 의해 지연되지 않도록 분리했습니다.
React Query count cache를 OS 뱃지의 단일 source-of-truth로 사용하도록 bridge를 추가했습니다.

Linear

Changes

  • 알림 목록/count API가 클라이언트 dayLimit 지정 없이 서버 기본 7일 윈도우를 사용하도록 정리하고 관련 OpenAPI 타입을 갱신했습니다.
  • /api/student/notification/count React Query cache 업데이트를 OS 앱 아이콘 뱃지로 반영하는 badge bridge를 추가했습니다.
  • 앱 시작/foreground 복귀/foreground FCM 수신 시 count를 refetch해 OS 뱃지를 동기화하고, 로그아웃/탈퇴 cleanup 시 OS 뱃지를 0으로 초기화합니다.
  • 푸시 알림 탭 시 인증 준비 후 notificationId 기반 개별 읽음 처리를 수행하고, 딥링크 navigation은 read 처리와 분리해 즉시 진행되도록 했습니다.
  • 알림센터 단건/전체 읽음 처리에 optimistic cache update와 rollback을 적용해 인앱 뱃지와 OS 뱃지가 cache 변경을 따라가도록 했습니다.
  • notification queryKey, read API helper, count refetch helper를 공용화하고 count refetch 실패가 unhandled rejection으로 새지 않도록 처리했습니다.

Testing

  • pnpm --filter native typecheck
  • 코드리뷰 기준으로 앱 시작, 앱 복귀, foreground FCM 수신, 알림 탭 딥링크, 알림센터 단건 읽음/전체 읽음 경로를 점검했습니다.
  • 실제 APNs/FCM 디바이스 수신 및 런처별 Android badge 반영은 로컬에서 수동 검증하지 못했습니다.

Risk / Impact

  • 영향 범위: 학생 앱 알림센터, MainTabBar 알림 뱃지, iOS/Android OS 앱 아이콘 뱃지, 푸시 알림 탭 딥링크 처리
  • 확인이 필요한 부분: 서버가 푸시 payload에 notificationId를 내려주는지, /api/student/notification/count와 APNs badge count가 동일한 최근 7일 unread 기준인지, Android 런처별 badge 반영이 기대 범위인지
  • 배포 시 유의사항: 백엔드의 payload notificationId 추가 및 count/badge 계산 변경이 함께 배포되어야 푸시 탭 읽음 처리와 OS 뱃지 동기화가 완전하게 동작합니다.

Screenshots / Video

  • UI 시각 변경 없음

sterdsterd added 13 commits May 27, 2026 10:43
…deeplink nav

handleNotificationPayload 가 markNotificationAsRead 를 await 한 뒤 deeplink 로
이동하던 구조에서, 콜드스타트 + 미인증 상태에 푸시 탭이 들어오면 다음 문제가 있었다.

- markNotificationAsRead 가 authMiddleware 를 거치는 client.POST 로 나가면서
  reissue 실패 -> signOut() 사이드이펙트가 트리거될 수 있었다.
- read POST + 후속 syncNotificationBadgeCount 가 navigation 을 블로킹해
  PublishScreen 진입까지 한 박자 더 걸렸다.

waitForRouteRegistered('StudentApp') 게이트를 handleNotificationPayload
진입부에 둬서 학생 세션 hydration 이 완료된 뒤에만 read 처리/딥링크가
진행되도록 하고, markNotificationAsRead 는 fire-and-forget(void) 으로
흘려보내 navigation 즉시 시작.
…ubscriber

기존에는 React Query 캐시와 OS 앱 아이콘 뱃지가 각자 다른 fetch 경로를 통해
갱신되어 (NotificationsScreen / useFcmToken / useDeepLinkHandler / useNotificationBadgeSync
가 각각 syncNotificationBadgeCount 를 직접 호출) 같은 사용자 액션에 같은
/notification/count 가 두 번 fetch 되거나, 잠시 두 값이 다른 round trip 결과를
들고 갈 위험이 있었다.

useNotificationBadgeBridge 를 도입해 /api/student/notification/count 캐시 update
이벤트만 구독하고, 그 결과를 OS 뱃지로 push 한다. 호출자들은 이제 invalidate/
setQueryData/refetch 만 신경 쓰면 되고, OS 뱃지는 캐시를 따라간다.

- useNotificationBadgeBridge: 새 훅, App.tsx 부트 1회 마운트
- useFcmToken.onMessage: syncNotificationBadgeCount -> invalidate count 캐시
- useDeepLinkHandler.markNotificationAsRead: invalidate 만 유지, sync 호출 제거
- useNotificationBadgeSync: foreground 복귀시 invalidate (refetchType: 'all')
- NotificationsScreen: read/readAll onSuccess 의 직접 sync 호출과
  handleReadAll 의 직접 setBadge/rollback 제거 (optimistic cache patch 만으로
  subscriber 가 OS 뱃지 동기화)
- notificationBadge.ts: syncNotificationBadgeCount 제거(dead code),
  setNotificationBadgeCount/clearNotificationBadgeCount 반환 타입 Promise<void> 로
  단순화 (sentinel false/boolean 충돌 제거)
readNotification onSuccess 가 invalidate 만 하던 구조라 사용자가 단건 탭 후
서버 refetch 가 돌 때까지 hasBadge 가 잠시 남았다. handleReadAll 과 동일하게
count cache -1, 리스트 isRead=true 를 즉시 패치하고 onError 에서 rollback.

OS 앱 아이콘 뱃지는 useNotificationBadgeBridge 가 count cache update 를
구독하므로 별도 처리 불필요.
서버 사이드(StudentNotificationController + NotificationFacade DEFAULT_DAY_LIMIT=7,
MAT-907) 가 list / count / APNs badge 셋을 동일 7일 윈도우로 계산한다는 것을
JSDoc 로 남긴다. 클라이언트에서 dayLimit 을 임의로 바꾸면 alarms list,
in-app tab bell, OS app icon badge 의 source 가 어긋날 수 있다.
usePostReadNotification 와 useDeepLinkHandler.markNotificationAsRead 가 각자
같은 SyntaxError swallow 워크어라운드를 들고 있어, 백엔드가 빈 body 응답을
고치면 두 군데를 같이 손봐야 했다.

postReadNotification 헬퍼로 단일화해 client.POST + SyntaxError 처리 한 곳에서
관리한다. mutation hook 은 그 헬퍼를 mutationFn 으로 받아 단순화.
여러 위치(useDeepLinkHandler / useFcmToken / useNotificationBadgeSync /
useNotificationBadgeBridge / NotificationsScreen / useIncalidateNotificationData)
에서 /api/student/notification 과 /api/student/notification/count 의 queryKey 를
각자 raw array 또는 TanstackQueryClient.queryOptions(...).queryKey 로 만들고
있었다.

queryKeys.ts 한 곳에서 path 상수와 queryKey 를 export 해 모든 호출처가 같은
source-of-truth 를 쓰도록 통일. openapi-react-query 내부 표현이 바뀌더라도
한 곳만 손보면 된다.
…cationCount

openapi-react-query useQuery 시그니처는 (method, path, init?, options?) 인데
3번째 자리에 { enabled } 가 들어가 React Query options 가 아닌 init 으로
취급되고 있었다. 결과:

- enabled: false 를 넘겨도 query 가 비활성화되지 않음
- queryKey 가 ['get', path, { enabled }] 로 오염되어 queryKeys.ts 의
  notificationCountQueryKey (['get', path, {}]) 와 형식이 어긋남

호출부가 전부 enabled 기본값 true 라 즉시 장애는 없었지만, count queryKey 를
source-of-truth 로 쓰는 이번 작업 컨텍스트에서 source 일치를 보장하도록
init 자리에 {} 를 두고 options 를 4번째 자리로 이동.
@linear
Copy link
Copy Markdown

linear Bot commented May 27, 2026

MAT-920

@claude
Copy link
Copy Markdown

claude Bot commented May 27, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@sterdsterd sterdsterd self-assigned this May 27, 2026
@sterdsterd sterdsterd added the 🐞 Fix 버그 수정 label May 27, 2026
@sterdsterd sterdsterd merged commit 7a3b549 into develop May 27, 2026
2 checks passed
@sterdsterd sterdsterd deleted the fix/mat-920-fcm-badge branch May 27, 2026 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 Fix 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant