Skip to content

feat(app): 서버 알림을 수신·이동·알림함·설정까지 잇는다 (#2321) - #2322

Merged
Sadturtleman merged 3 commits into
developfrom
feat/2321-fcm-v1-contract
Oct 27, 2026
Merged

Sadturtleman merged 3 commits into
developfrom
feat/2321-fcm-v1-contract

Conversation

@Sadturtleman

@Sadturtleman Sadturtleman commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

📌𝘐𝘴𝘴𝘶𝘦𝘴

📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯

BE#317 의 비결제 알림(A~E)·FCM data-only v1 계약(config/firebase/NOTIFICATION_CONTRACT.md)을 앱이 받아 알림을 그리고, 탭·알림함에서 현재 목적지로 이동하고, 알림 설정을 고칠 수 있게 합니다. 결제(F)와 알림톡·설치 후 목적지 복원은 범위 밖입니다.

1. 기기 등록·권한

  • 설치 UUID(PushInstallationIdStore, DEVICE 수명·백업 제외)를 만들어 PUT users/push-tokens 에 installationId·notificationPermission 을 싣습니다.
  • 로그인 확정과 Activity 재개 때 areNotificationsEnabled() 가 바뀌었으면 PUT users/push-installations 로 보고하고 같은 식별자를 다시 등록합니다. 등록 중복 판정은 (식별자, 권한) 쌍입니다.
  • 서버가 등록 응답에 requiresReset 을 실으면 프로세스당 한 번 FID 를 지우고 새로 받아 등록합니다(FCM 대기는 뮤텍스 밖).

2. 수신

  • data 의 schemaVersion·destination·ID·action·notificationId 를 읽습니다. 모르는 schemaVersion 이면 홈으로 엽니다.
  • 서버 알림 ID 를 발생 식별자로 써 같은 알림은 트레이에 한 장만 남습니다. 탭 PendingIntent 에 SERVER_ID 와 목적지 키만 거른 SERVER_DATA 를 더했습니다(기존 extra 키 유지).

3. 탭·알림함 → 목적지

  • GET users/notifications/{id}/target 으로 현재 목적지를 묻고 PATCH …/read 로 읽음 처리합니다. 실패·3초 초과면 실어 온 data 로, 그것도 없으면 탭으로 엽니다. 지워진 대상은 목록 + 서버 문구 스낵바입니다.
  • ServerNotificationDestination 이 서버 whitelist 전부를 앱 화면으로 옮깁니다.
서버 목적지 앱
RECORD_LIST · WEEKLY_REPORT · EASY_QUESTIONS 마음의 기록 탭
RECORD_DETAIL(DAILY_QUESTION) / DAILY_QUESTION 기록 상세 / 오늘 질문 작성
TIME_LETTER_LIST / DETAIL(+EDIT 면 수정 화면을 위에) / COMPOSE 타임레터 탭 / 상세 / 수신인 미리 고른 작성
RECEIVER_LIST · INVITATION_DETAIL / RECEIVER_DETAIL / AGENT_SETTINGS 설정 수신인 목록 / 수신인 / 대리인
그 밖(보관 확인·웹 링크·결제·수신자 홈) 홈

일기·깊은 생각 상세는 payload 에 찾을 달이 없어 기록 탭으로 엽니다.

  • 홈 탑바에 알림함(Route.NotificationInbox)과 미읽음 점을 두었습니다. 알림함 탭도 푸시 탭과 같은 진입 큐·목적지 경로를 탑니다.
  • Route.Setting 의 시작점을 SettingStart 로 일반화했습니다(기존 수신인 등록 진입 포함).

4. 앱 진입·안내

  • 로그인이 확정될 때마다 POST …/notification-preferences/entry 를 보냅니다. 쉬운 질문이 오면 시트로 띄우고, 고르면 그 질문을 제목으로 일기를 엽니다. 보관 절차가 취소됐으면 스낵바로 알립니다.
  • 기기 알림이 꺼져 있으면 홈에 「알림 켜기」 배너를 띄웁니다.

5. 설정 > 알림

  • 기록 알림(시각·요일·주간 목표·ON/OFF), 대리인(지정·변경·해제), 수신인 기념일(등록·삭제) 화면을 MVI 로 더했습니다.

6. 로컬 데일리 알림 제거

서버 B 와 이중 발송되고 하루 1건 상한·야간 이월을 깨므로 DailyNotificationWorker·스케줄러·관찰자·Konsist 가드를 지우고, LegacyDailyNotification.clear() 가 남은 예약과 채널을 지웁니다.

검증

  • ktlintCheck, :konsist:test, :app :core:data :core:common :core:network :core:ui :feature:setting:{data,presentation} :feature:home:presentation :feature:timeletter:presentation :feature:mindrecord:presentation 단위 테스트, :app:compileDebugAndroidTestKotlin, 5개 모듈 compileDebugScreenshotTestKotlin, :app :core:ui :feature:{setting,home}:presentation lintDebug 로컬 통과
  • 새 테스트: 목적지 해석(전 whitelist), 권한 보고·재등록·무효 재발급, /target 해석·fallback·시간 초과, 진입 보고, FCM v1 payload·Intent 왕복, 알림함·배지, 설정 3화면 ViewModel, LocalTime 와이어 변환, API wire 계약
  • 실기기 FCM 수신은 확인하지 못했습니다. ADMIN 테스트 발송 계정이 없어 서버 → 기기 도달(특히 FID 를 token 으로 쓰는 조합)은 후속 확인이 필요합니다.

🧪 CI Test Plan

{
  "androidTest": {
    "mode": "selected",
    "reason": "알림 진입 큐가 목적지 조회를 거치도록 바뀌고 Route.Setting 시작점이 SettingStart 로 일반화됐다. 기존 cold·background 알림 진입과 설정 host 시작점·수신인 수정 흐름을 API 34 에서 확인한다.",
    "tests": [
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/NotificationNavigationAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.NotificationNavigationAndroidTest#warmNotificationWhileLoggedOut_isReceivedWithoutBypassingOnboarding",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/NotificationNavigationAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.NotificationNavigationAndroidTest#backgroundNotification_resumesSameActivityAndNavigatesToDestination",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/NotificationColdStartAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.NotificationColdStartAndroidTest#coldPendingIntentLaunchWhileLoggedIn_opensDestination",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#settingNavHost_startWithRecipientRegistrationOpensRegisterFirstAndBackExitsHost",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#appNavigation_settingRouteArgumentSelectsRegisterAsFirstScreen",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#settingNavHost_passwordChangeEntryAndBackKeepSettingsHost",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#settingNavHost_receiverManageListRowNavigatesToEditWithExactReceiverId",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#settingNavHost_deliveryConditionReceiverSelectionPreservesExactReceiverId",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingImplementedCoverageAndroidTest#settingNavHost_withdrawGuideCancelThenAgreementConfirmPreservesBoundary",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingEditorSaveSignalAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingEditorSaveSignalAndroidTest#receiverEdit_saveSuccessPopsExactlyOneScreenToReceiverList",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingEditorSaveSignalAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingEditorSaveSignalAndroidTest#receiverEdit_saveFailureShowsMessageAndStaysOnEdit",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingEditorSaveSignalAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingEditorSaveSignalAndroidTest#receiverEdit_invalidEmailOrPhoneBlocksSaveWithoutRequest",
        "device": "api34"
      },
      {
        "path": "app/src/androidTest/java/com/afternote/afternote_fe/SettingEditorSaveSignalAndroidTest.kt",
        "selector": "com.afternote.afternote_fe.SettingEditorSaveSignalAndroidTest#deliveryCondition_saveSuccessPopsExactlyOneScreenToReceiverSelection",
        "device": "api34"
      }
    ]
  }
}

