Skip to content

feat(platform): App Links URI 계약·인텐트 수신·인증 후 목적지 재개 #924

Closed as not planned
1 of 1 issue completed
Closed as not planned

Description

@Sadturtleman

2026-09-08 범위 분리·착수 조건

수신자 경로의 외부 식별자·인증 관문 선행 계약은 #1951로 분리했다. 이 이슈는 Android intent 수신·인증 후 재개 구현을 계속 소유한다. 미확정 수신자 경로·관문 구현 계획은 제거하고 #1951에서 결정 근거만 추적한다. 이 이슈는 확정된 비수신자 경로의 수신·재개만 소유한다. 공통 파서 #1857의 비수신자 부분까지 카카오 서버 준비로 묶지는 않는다.


작업 유형

enhancement — 신규 기능·기존 기능 개선

주 담당 모듈

platform — Android 앱·CI·빌드·릴리스·저장소 운영

개요

목적

검증된 App Link가 앱의 정확한 목적지로 진입하고, 로그인·지문 게이트가 필요하면 원래 목적지를 보관했다가 인증 후 재개하도록 만든다.

플랫폼 소관 로컬 그래프 이관은 #1698, 최종 앱 루트 교체는 #1702로 분리했다. 이 이슈는 Nav2/Nav3 타입에 결합하지 않는 URI → 엔진 중립 목적지 계약, Android intent 수신, 인증 후 재개만 소유한다.

현재 확인된 상태

  • 앱링크 도메인은 afternote.kro.kr로 확정되어 있고 assetlinks.json 내용 수정은 Afternote/Afternote-BE#228에서 완료됐다.
  • 앱 MainActivity에는 아직 앱링크용 VIEW/BROWSABLE intent-filter가 없다.
  • 알림 PendingIntent·warm 재진입·FCM 목적지 배선은 #1111이 별도로 소유한다.
  • 로컬 그래프 이관·최종 루트 전환·회귀 기준은 각각 #1698, #1702, #1601에서 추적한다.

범위

1. 확정된 공통 URI 계약 재사용

  • feat(core): App Link URI 계약과 엔진 중립 목적지 파서를 세운다 #1857 / PR #1859의 기존 엔진 중립 파서·비수신자 목적지·입력 검증을 재사용한다. 새 sealed 목적지 타입이나 URI 후보 표를 중복 설계하지 않는다.
  • 현재 확정된 비수신자 목적지만 Android 수신과 인증 후 재개에 연결한다.
  • 수신자 식별자·관문은 #1951에서 결정 근거만 추적한다. FCM의 미확정 data 키·link 필드를 여기서 가정하지 않는다.

2. Android intent 수신

  • MainActivity에 필요한 VIEW/BROWSABLE/https intent-filter를 추가하고 android:autoVerify를 사용한다.
  • cold start의 최초 intent와 onNewIntent warm 재진입을 동일한 입력 계약으로 정규화한다.
  • parser와 실제 destination consumer가 준비되기 전에는 브라우저 링크를 가로채 홈으로 보내는 반쪽 활성화를 머지하지 않는다.

3. 인증 게이트 후 재개

  • NavigationTarget으로부터 필요한 로그인·지문 게이트를 판정한다.
  • 통과하지 못한 첫 게이트에서 목표를 보관하고, 성공 후 원래 목표를 정확히 한 번 재개한다.
  • pending target은 프로세스 사망과 Activity 재생성을 견디며, 로그아웃·취소·만료 때 안전하게 폐기된다.
  • 최종 back stack 구성은 #1702의 루트 navigator adapter가 소유하고 이 이슈는 특정 navigation engine 자료형을 노출하지 않는다.

완료 조건

  1. 기존 파서가 지원하는 비수신자 목적지에 대해 URI parsing·ID validation·gate 판정 단위 테스트가 있다.
  2. cold start와 warm onNewIntent에서 같은 목적지로 이동한다.
  3. 로그인·지문 각각에서 게이트 통과 후 원래 목적지를 한 번만 재개한다.
  4. 프로세스 재생성 후 pending target이 복원되고, 취소·로그아웃·잘못된 링크에서는 폐기된다.
  5. Digital Asset Links 검증과 실제 https 링크 진입을 adb 또는 실기기에서 확인한다.
  6. App Link parser가 Nav2/Navigation 3 구현 타입을 import하지 않는다.
  7. 관련 유닛·Robolectric·androidTest와 전체 CI가 통과한다.

범위 밖

