You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
GET /app/version 이 배선 0건 — 강제 업데이트를 붙이거나 죽은 서비스를 걷는다 #1539
즉 앱은 서버가 「이 버전은 더 못 쓴다」고 말해도 그 사실을 물어보지 않는다. 구버전 사용자가 계속 쓰다 다른 곳에서 깨진다.
완료 조건
둘 중 하나로 끝낸다. 지금처럼 «있는데 안 쓰는» 상태로 두지 않는다.
배선한다 — 앱 시작 시 버전을 조회해 강제 업데이트가 필요하면 스토어로 보내는 관문을 붙인다. 서버 무응답·조회 실패에 앱이 잠기지 않아야 한다(fail-open)
또는 걷는다 — 제품 요구가 아니면 AppVersionApiService·DI 등록·계약 테스트를 함께 지운다
참고
#423 의 「후속(범위 밖)」에 적혀 있던 «신규 엔드포인트들의 usecase/UI 배선» 중 아직 주인이 없는 갈래다. 같은 후속의 나머지는 이미 추적되고 있다 — 비밀번호 찾기 배선은 #457, 아이디 찾기 결과 화면은 #474, 구 users/delivery-condition 화면 마이그레이션은 이미 완료(구 엔드포인트 참조 0건, 설정 화면이 수신자별 getReceiverDeliveryConditions 를 쓴다).
POST /users/me/activity 는 의도적으로 배선하지 않는다 — 서버가 로그인·토큰 재발급에서 마지막 활동 시각을 갱신하므로 중복 왕복이다(Afternote-BE#137, #1413).
선언만 있는 게 아니라 도메인 한 벌이 통째로 살아 있다 — AppVersionAdminController(/api/v1/admin/app-versions 릴리스 등록·조회) · AppVersionDataInitializer(시드) · AppVersionReleaseRepository · 서비스·컨트롤러 테스트 3종. WhiteListUrl.java:31 에 APP_VERSION("/api/v1/app/version") 로 무인증 화이트리스트 등록까지 돼 있다.
응답 계약(AppVersionCheckResponse record)은 FE AppVersionDto 와 필드·nullability 가 정확히 일치한다 — updateRequired: boolean · latestVersionCode: int · storeUrl: String?(nullable=true).
→ 「제품 요구가 아니면 걷는다」의 전제인 «죽었다» 가 반증됐다. 걷는 쪽은 오판이다. 지우면 Play 출시 시점에 그대로 다시 짜야 하고, 누락 키를 fail 시키는 AppVersionContractTest 회귀 가드까지 같이 사라진다.
덧붙여 계약 문구가 이 이슈 본문과 충돌한다. BE @Operation description:
스플래시 시작 시 호출합니다. … API 호출 실패 또는 updateRequired=true이면 클라이언트에서 스플래시 진입을 막습니다.
BE 는 조회 실패에도 fail-closed(스플래시 진입 차단)를 기술하는데, 이 이슈 완료 조건은 fail-open("서버 무응답·조회 실패에 앱이 잠기지 않아야 한다")을 요구한다. 배선 전에 어느 쪽인지 확정이 선행이다.
AFTERNOTE_VERSION_CODE 를 주입하는 워크플로 0건. docs/release/distribution.md:181 이 명시한다 — "일반 로컬·Firebase 빌드는 기본 versionCode 1을 유지한다. … 현재 값을 자동 산출·주입하거나 Play에 배포하는 workflow는 없다."
즉 1 < 10001 이 항상 참 → 존재하는 모든 빌드가 updateRequired=true 를 받는다. 관문을 붙이는 순간 개발·기획·QA 테스터 전원이 스플래시에서 잠기고, 보내지는 목적지는 http://your-playstore-domain — 존재하지 않는 도메인이다.
4. 보낼 곳 자체가 없다 — Play Store 등록이 아직 없다
docs/play-release.md:18
2026-08-05 기준, 현재 팀에서 확인 가능한 Google 계정은 Play Console 접속 시 /console/signup의 개발자 계정 생성 화면으로 이동한다. 따라서 아직 앱 등록과 Play App Signing 등록이 없으며…
배포 채널은 Firebase App Distribution 하나뿐이다. 게다가 같은 런북의 기본안대로면 Firebase APK 와 Play APK 는 설치 인증서가 달라 서로 위에 업데이트할 수 없다 — 강제 업데이트 관문이 테스터를 스토어로 보내도 인플레이스 업데이트가 원리적으로 불가능하고 삭제·재설치가 필요하다.
5. 시안 0건 (참고 — 이건 블로커가 아니다)
Figma 정본 페이지 정리 Screen Design(4327:43064) 의 11개 구역 전수 walk 결과 강제 업데이트 화면·다이얼로그 0건. 업데이트 텍스트 히트는 전부 약관 문구("애프터노트는 서비스 품질 향상을 위해 기능을 변경하거나 업데이트할 수 있습니다")뿐이고, playstore 페이지(3309:16877)는 스토어 등록용 스크린샷이라 무관하다.
다만 2026-08-23 디자이너 위임으로 시안 부재는 블로킹 근거가 아니다 — FE 재량으로 확정하면 된다. 진짜 블로커는 3·4번이다.
결론과 조치
걷지 않는다 — 서비스가 살아 있다. AppVersionApiService·DI·DTO·계약 테스트는 그대로 둔다.
지금 배선하지 않는다 — 붙이는 즉시 전 사용자 잠금이고, 보낼 스토어가 없다. 이건 FE 안에서 닫을 수 없는 선행 조건이다.
AFTERNOTE_VERSION_CODE 가 실제 릴리스 versionCode 로 주입돼 서버 latestVersionCode 와 같은 축에서 비교 가능할 것
서버가 storeUrl 에 실 Play Store URL 을 들고 있을 것 — 현재 dev 실측값은 http://your-playstore-domain 플레이스홀더다(BE application.yml 의 APP_ANDROID_STORE_URL 배포 환경변수가 미확정 값). 소비처가 0건인 지금은 무해하므로 별건으로 떼지 않고, 이 이슈 재개 시 재측정해 여전히 플레이스홀더면 그때 BE 와 확정한다.
실패 시 fail-open / fail-closed 를 BE 계약 문구와 이 이슈 사이에서 확정할 것 (현재 상반)
Firebase 테스터가 Play 빌드로 넘어갈 때의 삭제·재설치 안내를 관문 문구에 포함할지 결정
이 이슈의 2026-08-30 재판정(«걷지 않는다 · 지금 배선하지 않는다»)을 다시 실측했다. 두 결론 중 앞의 것은 그대로 유효하고, 뒤의 것은 관문 안에서 닫을 수 있었다.
유효한 것 — 걷지 않는다
BE 실코드 재확인. origin/main8e605f4 · origin/release8bfe43d 양쪽에 domain/appversion 파일 14개가 있고, AppVersionController.java · AppVersionCheckResponse.java 는 두 브랜치에서 바이트 동일이다. main↔release 129 커밋 차에 이 도메인은 걸리지 않는다 — 실서버에도 살아 있다.
응답 스키마도 실코드로 확정했다(updateRequired: boolean · latestVersionCode: int · storeUrl: String?). 기존 AppVersionDto 와 정확히 일치해 DTO 는 손대지 않았다.
닫은 것 — 「지금 배선하면 전 사용자가 잠긴다」
그 판정은 맞다. 다만 원인 두 가지가 전부 «관문이 사용자를 어디로도 보낼 수 없는 상태» 이고, 그건 관문 자신이 런타임에 판정할 수 있다. 그래서 updateRequired=true 를 받아도 아래 둘을 각각 확인한 뒤에만 막게 했다.
모든 빌드 versionCode = 1, 서버 latest = 10001. Firebase 산출물은 서명이 달라 Play 가 못 덮는다
BuildConfig.STORE_DISTRIBUTED_BUILD — 릴리스 워크플로가 versionCode 를 주입한 산출물에서만 참
거짓 → 막지 않음
오늘 존재하는 모든 빌드에서 아무도 잠기지 않는다. 오늘의 실서버 응답값(updateRequired=true + 플레이스홀더 URL)을 그대로 넣은 단위 테스트가 그것을 직접 단언한다. 그러면서 조회 자체는 매 기동마다 실제로 나가므로, 경계가 깨지면 Play 출시 때가 아니라 지금 텔레메트리에 드러난다.
「배선 재개 시 착수 조건」 4개도 이 구조가 각각 흡수한다.
versionCode 축 일치 — 안 맞는 동안은 관문이 스스로 켜지지 않는다. 주입이 서면 자동으로 켜진다.
storeUrl 플레이스홀더 — 실 URL 이 아닌 동안은 막지 않고, stage=force_update_store_url non-fatal 로 남는다. 재측정을 사람이 기억할 필요가 없다.
fail-open / fail-closed — 이슈 완료 조건대로 fail-open 으로 확정했다. BE @Operation 문구(«API 호출 실패 시에도 스플래시 진입 차단»)와 상반되는 점은 코드 KDoc·PR 본문에 명시했다. 덧붙여 관문이 시작을 지연시키지도 않게 했다 — 스플래시를 붙잡지 않고 앱은 평소대로 뜬 뒤 판정이 오면 그 위에 팝업이 얹힌다. 타임아웃 값에 기대는 실패 경로 자체가 없다.
Firebase→Play 삭제·재설치 안내 — 2번 조건 때문에 Firebase 빌드는 관문을 아예 보지 않으므로 이 문구 결정이 필요 없어졌다.
구할 판단 — blocked_by #852 를 걷을 것인가
위 근거로 이 코드는 #852 를 기다릴 이유가 없다고 본다. 다만 그 관계를 건 판단이 이 이슈 본인의 것이라 임의로 걷지 않았고, PR 본문도 Closes 가 아니라 Refs #1539 로 두었다(Closes 를 쓰면 merge-order-guard 가 red 가 된다).
동의하면 blocked_by 를 걷어 주면 PR 본문을 Closes #1539 로 올린다. 반대로 「관문 코드가 develop 에 있는 것 자체가 이르다」고 보면 PR 을 닫고 이 이슈는 지금 상태로 남긴다.
#852 와의 blocked_by 를 해제합니다. PR #1642 의 관문은 storeUrl 이 실 Play 주소이고 STORE_DISTRIBUTED_BUILD 가 참일 때만 막으므로, #852 가 세울 versionCode 주입·Play 등록은 이 코드의 선행 조건이 아니라 «켜지는 시점» 일 뿐입니다. 관계를 걸었던 전제(지금 배선하면 전원 잠금)가 코드로 사라졌습니다. #852 는 그대로 열려 있고 범위도 줄지 않았습니다. PR 은 Closes #1539 로 올립니다.
Activity
github-actions commentedon Aug 29, 2026
bug 이슈는 사이드바 Fields > Priority 로 심각도를 지정해야 합니다. 판정 기준:
값이 비어 있어도 이슈를 닫지는 않습니다. 이 안내는 이슈당 한 번만 답니다.
OnlyReadOneBook commentedon Aug 30, 2026
재판정 — 두 갈래 다 지금은 실행할 수 없다. 실측이 이지선다의 전제를 둘 다 깼다
origin/developfc8f32cd5· Afternote-BEorigin/main0a4b870· 실서버 실측 2026-08-30 20:31 KST.1. 「죽은 서비스」가 아니다 — BE 는 살아 있고 적극 유지 중이다
BE 실코드로 판정했다(OpenAPI 문서는 전 필드 optional 로 보여 근거가 못 되므로 제외).
src/main/java/com/afternote/domain/appversion/controller/AppVersionController.java선언만 있는 게 아니라 도메인 한 벌이 통째로 살아 있다 —
AppVersionAdminController(/api/v1/admin/app-versions릴리스 등록·조회) ·AppVersionDataInitializer(시드) ·AppVersionReleaseRepository· 서비스·컨트롤러 테스트 3종.WhiteListUrl.java:31에APP_VERSION("/api/v1/app/version")로 무인증 화이트리스트 등록까지 돼 있다.응답 계약(
AppVersionCheckResponserecord)은 FEAppVersionDto와 필드·nullability 가 정확히 일치한다 —updateRequired: boolean·latestVersionCode: int·storeUrl: String?(nullable=true).→ 「제품 요구가 아니면 걷는다」의 전제인 «죽었다» 가 반증됐다. 걷는 쪽은 오판이다. 지우면 Play 출시 시점에 그대로 다시 짜야 하고, 누락 키를 fail 시키는
AppVersionContractTest회귀 가드까지 같이 사라진다.덧붙여 계약 문구가 이 이슈 본문과 충돌한다. BE
@Operationdescription:BE 는 조회 실패에도 fail-closed(스플래시 진입 차단)를 기술하는데, 이 이슈 완료 조건은 fail-open("서버 무응답·조회 실패에 앱이 잠기지 않아야 한다")을 요구한다. 배선 전에 어느 쪽인지 확정이 선행이다.
2. 배선 0건은 사실이다 (재확인)
src/test·src/androidTest포함 심볼명 전수 집계:AppVersionApiServicedi/ServiceModule.kt2 ·ApiWireContractSmokeTest2AppVersionDtoAppVersionContractTest4checkVersionApiWireContractSmokeTest1presentation·domain참조 0건,*/src/androidTest/*참조 0건. 본문 그대로다.3. 그런데 지금 배선하면 모든 사용자가 100% 앱에서 잠긴다 — 실서버 실측
그리고 이 앱의 실제
versionCode는 모든 빌드에서 1 이다.build-logic/src/main/kotlin/VersionCode.kt:4—DEFAULT_AFTERNOTE_VERSION_CODE = 1AFTERNOTE_VERSION_CODE를 주입하는 워크플로 0건.docs/release/distribution.md:181이 명시한다 — "일반 로컬·Firebase 빌드는 기본versionCode1을 유지한다. … 현재 값을 자동 산출·주입하거나 Play에 배포하는 workflow는 없다."즉
1 < 10001이 항상 참 → 존재하는 모든 빌드가updateRequired=true를 받는다. 관문을 붙이는 순간 개발·기획·QA 테스터 전원이 스플래시에서 잠기고, 보내지는 목적지는http://your-playstore-domain— 존재하지 않는 도메인이다.4. 보낼 곳 자체가 없다 — Play Store 등록이 아직 없다
docs/play-release.md:18배포 채널은 Firebase App Distribution 하나뿐이다. 게다가 같은 런북의 기본안대로면 Firebase APK 와 Play APK 는 설치 인증서가 달라 서로 위에 업데이트할 수 없다 — 강제 업데이트 관문이 테스터를 스토어로 보내도 인플레이스 업데이트가 원리적으로 불가능하고 삭제·재설치가 필요하다.
5. 시안 0건 (참고 — 이건 블로커가 아니다)
Figma 정본 페이지
정리 Screen Design(4327:43064) 의 11개 구역 전수 walk 결과 강제 업데이트 화면·다이얼로그 0건.업데이트텍스트 히트는 전부 약관 문구("애프터노트는 서비스 품질 향상을 위해 기능을 변경하거나 업데이트할 수 있습니다")뿐이고,playstore페이지(3309:16877)는 스토어 등록용 스크린샷이라 무관하다.다만 2026-08-23 디자이너 위임으로 시안 부재는 블로킹 근거가 아니다 — FE 재량으로 확정하면 된다. 진짜 블로커는 3·4번이다.
결론과 조치
AppVersionApiService·DI·DTO·계약 테스트는 그대로 둔다.blocked_by로 건다. Google Play 내부 테스트 트랙 배포 자동화 #852 범위가 선행 조건을 정확히 덮는다 — Play Console 등록·Play App Signing, 그리고 "versionCode가 모든 배포에서 단조 증가하도록 repository 정책을 정하고". Google Play 내부 테스트 트랙 배포 자동화 #852 자신도 완료 조건에 "Play Console 또는 계정 준비가 안 된 동안에는 이슈를 blocked 상태로 두되 credential을 임시로 우회하지 않는다" 를 두고 있다.배선 재개 시 착수 조건 (#852 이후 이 이슈에서 확인한다)
AFTERNOTE_VERSION_CODE가 실제 릴리스 versionCode 로 주입돼 서버latestVersionCode와 같은 축에서 비교 가능할 것storeUrl에 실 Play Store URL 을 들고 있을 것 — 현재 dev 실측값은http://your-playstore-domain플레이스홀더다(BEapplication.yml의APP_ANDROID_STORE_URL배포 환경변수가 미확정 값). 소비처가 0건인 지금은 무해하므로 별건으로 떼지 않고, 이 이슈 재개 시 재측정해 여전히 플레이스홀더면 그때 BE 와 확정한다.OnlyReadOneBook commentedon Aug 30, 2026
배선했다 — PR #1642.
blocked_by #852를 걷을지 판단을 구한다이 이슈의 2026-08-30 재판정(«걷지 않는다 · 지금 배선하지 않는다»)을 다시 실측했다. 두 결론 중 앞의 것은 그대로 유효하고, 뒤의 것은 관문 안에서 닫을 수 있었다.
유효한 것 — 걷지 않는다
BE 실코드 재확인.
origin/main8e605f4·origin/release8bfe43d양쪽에domain/appversion파일 14개가 있고,AppVersionController.java·AppVersionCheckResponse.java는 두 브랜치에서 바이트 동일이다. main↔release 129 커밋 차에 이 도메인은 걸리지 않는다 — 실서버에도 살아 있다.응답 스키마도 실코드로 확정했다(
updateRequired: boolean·latestVersionCode: int·storeUrl: String?). 기존AppVersionDto와 정확히 일치해 DTO 는 손대지 않았다.닫은 것 — 「지금 배선하면 전 사용자가 잠긴다」
그 판정은 맞다. 다만 원인 두 가지가 전부 «관문이 사용자를 어디로도 보낼 수 없는 상태» 이고, 그건 관문 자신이 런타임에 판정할 수 있다. 그래서
updateRequired=true를 받아도 아래 둘을 각각 확인한 뒤에만 막게 했다.storeUrl이http://your-playstore-domain플레이스홀더market://details?id=·https://play.google.com/store/apps/details?화이트리스트BuildConfig.STORE_DISTRIBUTED_BUILD— 릴리스 워크플로가 versionCode 를 주입한 산출물에서만 참오늘 존재하는 모든 빌드에서 아무도 잠기지 않는다. 오늘의 실서버 응답값(
updateRequired=true+ 플레이스홀더 URL)을 그대로 넣은 단위 테스트가 그것을 직접 단언한다. 그러면서 조회 자체는 매 기동마다 실제로 나가므로, 경계가 깨지면 Play 출시 때가 아니라 지금 텔레메트리에 드러난다.「배선 재개 시 착수 조건」 4개도 이 구조가 각각 흡수한다.
storeUrl플레이스홀더 — 실 URL 이 아닌 동안은 막지 않고,stage=force_update_store_urlnon-fatal 로 남는다. 재측정을 사람이 기억할 필요가 없다.@Operation문구(«API 호출 실패 시에도 스플래시 진입 차단»)와 상반되는 점은 코드 KDoc·PR 본문에 명시했다. 덧붙여 관문이 시작을 지연시키지도 않게 했다 — 스플래시를 붙잡지 않고 앱은 평소대로 뜬 뒤 판정이 오면 그 위에 팝업이 얹힌다. 타임아웃 값에 기대는 실패 경로 자체가 없다.구할 판단 —
blocked_by #852를 걷을 것인가위 근거로 이 코드는 #852 를 기다릴 이유가 없다고 본다. 다만 그 관계를 건 판단이 이 이슈 본인의 것이라 임의로 걷지 않았고, PR 본문도
Closes가 아니라Refs #1539로 두었다(Closes를 쓰면merge-order-guard가 red 가 된다).동의하면
blocked_by를 걷어 주면 PR 본문을Closes #1539로 올린다. 반대로 「관문 코드가 develop 에 있는 것 자체가 이르다」고 보면 PR 을 닫고 이 이슈는 지금 상태로 남긴다.OnlyReadOneBook commentedon Sep 2, 2026
#852 와의
blocked_by를 해제합니다. PR #1642 의 관문은storeUrl이 실 Play 주소이고STORE_DISTRIBUTED_BUILD가 참일 때만 막으므로, #852 가 세울 versionCode 주입·Play 등록은 이 코드의 선행 조건이 아니라 «켜지는 시점» 일 뿐입니다. 관계를 걸었던 전제(지금 배선하면 전원 잠금)가 코드로 사라졌습니다. #852 는 그대로 열려 있고 범위도 줄지 않았습니다. PR 은Closes #1539로 올립니다.test(app): 강제 업데이트 관문의 테스트 전용 노출을 닫는다 (#1539)
merge: preserve force-update gate with develop visibility contracts (#…
chore(release): Firebase 배포에 대역 안 versionCode 를 주입하고 Play 대역 하한을 적용한다
chore(release): Firebase 배포에 대역 안 versionCode 를 주입하고 Play 대역 하한을 적용한다
chore(release): Firebase 배포에 대역 안 versionCode 를 주입하고 Play 대역 하한을 적용한다