📷𝘚𝘤𝘳𝘦𝘦𝘯𝘴𝘩𝘰𝘵

새 화면(알림함·쉬운 질문 시트·기록 알림·대리인·기념일·홈 배너)은 기존 디자인 토큰과 설정 컴포넌트로 구성했고 시안은 없습니다. 기존 Preview·스크린샷 대상의 기본 인자는 바꾸지 않았습니다.

💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴

  • 서버 목적지 → 앱 화면 매핑은 ServerNotificationDestination 한 곳입니다. 상세 화면 진입점이 늘면 여기만 고치면 됩니다.
  • 대리인·기념일 화면은 시안 없이 만든 1차 구성입니다.

🤖 Generated with Claude Code

- 설치 UUID 를 만들어 push-tokens 등록에 installationId·notificationPermission 을 싣고,
  로그인·포그라운드 복귀 때 push-installations 로 OS 알림 권한을 보고한다.
- data-only v1 payload(schemaVersion·destination·notificationId)를 읽어 알림을 그리고,
  서버 알림 ID 를 발생 식별자와 탭 Intent 에 싣는다.
- 탭하면 notifications/{id}/target 으로 현재 목적지를 확인하고 읽음 처리한다. 지워진 대상은
  목록과 안내를 띄우고, 조회 실패·시간 초과면 payload 목적지로 연다.