자식 이슈

No response

참고

Activity

  1. 1hyok commented on Aug 23, 2026

    @1hyok
    Collaborator

    본문 사실 검증 실측 (커밋 104462fd 기준 전수 대조)

    결과: 정정 1건 외 전부 일치.

    일치 확인 — AppNavigationActions.kt 577줄 / override fun 101개, 분류 46·27·11·17(물리적 한 줄 기준으로 정확히 재현), popUpTo 11곳 전부 1~2줄 해설 주석 동반, 인증 게이트 3중 라인 참조(MainActivity#L45 · AfternoteNavGraph#L35 · ReceiverNavGraph#L88), FCM getLaunchIntentForPackage() L78–93 · 목적지 없음, navDeepLink 0건(VIEW 필터는 카카오 SDK 별도 액티비티뿐), nested navigation<>() 6개, 그래프 스코프 공유 VM 7종(AfternoteHost · DeliveryVerificationFlow · SignUp · FindId · Setting · TimeLetterWrite · Timeletter — 에디터 VM은 자기 엔트리 스코프라 제외가 타당), MindRecord 파일럿 적합성(popUpTo 0 · 공유 VM 0), Manifest L64–68, toml L18.

    정정 1건 — hiltViewModel() 59곳 → 58곳 (무인자 49 + 부모/엔트리 인자 9. 주석 언급 3줄 중 하나를 세면 59가 나오는 집계 착오). 본문에 반영.

    확인 필요 항목 해소 — Navigation 3 은 1.0.0 stable 2025-11 출시, 현 stable 1.1.6 (2026-08-12). "alpha 면 3단계 보류" 조건 미발동. 본문 체크박스에 반영.

    참고: "나머지 17" 중 8개는 여러 줄로 포맷된 navigate 위임이라 순수 위임 비율은 실제 80%(81/101) — 본문 72% 주장은 보수적으로 성립.

  2. 1hyok commented on Aug 23, 2026

    @1hyok
    Collaborator

    1단계(mindrecord 파일럿)를 #959 로 분리

    이유: 마음의 기록은 모듈 소관이 분리돼 있어(0823 이관) 파일럿 대상 코드의 소유자와 이 이슈의 담당자가 다르다. #924 본체(앱링크 · 인증 게이트 3중 · FCM 목적지 · 전면 전환 설계)는 코어/내비게이션 소관으로 남기고, 파일럿 실행만 해당 모듈 소관으로 넘긴다.

    인계한 것 — 착수 전 전환 가능성을 확인하려고 프로토타입을 만들어 에뮬레이터로 돌려봤고, 그 결과를 #959 본문에 정리해 뒀다.

    • 실측 통과 시나리오 9개 (경계 back 전파 = 이 이슈가 지목한 최대 리스크 항목 포함)
    • 구현 함정 3가지: NavDisplay.onBack 시그니처, entryDecorators 기본값 대체, DiaryWriteViewModel 의 SavedStateHandle → assisted 주입 전환
    • 의존성 좌표 · 바텀바 판정 호이스팅 방식 · 충돌 지형([fix] #627 홈 TODAY'S QUESTION 답변 CTA 를 작성 화면에 연결한다 #894 교차)

    미검증 1건: predictive back 미리보기 애니메이션 — 실기기 확인 필요.

    2단계(그래프 스코프 VM 7곳 이관 설계)는 #959 결과를 재료로 이 이슈에서 계속 판단한다.

  3. 1hyok commented on Aug 23, 2026

    @1hyok
    Collaborator

    0단계 — URI 스키마 설계안 (FE 안, BE 협의 대기 항목 명시)

    1단계가 #959 로 분리돼 이 이슈에 남은 선행 작업이 0단계다. 착수하며 FCM 현황을 BE 까지 실측했고, 본문 전제 하나가 실제와 달랐다.

    실측 정정 — "FCM 이 반쪽으로 굴러간다" 가 아니라 "아직 안 굴러간다"

    항목 본문 서술 실측 (Afternote-BE HEAD, 381개 java 전수)
    FE 목적지 처리 없음 (getLaunchIntentForPackage) ✅ 그대로
    BE 발송 지점 (언급 없음) AdminPushTestService 1곳뿐 — Map.of("type","TEST"), 관리자 테스트용
    전달 알림 — 이메일로만 나감 (DeliveryNotificationService → JavaMailSender)
    data 통로 — 이미 열려 있음 — FcmPushSender.sendToUser(userId, title, body, Map<String,String> data)

    즉 사용자에게 도달하는 실서비스 푸시가 아직 없다. 반대로 data 맵 통로는 이미 있어서, 목적지를 싣는 데 BE 인프라 변경은 필요 없다 — 키 계약만 합의하면 된다. 이 이슈의 "첫 수혜자" 논거는 유지되지만, 순서는 발송 지점 신설(BE) → 목적지 파싱(FE) 이다.


    설계 1 — 앱링크와 FCM 이 같은 URI 파서를 공유한다

    FCM data 에 목적지를 타입·ID 쌍으로 싣지 않고 URI 문자열 하나로 싣는다.

    { "link": "https://<domain>/afternote/42", "type": "AFTERNOTE_DELIVERED" }
    • link — 앱링크로 열리는 것과 완전히 동일한 URI. FE 는 인텐트 출처(브라우저 VIEW / 알림 탭)와 무관하게 파서 하나만 유지한다.
    • type — 분기 아닌 분석·로깅용 선택 필드. 목적지 판단은 link 단독으로 끝낸다(두 필드가 갈라지면 진실이 둘이 된다).
    • link 부재·파싱 실패 시 현행처럼 홈으로 (하위호환).

    설계 2 — 경로 표

    도메인은 미확정(BE 협의). 경로는 화면 IA 기준.

    URI 경로 목적지 게이트 알림 후보
    /afternote 애프터노트 홈 로그인 + 지문 —
    /afternote/{id} 애프터노트 상세 로그인 + 지문 기록 전달 예정 안내
    /received 받은 기록함 로그인 열람 신청 상태 변경
    /received/senders/{senderId} 발신자 상세 로그인 발신자 등록 완료
    /received/afternote/{id} 수신 애프터노트 상세 로그인 + 수신자 본인인증 기록 도착 (현재 이메일)
    /timeletter/{id} 타임레터 상세 로그인 타임레터 도착
    /mindrecord/daily-question 데일리질문 작성 로그인 오늘의 질문
    /settings/notification 알림 설정 로그인 —

    게이트 열은 #924 본문의 인증 3중 구조를 그대로 반영한 것으로, Nav2 로는 이 표의 굵은 항목을 표현할 수단이 없다는 게 Nav3 논거의 핵심이다.

    설계 3 — 게이트 통과 후 재개 (Nav3 전제)

    // 1) URI → 목표 백스택
    val target = uri.toBackStack()        // 예: [Home, Receiver, AfternoteList, AfternoteDetail(42)]
    
    // 2) 통과 못 한 첫 게이트에서 멈추고 목표를 보관
    val gate = target.firstUnpassedGate() // 로그인 / 지문 / 수신자 본인인증
    backStack = if (gate == null) target else gate.entryStack().also { pendingTarget = target }
    
    // 3) 게이트 통과 시 재개
    fun onGatePassed() { backStack = pendingTarget ?: listOf(Home); pendingTarget = null }

    pendingTarget 은 프로세스 사망을 넘겨야 하므로 rememberSaveable/SavedStateHandle 보관 대상이다.

    설계 4 — 매퍼는 테스트가 필수다

    Nav2 navDeepLink<T> 의 타입 매칭이 사라지는 대가로, 라우트 이름이 바뀌어도 컴파일 에러 없이 런타임에 조용히 깨진다. 경로 표의 모든 행에 대해 uri → 백스택 왕복 단위 테스트를 매퍼와 같은 PR 에 넣는다.


    BE 협의 항목 (이 이슈의 실제 블로커)

    1. 도메인 확정 — assetlinks.json 을 어느 호스트 /.well-known/ 에 올릴지. 현행 API 호스트(afternote.kro.kr) 재사용 여부.
    2. data 키 계약 — 위 link / type.
    3. 실서비스 발송 지점 신설 — 최소 "기록 도착"(현재 이메일 전용)부터. 발송 시 data.link 동봉.

    3번이 없으면 FE 가 목적지 파싱을 붙여도 검증할 대상이 없다. BE 이슈로 분리해 blocked_by 로 건다.

    FE 선행 가능 (도메인 무관)

    • MainActivity VIEW/BROWSABLE intent-filter 골격 — 현재 LAUNCHER 하나뿐. 호스트만 나중에 채움. (카카오 OAuth 필터는 별도 액티비티라 무관)
    • URI ↔ NavKey 매퍼 + 테스트 — 경로 표가 확정이면 도메인 없이 path 부분만으로 작성 가능
  4. 1hyok commented on Aug 23, 2026

    @1hyok
    Collaborator

    0단계 블로커 실측 — assetlinks.json 은 이미 떠 있고, 대신 내용이 틀렸다

    본문의 "assetlinks.json 호스팅 → 백엔드·인프라 의존, 도메인 확정 필요" 를 실측했다. 호스팅·도메인은 이미 해결돼 있었다 — 패스키(Afternote/Afternote-BE#133)가 같은 파일을 필요로 해서 먼저 올라갔다.

    curl https://afternote.kro.kr/.well-known/assetlinks.json  →  HTTP 200
    

    즉 도메인 확정 대기는 해제다. afternote.kro.kr 을 그대로 쓰면 된다.

    다만 내용에 두 가지 문제가 있어 그대로는 앱링크가 동작하지 않는다:

    1. package_name 이 com.afternote.app — 실제는 com.afternote.afternote_fe (applicationId·adb pm list 2중 확인). 이 상태로는 패스키(패스키 로그인 — Credential Manager 통합 인증 연동 #764)도 검증이 깨진다
    2. relation 이 get_login_creds 뿐 — 앱링크에 필요한 handle_all_urls 없음

    → Afternote/Afternote-BE#228 로 분리해 걸었다. 이 이슈의 0단계는 그 결과에 blocked_by.

    갱신된 0단계 상태

    항목 이전 실측 후
    도메인 확정 대기 ✅ afternote.kro.kr 확정
    assetlinks.json 호스팅 대기 ✅ 완료 (내용 수정만 남음 → BE#228)
    URI 스키마 설계 미착수 ✅ 위 코멘트에 설계안
    FCM data 키 계약 — 설계안의 link — BE 인프라 변경 불필요(sendToUser 가 이미 Map 수신)
    BE 실서비스 발송 지점 — ❌ 부재 (admin 테스트 1곳뿐) — 별건

    FE 선행 가능해진 것

    도메인이 확정됐으므로 MainActivity VIEW/BROWSABLE intent-filter 와 URI↔NavKey 매퍼(+테스트)를 BE#228 과 병렬로 작성할 수 있다. 다만 매퍼가 만드는 백스택을 실제로 태우려면 Nav3 전환(2·3단계)이 선행돼야 하므로, 매퍼 단독 머지는 죽은 코드가 된다 — BE#228 착지 + #959 결과를 보고 순서를 정한다.

  5. 1hyok commented on Aug 27, 2026

    @1hyok
    Collaborator

    2026-08-28 최신 공식 가이드 기준 결정 보강

    결론

    • 그린필드 Compose-only 앱이라면 Navigation 3를 기본 선택지로 둔다. 현재 stable은 1.1.7이고, 백스택을 앱 상태로 직접 모델링하는 이점이 마이그레이션 비용 없이 바로 적용된다.
    • 현재 Afternote는 Nav3가 열등해서 보류하는 것이 아니다. 이미 구축한 Nav2 타입 안전 route·popUpTo 정책·그래프 스코프 ViewModel·TestNavHostController 테스트의 이관 비용과, 아래 공식 가이드 미지원 범위가 겹치기 때문이다.
    • 따라서 기존 순서인 마음의 기록 허브를 Navigation 3 로컬 스택으로 — #1697 파일럿 #959 파일럿 → 그래프 스코프 VM 이관 설계 → 전면 전환 판단을 유지한다.

    새 공식 마이그레이션 가이드와 현재 코드 대조

    공식 가이드: https://developer.android.com/guide/navigation/navigation-3/migration-guide

    • 요구 조건 compileSdk >= 36, minSdk >= 23은 현재 프로젝트(37 / 26)가 충족한다.
    • 가이드는 Navigation 2와 3을 함께 옮기는 점진 전환이 아니라 단일 atomic 전환을 가정한다. #959의 로컬 스택 공존은 전면 마이그레이션 절차가 아니라 위험을 재는 파일럿으로 취급한다.
    • 가이드와 recipes가 아직 다루지 않는 항목에 한 단계를 넘는 nested navigation, shared destination, custom destination, deep link가 명시됐다.
    • 현재 코드는 Root → Route.Afternote → EditorFlow, Root → Route.Receiver → DeliveryVerificationFlow 구조와 flow-scoped ViewModel을 사용하므로 공식 절차를 기계적으로 적용할 수 없다.

    전환 전 회귀 기준으로 추가할 테스트

    • 바텀 탭 이동 후 각 스택·화면 상태 저장/복원
    • 로그인 완료, 로그아웃, 탈퇴 성공 시 인증 전후 전체 스택 경계
    • Editor/DeliveryVerification 등 flow-scoped ViewModel의 자식 화면 간 유지 및 그래프 이탈 시 clear
    • 프로세스 재생성 후 route와 pendingTarget 복원
    • predictive back 진행·취소·완료

    버전 참고:

  6. 1hyok commented on Aug 27, 2026

    @1hyok
    Collaborator

    블로커와 무관한 단순 back() 위임 통합은 #1301로 분리했습니다. #924에서는 전환 판단과 의존 관계만 유지하고, 독립 구현·검증은 #1301에서 추적합니다.

  7. 1hyok commented on Aug 27, 2026

    @1hyok
    Collaborator

    단순 back 위임 작업 소유권 분리

    이전 코멘트에서 #1301 하나로 연결한 범위를 실제 담당자 기준으로 바로잡았습니다.

    세 기능을 함께 구현했던 #1308은 독립적인 1hyok 범위가 남지 않아 미병합으로 닫고, 원격 브랜치와 로컬 worktree·브랜치도 삭제했습니다. 해당 diff는 재사용하지 않으며 #924에서는 #1301과 #1311을 독립 작업으로 추적합니다.

  8. 16 remaining items

  9. 1hyok commented on Sep 13, 2026

    @1hyok
    Collaborator

    이 PR 이 닫는 몫과 떼어낸 몫

    브랜치 feat/924-app-links-receive 의 커밋 c896a857 이 이 이슈의 코드 범위를 끝낸다. PR 은 Closes #924 로 연다. 대표 이슈는 그 형태로만 걸 수 있으므로(#1748), 이 PR 이 닫지 못하는 몫은 산문에 남기지 않고 이슈로 뗐다.

    이 PR 이 닫는 것

    완료 조건 닫는 근거
    1. URI parsing·ID validation·gate 판정 단위 테스트 AfternoteAppLinkParserTest 16건, NavigationTargetContractTest 7건
    2. cold start 와 warm onNewIntent 가 같은 목적지 두 경계를 AppLinkIntentContract 하나로 합치고 AppLinkIntentContractTest 4건
    3. 관문 통과 후 원래 목적지를 한 번만 재개 MainViewModelAppLinkTest 8건, AppLinkResumeEffectTest 3건
    4. 프로세스 재생성 복원, 로그아웃·잘못된 링크에서 폐기 MainViewModelAppLinkTest 의 SavedState·로그아웃 케이스
    6. 파서가 Nav2·Nav3 구현 타입을 import 하지 않음 core:common 의 deeplink 패키지 import 는 java.net.URI 하나다
    7. 유닛·계측·CI 통과 PR 의 CI 가 판정한다. 로컬에서 태스크 7개를 먼저 돌렸다

    목적지는 다섯이다. /, /afternote, /timeletter/{id}, /mindrecord/daily-question, /settings/notification. 관문 판정은 하드코딩하지 않고 목적지가 싣고 온 requiredGates 를 읽는다. 로그인은 앱 셸이, 지문은 목적지 그래프 자신이 지킨다.

    떼어낸 것

    둘 다 이 이슈를 blocked_by 로 걸어 뒀다. 후속 분리분이라 자식으로 매달지는 않았다.

    두 몫을 한 이슈로 묶지 않은 이유는 걸리는 지점이 다르기 때문이다. #2087 의 선행은 서명 지문과 기기이고, #2088 의 선행은 열린 PR 세 건이다. 묶으면 둘 중 하나가 다른 하나를 기다리게 된다.

  10. added
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 13, 2026
  11. 1hyok commented on Sep 26, 2026

    @1hyok
    Collaborator

    보류로 닫는다. 무엇을 만들지가 서버와 PM 결정에 달려 있다.

    구현 PR #2093 은 머지하지 않고 닫았다(브랜치 feat/924-app-links-receive 보존). 결정이 나면 그 계약으로 남은 범위만 새 이슈로 연다. 그때는 파서를 직접 짜지 않고 루트 엔진의 공식 딥링크 API(Nav2 navDeepLink 또는 Navigation 3 1.2.0 DeepLinkMatcher)로 받는다. 이 이슈를 기다리던 #2087, #2088 도 같은 이유로 닫는다.

  12. removed
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:platformCI·빌드·릴리스·저장소 운영 영역enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions