Repository navigation
release(platform): 누적 188 PR을 Firebase QA로 배포한다 (#2241) - #2242
Merged
Merged
Conversation
루트 Nav2 `NavHost` 가 `popEnterTransition`·`popExitTransition` 을 넘기지 않아 pop 기본값인 `fadeIn`/`fadeOut(tween(700))` 을 그대로 썼다. 앞 화면이 줄지 않고 제자리에서 흐려지기만 해, 제스처 진행 중 두 화면이 같은 크기로 겹쳐 그려졌다. 실기기(S25, Android 16)에서 재현된 증상이다. 값은 새로 정하지 않고 navigation3-ui 1.1.6 의 `defaultPredictivePopTransitionSpec()` 을 복제했다. 피처의 Nav3 로컬 스택은 `transitionSpec` 을 안 넘겨 이미 그 기본값으로 축소되므로, 같은 값을 core/ui 한 곳에 두고 두 엔진이 나눠 쓴다. 루트가 `NavDisplay` 로 바뀔 때(#1702) 같은 상수를 `predictivePopTransitionSpec` 에 그대로 넘길 수 있다. 부작용이 하나 딸린다. navigation-compose 2.9.8 은 `composeNavigator.isPop.value || inPredictiveBack` 으로 같은 popEnter/popExit 을 고른다(`NavHost.kt:573,587`). 제스처가 아닌 pop, 즉 화면 안 뒤로가기 버튼과 시스템 back 의 애니메이션도 fade 에서 축소로 함께 바뀐다. Nav3 는 `popTransitionSpec` 과 `predictivePopTransitionSpec` 이 갈려 있지만 Nav2 는 갈림이 없어 우회로가 없다. 모서리 라운딩은 붙이지 않았다. `ExitTransition` 은 clip 이나 shape 를 표현하지 못하고 Nav3 기본값도 `scaleOut` 뿐이라, `docs/qa/predictive-back.md` 의 "모서리가 둥글어지며" 기대 문구를 실제 기준에 맞춰 고쳤다. 회귀는 konsist 가드 하나로 잠갔다. 두 인자는 기본값이 있어 지워도 컴파일이 통과하고, 어긋난 축이 그리기 하나라 기존 `PredictiveBackNavigationTest` 5건이 전부 초록으로 지나간다. 인자 제거, 값 교체, app 안 재정의, core/ui 상수 이름 변경 네 가지 변이로 실제로 빨개지는 것을 확인했다.
App Distribution 배포가 전부 versionCode 1 이라 Crashlytics 에서 배포본을 구분할 수 없다(#1865). distribution.md 의 "App Distribution 이 versionCode 로 구분하지 않으므로 올릴 이유가 없다" 는 근거는 참이지만 결론이 틀렸다. 이 채널의 APK 가 크래시를 보내는 곳은 같은 Firebase 프로젝트의 Crashlytics 이고, 거기서는 배포본이 versionName (versionCode) 로 묶인다. release versionName 은 고정이라 가를 축이 versionCode 하나뿐이다. 두 채널이 각자 run_number 로 같은 식을 쓰면 값이 겹친다. release-distribution.yml (main push 자동)과 release-play-internal.yml(수동)의 run 카운터가 독립이기 때문이다. 겹치면 Crashlytics 가 두 채널 빌드를 한 버전으로 묶고, 겹친 값이 Play 원장에 들어가면 resolve-play-version-code.mjs 의 최댓값 비교 때문에 run_number 가 따라잡을 때까지 Play 배포가 빌드 전에 거부된다. 그래서 대역을 1,000,000,000 에서 가르고 Firebase 를 아래, Play 를 위에 둔다. 순서가 이쪽이어야 Firebase 산출물이 Play 원장에 들어가도 최댓값 비교를 막지 못한다. Play 원장이 비어 있을 때와 첫 수동 업로드가 1 일 때 모두 성립하고, Firebase 수열의 최솟값 101 과도 겹치지 않는다. 승격 자체는 따로 금지한다. 근거는 문서에 이미 있는 "Firebase 는 출시 뒤에도 내부 QA 전용" 과 2026-09-06 별도 설치 채널 결정(인증서 상이)이다. 착수 게이트였던 회귀 판정 축은 Firebase 문서로 확인한 만큼만 적었다. 재개봉은 "닫을 때 Crashlytics 가 알지 못하던 앱 버전" 에서의 재발만 본다는 것이 명시돼 있고, Android 에서 그 버전의 정체가 versionName·versionCode 중 무엇인지는 명시가 없다. 확정(1.0 (1) 고정인 동안 재개봉은 일어날 수 없다)과 미확정(versionCode 만 올려서 되살아나는지)을 갈라 적고, 실측 완료 조건이 남는 이유로 뒀다. 콘솔 토스트 문구는 근거로 쓰지 않았다. 구현(Firebase 번호 주입·Play 대역 하한·워크플로)은 별도 범위다. 선행으로 #1990 을 적었다. 지금 develop 은 versionCode != 기본값만으로 storeDistributedBuild 를 세우고 그 값이 ForceUpdateGate 에 걸려 있어, 먼저 주입하면 QA 빌드에 강제 업데이트 관문이 켜진다.
여섯 필드만 고르던 비교라 고르지 않은 필드로 두 자리가 갈려도 그대로 지나갔다. bodySmallB 한쪽에만 letterSpacing = (-0.0025).em 을 넣고 실측하니 옛 잠금은 4건이 전부 초록이었고, TextStyle 전체를 비교하도록 바꾸자 bodySmall 이 letterSpacing 차이로 실패했다. 두 쌍은 나머지 필드가 전부 기본값이라 비교 범위를 넓혀도 현재 값은 그대로 통과한다. 렌더 변화가 없으니 골든 재생성도 없다.
「이 파일의 스타일은 전부 그 아래」가 사실이 아니었다. 기본 설정(밀도 480 · 글꼴 1.0) 하한인 약 19sp 위에 h1 28sp · h2 24sp · h3 20sp 가 있고, 위 표를 잰 기기(밀도 420 · 글꼴 배율 0.8, 하한 약 28sp)에서도 h1 은 58.8px 로 하한을 넘긴다. 하한만으로 닫히는 것은 18sp 이하고 제목이 Regular 인 근거는 획 대비가 아니라 시안이라는 것을 적었다. 바로 다음 문단의 시안 실측이 그 근거다. #2061
주석이 "gray5 로도 WCAG AA 4.5:1 은 못 넘는다(2.57:1)" 라고 적고 있었다. 2.57:1 은 교체 전 #9E9E9E 의 값이다. 라이트 gray5 는 #1798 이 #6E6E6E 로 내린 뒤라 gray1(#FAFAFA) 위 대비가 4.885:1 이고 본문 최소 4.5:1 을 넘는다. 같은 문단의 "gray5(#9E9E9E)" 표기도 라이트에서는 맞지 않아 걷어냈다. 결정 주체도 "디자이너 결정이 필요하고" 로 남아 있었다. #1636 은 2026-09-12 본문 개정에서 PM 확인으로 바뀌었고 디자이너에게 따로 묻지 않는다고 못 박았다. 수치는 WCAG 2.x 상대 휘도 식으로 재계산했다. #6E6E6E 4.8851:1, #9E9E9E 2.5668:1, 종전 gray4 #BDBDBD 1.8000:1. 주석만 바뀐다. 색은 그대로 두었다. 비선택 탭 색과 선택 구분 방식은 #1636 의 PM 대기 사항이다. 렌더된 스크린샷 81장이 base 와 바이트 동일해 골든은 불변이다.
notify-affected-pull-requests job 이 코멘트를 남기는 대상은 언제나 PR 이다.
GitHub 은 issues/{n}/comments 의 필요 스코프를 대상 타입으로 가르므로 PR 에
쓰려면 pull-requests 쓰기가 필요하다. read 로 두면 목록 GET 은 통과하고 첫
POST 에서 403 이 나 스택 상위 PR 안내가 통째로 막힌다.
merge-order-guard-policy.test.mjs 는 issues 쓰기만 계약으로 박아 두고
pull-requests 칸은 비워 뒀다. 그 빈칸이 read 로 머문 채 머지된 경로라
같은 자리에 단언을 더한다. 권한을 read 로 되돌리면 그 테스트가 실패한다.
PlusBadgeButton 이 원을 colors.black(#000000)으로 칠해 시안보다 한 단계 진하다. 시안 컴포넌트 52:7886 의 fill 은 #212121 이고, 그 값은 팔레트의 Gray9 와 같다. Black 토큰 정의는 건드리지 않는다. 이 컴포넌트가 고르는 토큰만 바꾼다. core 의 다른 어두운 원형 버튼인 PenFloatingActionButton 과 AfternoteFloatingActionButton 이 이미 gray9 를 쓰고 있어, 시안 정합과 core 내부 일관성이 같은 방향이다. 새 시맨틱 토큰은 만들지 않는다. 한 컴포넌트를 위한 토큰이고 gray9 가 이미 이 역할의 기존 토큰이다.
브랜치를 지우면 그 브랜치를 base 로 하던 열린 PR 이 GitHub 정책에 따라 즉시 닫힌다. 닫는 코멘트도 리뷰도 남지 않아, 승인까지 받은 #1626 이 사라진 사실이 이틀 동안 드러나지 않았다. 삭제 자체는 막지 않고 닫힘만 되돌린다. on: delete 워크플로가 삭제 이벤트를 받아, 타임라인에서 마지막 closed 바로 앞이 base_ref_deleted 인 미병합 PR 만 다시 열고 경위를 코멘트로 남긴다. 같은 이름의 브랜치가 다시 올라오지 않아 base 가 없으면 되살리는 대신 끊긴 사슬을 추적 이슈로 기록한다. 이미 머지된 PR 의 base 삭제 같은 정상 정리에는 반응하지 않는다. 판정과 문구는 recover-base-deleted-pull-requests.mjs 에 두고 정책 테스트로 고정한다. 워크플로 YAML 에는 분기가 없다. PR 코멘트가 issues 엔드포인트로 나가도 대상이 PR 이면 pull-requests: write 가 필요하다는 #2068 의 교훈을 처음부터 권한에 반영했다.
…n-guard maintenance(platform): base 브랜치 삭제로 자동 폐쇄된 PR 을 되살린다 (#2076)
fix(platform): 스택 안내 코멘트에 pull-requests 쓰기 권한을 준다 (#2068)
ListItem.isDraft, getPagedDrafts, draftOnly 쿼리를 제거한다. 셋 다 소비처가 없었다. ListItem.isDraft 의 KDoc 은 목록에서 여는 방향이 갈린다고 적었지만 그 분기를 하는 화면 코드가 없고, develop 에는 임시저장 기능 자체가 없다 (요청 payload 의 isDraft 도 presentation 의 참조도 0건). 목록에서 임시저장을 구분해 보여줄지는 #1792 가 PM 에게 물어 둔 결정이고 아직 답이 없다. 그 답에 따라 기능이 안 들어갈 수도 있어 지금 계약을 박으면 죽은 코드가 된다. 남기는 것은 #1745·#1762 의 범위인 조회·요청 계약이다. DraftDetail 과 getDraftDetail 은 이어쓰기 프리필이 발행 상세 매퍼의 requireNotNull(playlist) 에 걸리는 것을 푸는 본체이고, 요청 payload 의 isDraft 는 임시저장으로 저장하는 계약이다. 둘 다 소비자는 #808 이다. AfternotePagingSource 의 pageSize 고정은 유지한다. Refresh/Append 힌트가 서버 offset 을 어긋내는 별개 버그 수정이다. 그 회귀 테스트도 draft 축만 걷어내고 남겼다.
제목의 `(#N)` 하나만 보던 탓에, 한 PR 이 본문에 닫는 이슈를 둘 적어도 두 번째 이슈는 라벨도 링크 코멘트도 받지 못했다. Repository Quality 가 제목에 `(#N)` 을 정확히 하나만 허용하므로 제목에서 나오는 이슈는 구조적으로 최대 한 건이고, GraphQL 질의는 본문을 가져오지도 않았다. base 가 기본 브랜치가 아닌 스택 PR 은 closingIssuesReferences 가 비어 Development 칸까지 비므로 라벨이 유일한 신호다. PR #2051 의 두 번째 이슈 #2045 가 그렇게 묻혀 미착수 조사에서 미착수로 읽혔다. 지금 저장소 전체에 같은 이유로 신호가 빠진 이슈가 여섯 건이다. 판정은 validate-pr-issue-link 의 extractClosingIssueNumbers 를 그대로 쓴다. 게이트가 인정하는 링크와 라벨이 인정하는 링크가 갈리지 않게 하려는 것이고, merge-order-guard 가 본문에서 닫는 이슈를 뽑는 패턴과도 같은 모양이다. Refs·Part of·Related to 는 종전대로 줍지 않으므로, 참고로 언급했을 뿐인 이슈에 라벨이 붙지 않는다는 종전 결정은 그대로다. Closes #2082
설정의 비밀번호 변경이 이용 불가 안내 대신 실제 화면으로 간다. 서버·네트워크·데이터·도메인 계약(`AccountRepository.passwordChange` → `POST auth/password/change`)은 이미 develop 에 들어와 있었고 소비처만 없었다. 이 커밋이 그 첫 소비처다. 화면은 서버 계약과 같은 두 칸(현재·새 비밀번호)이다. 새 비밀번호 규칙은 BE `PasswordValidation.REGEX` 를 그대로 옮겨 제출 전에 막는다. 숫자를 `[0-9]` 로 명시한 이유는 Android 정규식의 `\d` 가 유니코드 숫자까지 세어 서버 판정과 갈리기 때문이다(#1628). 현재 비밀번호에는 규칙을 걸지 않는다. 옛 규칙으로 만든 비밀번호를 쓰는 계정이 변경 자체를 못 하게 되기 때문이다. `AccountRepositoryImpl.passwordChange` 만 `.mapAccountFailure()` 가 빠져 있어 1206(새 비밀번호가 기존과 같음)과 1702(소셜 가입 계정)가 원본 예외로 새고 있었다. 붙이고 회귀 테스트로 잠갔다. ViewModel 은 konsist 규칙 B 에 따라 `MviViewModel` 을 상속한다. 그 결과 `MviViewModel` 과 `ObserveSignal` 이 첫 프로덕션 참조를 얻어 `INTENTIONAL_PRODUCTION_CONTRACTS` 의 두 항목이 stale 이 되므로 함께 지운다. base 는 develop 이 아니라 PR #2002 의 `fix/2000-setting-menu-feedback` 이다. 그 PR 이 같은 6줄을 `onUnavailableClick` 으로 몰아넣고 `SettingScreen` 시그니처에서 파라미터를 걷어내는 중이라 develop 기준으로 짜면 같은 줄을 두 번 고치게 된다. `SettingFlowTest` 의 이용 불가 목록에서도 비밀번호 변경을 뺐다. 나머지 5개 목적지(고객센터, 문의, FAQ, 이용약관, 개인정보 처리방침)는 제품 결정 대기라 그대로 둔다. 시안 대조는 하지 못했다. 이 화면 전용 시안을 열 수단이 없어 배치는 회원가입 3단계와 비밀번호 찾기의 선례를 따랐다. 현재 비밀번호 불일치(1202)의 실패 번역은 PR #2020 이 그 번역표를 파일째 옮기는 중이라 이 커밋의 범위에서 뺐다. Closes #2083 Refs #564
설정 > 수신자 목록이 0건이면 LazyColumn 이 아무것도 그리지 않아 상단바만 남은 빈 화면이 됐다. 노션 2차 QA 6번이 짚은 빈 목록 안내와 등록 진입점 부재 그대로다. receivers 가 비면 목록 대신 안내 문구와 [수신자 등록하기] 버튼을 그린다. 분기 모양은 같은 모듈 NoticeListScreen 의 0건 분기와 같고, 문구·일러스트 구성은 확정 시안 4163:20979 을 구현한 애프터노트 SelectReceiverEmpty 를 준거로 삼았다. 등록 CTA 는 Scaffold bottomBar 가 아니라 본문 안에 둔다. 뒤에 목록 상태 슬롯(#1281)으로 이 본체를 통째로 옮길 때 빈 상태만 CTA 를 잃지 않게 하려는 것이다. 범위는 #556 에서 떼어 낸 목록 0건 축 하나다. 이 커밋으로 #556 이 닫히지 않으므로 대표 이슈를 #2086 으로 새로 판다. 나머지 두 축은 이번 범위 밖이다. 검색 결과 0건은 검색어를 private 스캐폴드가 쥐고 있어 core:ui ReceiverSelectScreen 안에서 닫아야 하는데 그 items 블록을 #2057 이 재작성 중이고, 설정 사후 전달 조건 선택 화면의 0건은 #1998 PM 답변에 걸린 #2051 의 목록 상태 분기에 얹힌다. 타임레터 RecipientListScreen 은 공용 컴포넌트를 쓰지 않는 별개 구현이라 별도 이슈다. Closes #2086 Refs #556
서버는 10MiB 를 넘는 업로드를 code 1803(FILE_SIZE_EXCEEDED)으로 거절하는데 FE 에 번역 자리가 없어 소비처가 일반 실패로 받아 재시도를 권한다. 파일 크기가 그대로인 한 재시도는 같은 거절을 다시 받는다. - core:domain 에 FileUploadFailure 루트와 FileSizeExceeded 하위 타입을 둔다. 사진·영상 두 저장소가 같은 POST /files/presigned-url 에 원본 크기를 그대로 실어 보내 같은 코드를 맞으므로 매체별로 가르지 않는다. - core:data 의 mapUploadFailure 가 code 1803 만 치환하고 사유를 확인하지 못한 실패는 원본 그대로 흘린다. 전송 실패와 S3 PUT 실패는 재시도가 실제로 통하는 경로라 건드리지 않는다. PushSettingFailureMapper 가 세운 별도 파일 관례를 따른다. - PhotoUploadRepositoryImpl·VideoUploadRepositoryImpl 이 runCatchingCancellable 결과에 번역을 체인한다. 표시 문구는 이 커밋 범위가 아니다. 애프터노트 에디터 연결은 시안 정본(4327:72071) 대조가 남아 #1868 에 그대로 있고, 마음의 기록은 #2080, 타임레터는 #2081 이 맡는다. Closes #2089 Refs #1868
발행 상세 매퍼는 PLAYLIST 에 playlist 가 없으면 던진다. 임시저장은 카테고리별 필수값 검증을
건너뛰므로 곡을 한 곡도 안 담으면 서버가 playlist 자체를 생략하고, 그 응답이 발행 매퍼로 가면
이어쓰기가 열리지 않는다. 같은 GET /afternotes/{id} 를 관용해서 읽는 경로를 따로 둔다.
- core 도메인에 DraftDetail 과 DraftContent 를 둔다. 공통값만 직접 소유하고 종류별 값은 content
가 배타적으로 표현하는 구조는 발행 Detail·DetailContent 와 같다. 다른 것은 필수값 보장뿐이다 —
여기서는 아직 안 쓴 칸이 null·빈 목록으로 온다.
- 종류별 값을 평평하게 늘어놓지 않은 이유는 그렇게 두면 ESTATE 에 계정 정보 칸이, GALLERY 에 곡
목록 칸이 생기기 때문이다. 서버가 그 값을 실어 보내도 아무도 모르게 버려지고 잘못된 조합이
컴파일된다. 소비처(에디터 프리필)도 결국 종류로 갈라 sealed 를 만들므로 접는 지점을 도메인에 둔다.
- 발행용 DetailContent 를 재사용하지 않는 것은 그쪽 credentials 가 non-null 계약이라 「아직 안 씀」
을 표현할 자리가 없어서다. 재사용하려면 발행 계약을 임시저장 사정으로 느슨하게 만들어야 한다.
- toDraftDomain 은 종류 해석만 엄격하다. 해석 못 하는 category 는 폼을 못 고른다(#1048).
Closes #1745
…/1617-dirty-field-patch
앞 주석은 기본값을 두면 append 가 한 번 더 돈다고 적었는데 근거가 없다. AfternotePagingSource 가 loadSize 힌트를 무시하므로 이 값은 서버 요청도 수신 개수도 바꾸지 않는다. AfternotePagingRequestTest 가 초기 30개 힌트에도 (0..4) to 10 으로 요청된다는 것을 이미 고정하고 있다.
AfternotePagingSource 가 loadSize 힌트를 무시하고 size 를 PAGE_SIZE 로 고정하므로 이 값은 서버 요청도 수신 개수도 바꾸지 않는다. 동작에 영향이 없는 값을 config 에 두면 그것이 동작을 정한다고 읽힌다. 페이지 크기를 고정하는 이유는 AfternotePagingSource 의 주석이 적고, 힌트를 무시한다는 사실은 AfternotePagingRequestTest 가 고정한다.
feat(afternote): 임시저장 조회와 저장 payload를 분리한다 (#1762)
제목과 작성일이 둘 다 가중치 없이 한 Row 에 놓여 있어, 긴 제목이 가용 폭을 모두 쓰면 뒤에 측정되는 작성일이 0dp 를 받았다. 글자 배율 2.0 과 8글자 이름이 겹치는 조합에서 「2026.09.13」 이 열 줄로 한 글자씩 쪼개지고 카드 높이가 400dp 대까지 늘었다. 날짜 노드의 경계도 0 이라 접근성에서도 짚이지 않았다. Row 는 가중치 없는 자식을 먼저 재므로 제목에만 weight 를 준다 — 작성일이 제 폭을 온전히 받고 제목이 남은 폭을 쓴다. fill = false 라 짧은 이름에서는 SpaceBetween 이 종전과 같은 자리에 둘을 놓는다. 형식이 고정된 작성일은 maxLines = 1 로 두어 어떤 배율에서도 줄이 나뉘지 않게 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY5b56rba9yXz5JJCd5P26
수정 저장은 PATCH 라 보낸 값이 기존 기록을 덮는다. 프리필이 실패하면 화면은 빈 폼이므로, 그 상태의 저장은 사용자가 한 번도 보지 못한 원본을 자기 입력으로 갈아치운다. 종전 가드 `isEditingDraft && draftLoadError != null` 의 isEditingDraft 는 route.isDraft 일 때만 섰고, 저장 경로는 임시저장 이어쓰기와 정식 기록 수정이 똑같이 PATCH 라 정식 수정만 가드 밖에 있었다. 두 진입을 함께 세는 isEditingExistingRecord 로 바꾸고, 판정을 「실패했는가」가 아니라 「읽었는가」(draftLoaded)로 옮긴다. 실패 플래그만 보면 조회가 끝나지 않은 창에서 오류도 없고 프리필도 없는 상태가 열린다. DiaryRecordDateWireTest 의 날짜 생략 단언은 조회 실패 대신 「조회는 성공했으나 그 기록의 날짜를 해석할 수 없는」 경우로 세운다 — 조회 실패는 이제 저장 자체가 막혀 그 단언에 도달하지 못한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY5b56rba9yXz5JJCd5P26
submit() 은 「대상 레코드도 없고 오늘 질문도 없다」를 만나면 조회를 다시 걸어 사용자가 재시도할 수 있게 한다 (#565). 그 복구가 진입을 보지 않고 언제나 loadTodayQuestion() 을 불러, 과거 답변 수정에서 프리필이 실패하면 questionId 가 오늘 것으로 채워졌다. 다음 저장은 원래 답변의 PATCH 가 아니라 오늘 질문의 신규 POST 로 나가 고치던 글이 다른 질문에 남았다. 복구가 부를 대상을 진입이 정하게 한다 — editingAnswerId 가 있으면 같은 대상을 다시 조회한다. 신규 작성의 오늘 질문 재시도는 그대로다. 함께, 수정 진입은 대상을 읽은 뒤에만 저장을 연다. 못 읽은 채 저장하면 draftId 가 null 이라 PATCH 가 아니라 POST 로 나간다 — 일기 화면(#2027)과 같은 「실패했는가」가 아니라 「읽었는가」 판정이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY5b56rba9yXz5JJCd5P26
일기와 데일리답변이 복수 첨부를 Boolean 하나(isUploadingImage)로 잠그고 있었다. 두 가지가 어긋났다. 먼저 끝난 하나가 잠금을 통째로 풀었다 — 첨부 A·B 를 잇따라 고르고 A 만 끝나면 B 가 아직 올라가는 중인데 저장이 열렸고, 그 저장에는 B 가 빠진 본문이 실렸다 (#2029). 그리고 취소에는 내려놓을 자리가 없었다 — 업로드 코루틴은 작성 화면의 rememberCoroutineScope() 가 소유하므로 구성 변경이면 코루틴만 끊기고 Result 는 오지 않는다. 잠금이 참으로 굳어, 이미 쓴 내용을 저장하려면 관계없는 첨부를 한 번 더 성공시켜야 했다 (#2030). 잠금을 uploadingImageCount 로 세고 isUploadingImage 를 그 파생값으로 둔다. 올리고 내리는 짝은 try/finally 라 성공·실패·취소 어느 쪽으로 끝나든 자기 몫만 내려놓는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY5b56rba9yXz5JJCd5P26
fix(receiver): 발신자 상세 VM 이 route 를 assisted 로 받아 진입 크래시를 없앤다 (#2168)
build(app): app 이 직접 쓰지 않는 feature:afternote:domain 의존을 빼고 Nav2 회귀 주석을 고친다 (#2172)
…onse fix(core): 수신자 상세 응답 계약을 맞춰 수정 화면 조회 실패를 막는다 (#2155)
refactor(setting): Nav3와 수신자 목록 MVI 진입을 선행으로 정리한다 (#1695)
…ue-entry fix(core,app): Nav3 화면의 predictive back 배경을 불투명하게 유지한다 (#2145)
…ination fix(setting): 설정의 비밀번호 변경 목적지를 연결한다 (#2083)
…로 올린다 (#2045) 설정이 애프터노트 것을 베껴 만든 조회 실패 안내와 갱신 실패 배너를 core:ui 의 ReceiverListLoadFailure·ListRefreshErrorBanner 로 올리고 문구를 core_ui_ 리소스로 옮긴다. 설정 사본과 애프터노트 원본(수신자 선택의 private 안내, shared 배너), 그 setting_·afternote_ 문구를 지우고 두 기능이 core 것을 쓴다. 배너는 애프터노트 홈 목록도 쓰는 범용이라 receiver 가 아니라 loading 패키지에 둔다. 두 쪽 렌더는 그대로다.
fix(core): 수신자 목록 캐시의 로그인 세션 경계 보장 (#2135)
fix(core): 초성 점프 인덱스를 렌더되는 행에서 파생 (#939)
…apper refactor(afternote): 임시저장 프리필 계약의 이름과 매퍼 모양을 정리한다 (#2115)
refactor(afternote): 비교 스냅샷을 카테고리별로 분리한다 (#2163)
# Conflicts: # core/data/src/main/java/com/afternote/core/data/repoimpl/UserReceiverRepositoryImpl.kt
…tate fix(core,setting): Nav3 목록의 조회 상태와 MVI 재시도를 연결한다 (#2045)
…elop/github-actions-updates-cb21f7ad26 chore(deps): bump the github-actions-updates group with 4 updates (#2139)
fix(ci): CI 파서의 정규식 처리 지연과 XML 이중 해제 수정 (#2218)
…ncy-graph fix(ci): 소스 변경만 있는 스택 부모의 의존성 기준선 생성 (#2185)
…cision ci(platform): 댓글 없는 QA 배포 시점 판정 (#2239)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
먼저 볼 파일:
.github/workflows/release-distribution.yml,app/build.gradle.kts,docs/release/distribution.md.배포 범위
마지막 성공 Firebase 배포 이후 develop에 머지된 PR 188건을 main으로 승격합니다. 배포 시점 자동 판정 #2240을 포함하며 이 PR에서 새 앱 구현은 추가하지 않습니다. 연결된 완료 이슈는 GitHub의 실제 closingIssuesReferences를 전량 대조한 202건입니다. PR 제목에만 있는 이슈는 완료 목록에 임의로 추가하지 않았습니다.
36c4bc56d1288d18b7448b811433a91227a657218db2d75dad2f881b5b33a1e189bed64a6be65b9bf5ad611b23a32c6db2de4795bd9cee9fb32aacf0258210245e1bd88203be58a7b7767b29b2fc429a는 develop tree와 같으며 충돌이 없습니다.Collect Release Scope의 50 PR 제한으로 자동 수집이 이 누적 범위를 처리하지 못합니다. 아래 목록은 전체 first-parent 이력 188개와 각 PR의 실제 MERGED 상태·base·merge SHA, 페이지 끝까지 조회한 이슈 연결을 대조한 결과입니다. 일부만 잘라낸 목록이 아닙니다.검증
756e456f에서 Release AAB Preflight가 빌드·기동 검증에 성공했습니다. 이후 #2240은 판정 CI와 운영 문서만 추가했습니다.756e456f와 후보 사이의 차이는 새 배포 판정 CI 3개 파일과 운영 문서뿐이며 앱·빌드·계측 입력은 같습니다. 이번 PR의 계측 재실행은 본문 갱신과 이전 인프라 재시도의 concurrency로 취소가 섞였으므로, 취소를 통과로 표기하지 않고 동일 입력의 완료된 검증을 사용합니다.릴리스 승격의 담당 검사
현재 모듈 담당 가드에는 develop→main 승격 예외가 없어 이미 develop에 머지된 다른 담당 모듈 165개 파일을 새 담당 외 구현으로 판정합니다. 이 PR은 해당 트리를 그대로 승격하므로 기존
issue-assignee-exempt라벨로 담당 검사만 제외합니다. 대표 이슈 #2241의 실제 담당자는 1hyok이며, 앱 검증과 배포 산출물 검증은 실행합니다.관련 이슈
Closes #2241
포함 이슈
QA 포인트
대상은 develop
8db2d75dad2f881b5b33a1e189bed64a6be65b9b에서 만드는 Firebase release APK입니다. 아래 항목은 이번 통합 APK에서 실행할 계획이며, 기존 PR의 단위·계측 테스트 결과나 과거 기기 QA를 이번 APK의 통과 결과로 대체하지 않습니다. 계정 생성·비밀번호 변경·수신자 수정·노트 저장·첨부 삭제·녹음은 테스트 계정과 테스트 자료로만 수행합니다.설치와 기동: 마지막 Firebase 앱이 설치된 테스트 기기와 깨끗한 기기에서 새 APK를 설치하고 콜드 스타트한다. 기존 설치 위 업데이트가 서명 오류 없이 완료되고, 기존 테스트 데이터가 유지되며, 로그인·로그아웃 상태에 맞는 첫 화면이 크래시 없이 열린다. APK 해시·versionCode·Android 버전·배포 run을 기록한다. 근거: PR chore(release): Firebase 배포에 대역 안 versionCode 를 주입하고 Play 대역 하한을 적용한다 (#2062) #2073, security(platform): Auto Backup 에서 자격 저장소와 기기 사실을 제외한다 (#1876) #2012, feat(app,core): 강제 업데이트 관문을 앱 기동에 배선한다 (#1539) #1642.
세션과 토큰 재발급: 만료된 액세스 토큰과 유효한 갱신 토큰을 가진 테스트 세션에서 앱을 종료했다 다시 켜고 여러 탭의 목록을 연다. 동시 401 뒤 재발급이 진행되고 요청들이 성공 또는 오류 상태로 끝나며 무기한 로딩에 멈추지 않는다. 갱신 토큰까지 무효인 경우 이전 계정 자료가 남지 않는지 함께 확인한다. 만료 조건을 만들 수 없으면 미검증으로 기록한다. 근거: PR fix(core): 재발급 클라이언트에 전용 Dispatcher 를 줘 콜드 스타트의 동시 401 교착을 없앤다 (#2160) #2162, fix(core): 수신자 목록 캐시의 로그인 세션 경계 보장 (#2135) #2137.
계정 간 자료 격리: 수신자 자료가 서로 다른 테스트 계정 A와 B를 준비한다. A 목록을 읽은 뒤 로그아웃하고 B로 로그인해 목록을 열며, B의 첫 조회 실패도 재현한다. B 화면에 A의 수신자 이름·이메일·선택값이 나타나지 않고, 같은 계정의 일시적인 갱신 실패에서는 기존 행과 오류 안내가 함께 유지된다. 근거: PR fix(core): 수신자 목록 캐시의 로그인 세션 경계 보장 (#2135) #2137, fix(core,setting): Nav3 목록의 조회 상태와 MVI 재시도를 연결한다 (#2045) #2154.
인증 요청과 입력 변경: 테스트 이메일로 회원가입 및 현재 접근 가능한 아이디 찾기 화면에서 인증번호 발송·확인을 연속 누르고, 요청 중 이메일이나 코드를 바꾼다. 진행 중 같은 요청이 중복되지 않고, 이전 입력에 대한 늦은 응답이 새 입력을 인증 완료로 만들지 않으며, 실패 뒤 다시 시도할 수 있다. 테스트 수신함으로 도착과 요청 수를 확인한다. 근거: PR fix(onboarding): 아이디 찾기 인증번호 발송·확인을 진행 중인 Job 으로 막는다 (#1864) #2101, fix(onboarding): 인증 결과가 자기가 검증한 입력에만 적용되게 한다 (#2025) #2102.
가입 부분 성공과 비밀번호 규칙: 삭제 가능한 신규 테스트 계정으로 가입을 진행한다. 비밀번호 경계 7·8·15·16자와 허용 밖 문자에 대한 제출 가능 여부를 확인하고, 가입 성공 뒤 프로필 처리 실패를 재현할 수 있는 환경에서는 재시도가 실제 생성 계정에만 귀속되는지 확인한다. 지원하지 않는 실패 주입은 통과로 표시하지 않는다. 기존 계정 로그인에는 새 비밀번호 작성 규칙을 소급 적용하지 않는다. 근거: PR fix(onboarding): 비밀번호 규칙을 서버와 같은 8~15자 허용 목록으로 좁힌다 (#1628) #1644, fix(onboarding): 가입 부분 성공 복구를 실제로 만들어진 계정에 귀속한다 (#2026) #2103.
설정의 비밀번호 변경: 이메일 방식 테스트 계정에서 설정의 비밀번호 변경 메뉴에 들어가 잘못된 현재 비밀번호, 같은 새 비밀번호, 규칙 위반, 정상 변경을 차례로 시도한다. 실패는 성공으로 보이지 않고 입력을 고쳐 다시 제출할 수 있으며, 정상 변경 후 이전 설정 화면으로 돌아간다. 새 비밀번호로 다시 로그인되는지 확인하고 테스트 계정을 원상복구한다. 근거: PR fix(setting): 설정의 비밀번호 변경 목적지를 연결한다 (#2083) #2094. 현재 비밀번호 불일치의 세션 처리 개선 PR #2229는 이번 범위가 아니다.
화면 이동과 키보드: 로그인·가입·설정·수신자 흐름에서 하위 화면 진입, 뒤로가기, 취소, 탭 재선택을 반복한다. 목적 화면과 수신자 ID가 맞고 이전 화면으로 정상 복귀하며, 키보드가 열린 상태에서도 입력과 하단 버튼을 사용할 수 있다. 제스처 뒤로가기 지원 기기에서는 진행·취소 중 배경이 비거나 겹치지 않는다. 근거: PR refactor(platform): 소관 그래프 4종을 Navigation 3 로컬 스택으로 옮긴다 (#1698) #1824, refactor(setting): Nav3와 수신자 목록 MVI 진입을 선행으로 정리한다 (#1695) #2173, fix(core-ui,app): 인증 흐름에서 키보드가 하단 CTA 를 덮지 않게 IME inset 을 전달한다 (#1849) #1853, fix(platform): 시스템 여백 소비를 전달하고 하단 버튼 중복 보정 제거 (#1891) #1991, fix(platform): predictive back 진행 중 앞 화면을 축소해 겹침을 푼다 (#1869) #2058, fix(core,app): Nav3 화면의 predictive back 배경을 불투명하게 유지한다 (#2145) #2153.
수신자 목록과 편집: 수신자 0명·여러 명인 테스트 계정 및 관계·연락처가 비어 있는 수신자를 준비한다. 설정 관리 목록의 최초 로딩, 정상 0건, 최초 조회 실패, 기존 행이 있는 갱신 실패와 재시도를 확인한 뒤 행을 눌러 수정하고 복귀한다. 0건과 실패가 구분되고, 편집 화면에 응답값이 채워지며, 저장한 이름·연락처가 활성 목록에 반영된다. 근거: PR fix(setting): 수신자 목록을 선택 화면에서 관리 화면으로 바꾼다 (#631) #1922, fix(setting): 수신자 목록 0건에 안내와 등록 진입점을 그린다 (#2086) #2092, fix(setting): 수신자 목록 응답 계약을 맞춰 행 소실을 막는다 (#2105) #2120, fix(core): 수신자 상세 응답 계약을 맞춰 수정 화면 조회 실패를 막는다 (#2155) #2159, fix(core): 수신자 수정 성공 후 활성 목록 갱신 (#2127) #2130, fix(core,setting): Nav3 목록의 조회 상태와 MVI 재시도를 연결한다 (#2045) #2154.
수신자 검색과 복수 선택: 이름 초성이 다른 여러 테스트 수신자를 준비한다. 검색·초성 탭·스크롤·다중 선택을 거쳐 애프터노트 편집으로 돌아간다. 보이는 목록의 올바른 행으로 이동하고 선택이 유지되며, 선택한 수신자 ID가 중복·소실 없이 전달된다. 선택을 취소하면 기존 편집 내용을 보존한다. 근거: PR fix(core): 수신자 초성 탭과 검색 후 하이라이트 유지 (#1057) #2019, fix(core): 초성 점프 인덱스를 렌더되는 행에서 파생 (#939) #2057, feat(afternote): 수신자 선택 반환 채널을 복수로 열어 한 번에 N명 지정 (#1426) #1582, feat(afternote): 수신자 0건 빈 상태를 확정 시안 4163:20979 의 문구·일러스트로 바꾼다 (#1894) #1583.
애프터노트 목록 오류 복구: 카테고리별 테스트 노트가 있는 계정에서 카테고리를 바꾸고 조회·새로고침 실패를 재현한다. 카테고리 행과 가능한 기존 목록이 남고, "전체" 선택 또는 재시도로 복구할 수 있으며, 카테고리 행의 스크롤이 불필요하게 초기화되지 않는다. 근거: PR fix(afternote): 발신 흐름의 무음 실패에 복구 수단과 진행 상태를 결선한다 (#705) #1608, fix(afternote): 필터 조회가 실패해도 카테고리 행을 남겨 「전체」로 돌아오게 한다 (#1634) #1640, fix(afternote): 카테고리 전환 중 상단을 남기고 필터 행 스크롤을 보존한다 (#1635) #1646.
애프터노트 부분 수정과 미디어 삭제: 곡·사진·영상·수신자가 있는 테스트 노트를 연다. 제목만 바꿔 저장 후 다시 열어 나머지 값이 유지되는지 확인하고, 사진·영상은 각각 명시적으로 삭제해 저장 후 다시 조회한다. 삭제하지 않은 값은 유지되고 삭제한 미디어는 다시 나타나지 않으며, 저장 실패를 성공처럼 닫지 않는다. 저장 전 이탈 취소와 원본 조회 실패 시 저장 차단도 확인한다. 근거: PR fix(afternote): 수정 PATCH 가 안 건드린 필드를 싣지 않게 한다 (#1617) #1626, feat(afternote): 수정 모드에서 서버 영정사진·추모 영상을 지울 수 있게 한다 (#1597) #1620, fix(afternote): 발신 흐름의 무음 실패에 복구 수단과 진행 상태를 결선한다 (#705) #1608.
곡 없는 임시저장: 곡이 없는 테스트 추모 노트를 임시저장하고 목록에서 다시 연다. 발행 상세의 곡 필수 조건 때문에 조회가 실패하지 않고 임시저장 값이 복원되며, 곡을 추가한 뒤 최종 저장할 수 있다. 제목·수신자 등 다른 입력이 보존되는지도 확인한다. 근거: PR fix(afternote): 곡 없는 임시저장을 발행 상세와 분리해 조회한다 (#1745) #2095, refactor(afternote): 임시저장 프리필 계약의 이름과 매퍼 모양을 정리한다 (#2115) #2117.
마음기록 수정과 임시저장: 불완전한 테스트 일기 및 기존 일기·데일리 답변을 준비한다. 임시저장 후 이어쓰기, 기록일 변경, 수정 대상 조회 실패 후 재시도, 로딩 중 입력을 수행한다. 일기 초안이 복원되고 선택한 기록일이 유지되며, 조회 실패 중 수정 저장이 열리지 않는다. 답변 재시도는 원래 질문을 유지하고 늦은 프리필이 먼저 입력한 내용을 덮지 않는다. 근거: PR fix(mindrecord): 미완성 일기를 임시저장할 수 있게 연다 (#1065) #1943, fix(mindrecord): 일기 기록일 선택을 서버 계약대로 되살린다 (#1008) #1899, fix(mindrecord): 정식 일기 수정도 대상을 못 읽었으면 저장을 막는다 (#2027) #2097, fix(mindrecord): 답변 수정의 조회 실패 복구가 대상을 오늘 질문으로 바꾸지 않게 한다 (#2028) #2098, fix(mindrecord): 늦게 도착한 수정 프리필이 작성 중 입력을 덮지 않게 한다 (#2031) #2100.
마음기록 첨부와 저장 잠금: 테스트 이미지 두 개를 겹쳐 첨부하고 앞선 업로드만 먼저 끝나게 하거나 하나를 취소한다. 나머지 첨부가 진행 중이면 저장이 계속 잠기고, 모든 작업이 성공·실패·취소로 끝나면 저장 잠금이 남지 않는다. 화면 이탈 후 재진입에서도 입력·첨부 상태가 오해를 만들지 않는지 확인한다. 근거: PR fix(mindrecord): 진행 중인 첨부를 수로 세어 저장 잠금이 어긋나지 않게 한다 (#2029) #2099, fix(mindrecord): 본문 이미지 fileKey 를 서버 값으로 쓰고 정리 3건 (#1125) #1667.
타임레터 녹음과 임시저장: 테스트 녹음만 사용한다. 마이크 권한 거부·허용, 녹음 시작 직후 이탈, 정지 연속 누르기, 첨부 재생, 임시저장 후 재진입을 수행한다. 권한 거부는 복구 가능한 안내로 끝나고, 녹음 종료나 화면 이탈이 크래시·중복 파일을 만들지 않으며, 저장한 테스트 음성이 재진입 후 유지된다. 임시저장 목록의 제목·수신인·예정일과 선택 삭제가 대상 자료에만 적용되는지 확인한다. 근거: PR fix(timeletter): 음성 메시지 녹음 기능 구현 (#437) #440, fix(timeletter): 임시저장 목록 UI 개선 (#571) #767.
수신 기록과 열람 신청: 테스트 발신자가 등록된 수신 흐름에서 웰컴의 전달받은 기록 확인, 발신자 카드, 상세, 뒤로가기를 수행한다. 정확한 발신자 상세가 크래시 없이 열리며, 테스트 이메일·서류·마스터키 경로의 실패는 오류 안내와 재시도 또는 복귀로 처리된다. 현재 미구현 내보내기를 눌렀을 때 다운로드 완료로 잘못 알리지 않는다. 근거: PR fix(receiver): 발신자 상세 VM 이 route 를 assisted 로 받아 진입 크래시를 없앤다 (#2168) #2169, refactor(core,receiver): 공통 오류 팝업 2종을 세우고 열람 신청 흐름을 정렬한다 (#1736) #1609, refactor(receiver): 열람 신청 서류 제출 불변식을 UseCase 로 옮긴다 (#1701) #1835, fix(receiver): 미구현 내보내기를 성공이 아니라 실패로 닫는다 (#1726) #1629, refactor(home): 수신자 홈 내려받기 실패를 공통 오류 팝업으로 옮긴다 (#1737) #1940.
알림 동의와 실패 재시도: 테스트 계정에서 문자·이메일·푸시 마케팅 동의와 서비스 푸시 설정을 각각 바꾸고 재진입한다. 저장한 채널 값이 유지되고, 저장 실패 시 해당 값이 되돌아오며 실패 안내와 재시도가 가능하다. 마케팅 동의와 서비스 푸시 설정이 서로 잘못 바뀌지 않고, 진행 중 같은 토글의 중복 요청을 막는다. 근거: PR fix(setting): 마케팅 알림 수신 동의(문자/이메일/푸시)가 저장되지 않던 문제 수정 (#560) #1923, fix(setting): 푸시 알림 저장 실패 안내 및 재시도 보완 (#558) #1924.
알림 진입과 로그인 예약: Android 알림 권한 허용·거부 상태에서 앱의 cold·warm·background 진입을 확인한다. 알림을 눌러도 기존 화면·인증 경계를 깨거나 크래시가 나지 않으며, 로그인 전에는 데일리 알림이 새로 예약되지 않고 로그인 후 예약·로그아웃 후 취소가 반영된다. WorkManager 예약은 실행 지연과 구분해 확인한다. 이미 표시된 알림 삭제와 알림별 목적지 탭 이동은 이번 변경의 완료 조건이 아니다. 근거: PR fix(app,core): 알림 목적지를 계약값으로 실어 진입 큐까지 전달한다 (#1727) #1641, refactor(app): 알림 권한 상태·결과를 MVI로 전환 (#1809) #2033, fix(core): 로그인한 기기에만 데일리 알림을 예약한다 (#2146) #2151.
생체 인증과 배포 경계: 생체 인증과 기기 잠금이 설정된 테스트 기기에서 패스키 생성 진입 및 애프터노트 인증의 성공·취소·실패를 확인한다. API 30 이상에서는 암호 연산 실패를 성공으로 취급하지 않고, 지원되는 API 26~29 경로에서는 크래시 여부를 별도로 기록한다. 이번 QA는 로컬 인증 경계이며 서버 패스키 등록·목록 완성을 주장하지 않는다. 근거: PR security(setting): 패스키 생체 인증을 암호 연산으로 확정한다 (#1166) #2013. 서버 등록 개선 PR fix: 서버 패스키 등록과 목록 상태 복구 (#1944) #2236 및 구버전 호환 개선 PR #2226은 이번 범위가 아니다.
글자 확대와 화면 표현: 작은 화면에서 글자 크기를 확대하고 긴 테스트 이름·가로로 긴 프로필 사진을 사용한다. 홈·주간 요약·탈퇴 안내·설정·수신자 화면에서 핵심 문구와 버튼이 잘리거나 겹치지 않고 사진은 원을 채운다. 시스템 다크 모드에서도 현재 라이트 팔레트와 읽을 수 있는 시스템바를 유지한다. 근거: PR fix(mindrecord): 주간 요약 카드를 글자 크기에 따라 자라게 한다 (#1718) #1720, fix(core-ui,app): 다크 팔레트 확정까지 팔레트·시스템바를 라이트로 잠근다 (#1873) #1723, fix(core-ui): 라이트 테마 본문 대비를 4.89:1 로 올린다 (#1798) #1799, fix(setting): 글자 확대 시 탈퇴 계정 카드 잘림 수정 (#1147) #2004, fix(home): 큰 글씨·긴 이름에서 한마디 작성일이 한 글자씩 세로로 서는 것을 막는다 (#2032) #2096, fix(core): 가로로 긴 프로필 사진이 원을 꽉 채우도록 Crop 으로 그린다 (#2143) #2148.
백업과 기기 이행: 테스트 계정과 개인정보 없는 테스트 녹음만 저장한 전용 기기에서 지원되는 클라우드 복원 또는 기기 간 전송을 수행한다. 새 인증 전에 이전 기기의 로그인 토큰·수신자 마스터키·알림 권한 요청 이력·녹음 임시 원본이 복원되어 남지 않는지 확인한다. API 31 미만과 이상, 클라우드 복원과 기기 전송은 실행한 조건별로 구분하고 사용할 수 없는 복원 경로는 미검증으로 남긴다. 근거: PR security(platform): Auto Backup 에서 자격 저장소와 기기 사실을 제외한다 (#1876) #2012, fix(timeletter): 음성 메시지 녹음 기능 구현 (#437) #440.
범위 해석과 미검증 조건
OnboardingNavHost.kt의 로그인 링크는FindIdRoute로 이동하고FindIdScreen.onNextClick은 빈 콜백이다. 비밀번호 재설정의 프로덕션 진입 연결을 이번 완성 기능으로 안내하지 않는다. 설정의 로그인 사용자 비밀번호 변경 PR #2094와 구분한다.ForceUpdateGate는 스토어 배포 플래그가 없는 Firebase APK를 막지 않는다. Firebase QA의 기대결과는 업데이트 팝업 없이 정상 진입하는 것이다. Play 산출물의 강제 업데이트 동작은 별도 검증 대상이다.CI Test Plan
{ "androidTest": { "mode": "full", "reason": "마지막 Firebase 배포 이후 develop 누적을 main으로 승격하는 통합 릴리스입니다. 전역 Gradle 설정·build-logic·app 및 계측 하네스 변경이 포함되어 전체 계측 레인을 검증합니다.", "tests": [] } }