- 서버 B 알림과 겹치는 로컬 09:00 데일리 알림을 제거하고 남은 예약·채널을 지운다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Sadturtleman Sadturtleman added enhancement New feature or request issue-assignee-exempt 담당자 대조 가드 우회 — 대표 Issue 담당자가 아니어도 이 PR 은 통과한다 area:app 앱 셸·루트 네비게이션·앱 모듈 공통 영역 labels Oct 27, 2026
@github-actions github-actions Bot added the android-test PR head에서 Android Managed Device 계측 테스트 실행 label Oct 27, 2026
@Sadturtleman
Sadturtleman disabled auto-merge October 27, 2026 07:39
- 탭한 알림의 서버 data(destination·ID·action)를 끝까지 실어 마음의 기록 상세·오늘 질문,
  타임레터 상세·수정·수신인 지정 작성, 설정의 수신인·대리인 화면까지 연다. /target 조회가
  실패하면 실어 온 data 로, 그것도 없으면 탭으로 연다.
- 로그인마다 앱 진입을 보고하고 쉬운 질문 시트와 보관 절차 취소 안내를 띄운다.
- 홈 탑바에 알림함과 미읽음 점, 기기 알림이 꺼졌을 때 켜는 배너를 둔다.
- 설정 > 알림에 기록 알림 시각·요일·주간 목표, 대리인, 수신인 기념일 화면을 더한다.
- 서버가 토큰을 무효(requiresReset)라 하면 프로세스당 한 번 새 FID 를 받아 다시 등록한다.
- 알림 API 를 NotificationApiService 로 모으고 Route.Setting 시작점을 SettingStart 로 일반화한다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다 label Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

열린 PR 중 이 PR 과 같은 파일을 서로 다르게 고친 것이 있다. 어느 쪽이 먼저 머지되든 나중 쪽은 base 를 병합해야 통과한다 (#1750).

머지 자체는 GitHub 이 막는다 — 한쪽이 들어가면 다른 쪽은 CONFLICTING 이 되어 merge queue 에 못 들어간다.
이 라벨은 그 전에 순서를 정하라는 신호다. 먼저 갈 쪽을 정하고, 나중 쪽은 그 머지 뒤에 base 를 병합한다.
상대 PR 에도 같은 안내가 달린다.

이 코멘트는 리컨사일러가 매 실행 현재 상태로 고쳐 쓴다. 해소되면 sibling-conflict 라벨은 자동으로 떨어진다.

@Sadturtleman Sadturtleman changed the title feat(app): 서버 FCM v1 알림 계약을 수신·표시·이동까지 잇는다 (#2321) feat(app): 서버 알림을 수신·이동·알림함·설정까지 잇는다 (#2321) Oct 27, 2026
@Sadturtleman
Sadturtleman added this pull request to the merge queue Oct 27, 2026
Merged via the queue into develop with commit af339e1 Oct 27, 2026
23 checks passed
@Sadturtleman
Sadturtleman deleted the feat/2321-fcm-v1-contract branch October 27, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android-test PR head에서 Android Managed Device 계측 테스트 실행 area:app 앱 셸·루트 네비게이션·앱 모듈 공통 영역 enhancement New feature or request issue-assignee-exempt 담당자 대조 가드 우회 — 대표 Issue 담당자가 아니어도 이 PR 은 통과한다 sibling-conflict 열린 형제 PR 과 같은 파일에서 충돌 — 나중에 머지되는 쪽이 base 를 병합해야 한다. 해소되면 자동으로 떨어진다

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(app): 서버 FCM v1 알림 계약을 수신·표시·이동까지 잇는다

1 participant