diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4e8079..724cdc8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## Unreleased +릴리스 후 다음 변경 사항을 기록합니다. + +## 1.1.3 - 2026-05-18 + ### 개발자 영향 - Hero benchmark evidence: station-list first usable content 기준으로 `reportFullyDrawn()`을 연결하고, startup/list scroll/refresh/watchlist macrobenchmark 경로를 분리합니다. 판단은 `feature:station-list`의 순수 정책 `StationListFirstContentPolicy`로, 보고는 `app`의 `StartupDrawReporter` Compose 훅으로 책임을 나눠 측정 기준이 feature 경계 안에서 단일 출처를 갖도록 했습니다. @@ -18,9 +22,11 @@ - `:app`에 `benchmark` build type(`isProfileable=true`, `isDebuggable=false`, debug 키 서명)을 추가해 minified APK를 macrobenchmark가 추적할 수 있게 하고, `:benchmark` 매크로벤치마크 소스를 `com.android.test` 표준 위치인 `src/main/kotlin`으로 이동했습니다(이전 `src/androidTest/`는 컴파일 대상이 아님). - `benchmark` macrobenchmark의 UiAutomator 대기 한도(`WAIT_TIMEOUT_MS`)를 5초에서 10초로 늘려 실기기 selector 안정성을 확보합니다. - `docs/performance.md`에 `demoBenchmark` vs `demoDebug` APK 사이즈(2.51 MB / 22.70 MB) 비교를 추가해 R8 minify 효과를 단일 출처로 기록합니다. +- `docs/deployment.md`를 추가해 release branch, 버전 bump, PR/CI, tag push, prodRelease 산출물, signing/secret 경계를 한 곳에서 확인할 수 있게 했습니다. - `feature:station-list`의 `WatchToggleButton`(`StationListCards.kt`)과 station-list 새로고침 `IconButton`(`StationListScreen.kt`)이 자식 `Icon` 대신 부모 `Modifier.semantics`에 직접 `contentDescription`을 부여하도록 정리했습니다. UX/시각 변화는 없으며 접근성 트리에서 단일 노드로 노출되어 기존 bookmark `IconButton`과 동일한 패턴이 됩니다. - `benchmark` 매크로벤치마크의 `openWatchlistWithSavedStation`이 `waitForObject` → `click` 사이 Compose recomposition으로 `UiObject2`가 stale이 될 때 selector를 재해석하도록 `clickStable` retry 헬퍼를 도입했습니다. - 알려진 제약: 위 변경 후에도 `BaselineProfileGenerator`와 `openWatchlistFrameTiming`은 macrobenchmark phase / 디바이스 상태 상호작용으로 인해 실기기에서 일관되지 않은 selector 매치를 보이며 측정 표본을 수집하지 못합니다. baseline profile은 아직 설치하지 않은 상태로 측정합니다. 시도한 3가지 변경과 남은 조사 후보는 `docs/performance.md` "Known Limitations"를 참고하세요. +- 상세 릴리즈 노트는 [docs/release-notes/2026-05-18-v1.1.3.md](docs/release-notes/2026-05-18-v1.1.3.md)를 봅니다. ## 1.1.2 - 2026-05-14 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7fc02f95..e26a1b4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,10 @@ GasStation은 한국 운전자가 현재 위치 기반으로 가까운 주유소 명령 변경/확장 시 `docs/verification-matrix.md`를 먼저 갱신한 뒤 위 블록을 같이 동기화합니다. +## 릴리스와 배포 + +새 버전 발행은 [`docs/deployment.md`](docs/deployment.md)를 따릅니다. 릴리스 PR은 `app/build.gradle.kts`의 `versionCode`/`versionName`, `CHANGELOG.md`, `README.md`, `docs/release-notes/`를 함께 갱신하고, merge 후 `vX.Y.Z` 태그 push로 GitHub Actions의 release 성격 검증을 실행합니다. + ## 커밋 메시지 [Conventional Commits](https://www.conventionalcommits.org/)을 따릅니다. diff --git a/README.md b/README.md index 401c1f89..c00f0a71 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ | 사용자 플로우 | 현재 위치 조회 -> 목록 확인 -> 북마크 저장 -> watchlist 비교 -> 외부 지도 열기 | | 구조 | `app / feature / domain / data / core / tools / benchmark` 멀티모듈 | | 런타임 | 재현 가능한 `demo`, 실제 Opinet Open API 키 기반 `prod` | -| 현재 앱 버전 | `1.1.2` (`versionCode` 6) | +| 현재 앱 버전 | `1.1.3` (`versionCode` 7) | | 저장 | `station_cache`, `station_cache_snapshot`, `station_price_history`, `watched_station` | | 데이터 | `prod`는 실시간 Opinet API 응답, `demo`는 승인된 seed JSON 자산 | | 검증 | 단위 테스트, Compose/Robolectric, 기기 UI 테스트, 매크로벤치마크 | @@ -146,6 +146,9 @@ seed 생성과 `prod` 런타임 검색은 모두 `opinet.apikey`만 사용합니 ## 릴리즈 - [CHANGELOG](CHANGELOG.md): 버전별 주요 변경 사항을 요약합니다. +- [배포 절차](docs/deployment.md): release branch, 검증, tag push, prodRelease 산출물, signing/secret 경계를 정리합니다. +- [Unreleased](CHANGELOG.md#unreleased): v1.1.3 이후 변경 사항을 추적합니다. +- [1.1.3 릴리즈 노트](docs/release-notes/2026-05-18-v1.1.3.md): hero benchmark evidence, first usable content startup reporting, backend proxy ADR, physical-device performance snapshot, 배포 절차 문서화를 정리합니다. - [1.1.2 릴리즈 노트](docs/release-notes/2026-05-14-v1.1.2.md): build/test 속도 개선, CI 메모리 안정화, 검증 경로 분리를 정리합니다. - [1.1.1 릴리즈 노트](docs/release-notes/2026-05-13-v1.1.1.md): clean architecture remediation, observability 경계, station-list/data 분리, CI scope 조정을 정리합니다. - [1.1.0 릴리즈 노트](docs/release-notes/2026-05-11-v1.1.0.md): production baseline, CI, i18n, screenshot regression, coverage 기반 변경과 검증 결과를 정리합니다. @@ -165,6 +168,9 @@ seed 생성과 `prod` 런타임 검색은 모두 `opinet.apikey`만 사용합니 - [오프라인 전략](docs/offline-strategy.md): 캐시 스냅샷, stale 판정, refresh 실패, watchlist fallback을 다룹니다. - [테스트 전략](docs/test-strategy.md): 어떤 층을 어떤 테스트로 검증하는지 설명합니다. - [검증 매트릭스](docs/verification-matrix.md): 실제로 어떤 Gradle 명령을 돌리면 되는지 정리합니다. +- [배포 절차](docs/deployment.md): 릴리스 준비, GitHub PR/tag 흐름, Android release 산출물과 공개 배포 전 보안 gate를 설명합니다. +- [성능](docs/performance.md): hero macrobenchmark 정의, 실기기 측정값, baseline profile 경로와 제약을 정리합니다. +- [Backend proxy ADR](docs/adr/2026-05-18-backend-proxy-escalation.md): Opinet API key를 backend proxy로 승격해야 하는 조건을 기록합니다. - [심층 분석 리포트](docs/history/deep-analysis-report.md): 완료된 필수 수정과 조건부 승격 항목을 요약합니다. - [개선 분석](docs/history/improvement-analysis.md): 완료된 backlog 항목과 남은 개선 후보의 기준을 보관합니다. - `docs/superpowers/specs/`, `docs/superpowers/plans/`: 완료되었거나 진행했던 설계/구현 계획의 이력을 보관합니다. 현재 구조와 실행 명령의 기준은 위 live 문서와 코드입니다. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2d3c7797..02d3e550 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { defaultConfig { applicationId = "com.gasstation" - versionCode = 6 - versionName = "1.1.2" + versionCode = 7 + versionName = "1.1.3" } productFlavors { diff --git a/docs/agent-workflow.md b/docs/agent-workflow.md index bcdcd058..6a7dcc02 100644 --- a/docs/agent-workflow.md +++ b/docs/agent-workflow.md @@ -191,7 +191,7 @@ watchlist는 현재 목록의 복제 화면이 아니라 저장 항목 비교 - 새로 추가하거나 반복 setup을 정리하는 coroutine ViewModel test: `Dispatchers.Main`이 필요하면 feature-local JUnit rule/helper를 우선하고, station-list는 `MainDispatcherRule` 계약을 따른다. - app 조립/flavor/startup: `app:testDemoDebugUnitTest`, `app:testProdDebugUnitTest` - demo 실제 플로우: `app:connectedDemoDebugAndroidTest` -- benchmark: `benchmark:assemble` 또는 `benchmark:connectedDebugAndroidTest` +- benchmark: `benchmark:assemble` 또는 실기기 evidence 수집용 `benchmark:connectedBenchmarkAndroidTest` 정확한 명령 조합은 `docs/verification-matrix.md`를 따른다. @@ -203,6 +203,7 @@ watchlist는 현재 목록의 복제 화면이 아니라 저장 항목 비교 - 상태 원천이나 lifecycle이 바뀌면 `docs/state-model.md` - 캐시, stale, refresh 실패, watchlist fallback이 바뀌면 `docs/offline-strategy.md` - 테스트 의미나 명령이 바뀌면 `docs/test-strategy.md`와 `docs/verification-matrix.md` +- 성능 측정, benchmark journey, baseline profile 경로가 바뀌면 `docs/performance.md`와 `docs/verification-matrix.md` - README가 설명하는 대표 사용자 흐름이 바뀌면 `README.md` - 일회성 기능 설계나 구현 계획은 `docs/superpowers/specs/`와 `docs/superpowers/plans/` diff --git a/docs/architecture.md b/docs/architecture.md index 0c5d8049..338b7896 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -73,7 +73,7 @@ flowchart LR | 모듈 | 책임 | | --- | --- | -| `app` | Hilt 조립, startup hook 실행, navigation, flavor별 바인딩, 외부 지도 런처 연결, Logcat 기반 이벤트 로거 연결, flavor별 `CrashReporter` 구현(NoOp/Logcat) Hilt 바인딩 | +| `app` | Hilt 조립, startup hook 실행, navigation, flavor별 바인딩, first-content startup reporting bridge, 외부 지도 런처 연결, Logcat 기반 이벤트 로거 연결, flavor별 `CrashReporter` 구현(NoOp/Logcat) Hilt 바인딩 | | `feature:station-list` | 권한/GPS/위치/새로고침을 포함한 목록 화면 상태와 effect 처리 | | `feature:settings` | 설정 요약 목록과 상세 선택 화면 렌더링, 같은 `SettingsViewModel` 공유 | | `feature:watchlist` | 저장한 주유소 비교 화면 렌더링 | @@ -90,7 +90,7 @@ flowchart LR | `core:database` | Room DB, DAO, migration | | `core:datastore` | storage-local `StoredUserPreferences` DataStore와 커스텀 serializer. 선호값은 primitive/string enum name으로 저장 | | `tools:demo-seed` | Opinet 결과를 기준으로 demo seed JSON을 다시 생성하는 JVM CLI | -| `benchmark` | `demo` 경로를 대상으로 cold start, watchlist 이동, baseline profile 측정 | +| `benchmark` | `demo` 경로를 대상으로 startup-to-first-content, list scroll, refresh, watchlist 진입 macrobenchmark와 baseline profile journey 측정 | ## 의존성 해석 기준 diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 00000000..4f4d337b --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,68 @@ +# 배포 절차 + +이 문서는 GasStation 새 버전을 발행할 때 확인할 배포 흐름의 단일 출처입니다. `docs/verification-matrix.md`는 검증 명령을, `CHANGELOG.md`와 `docs/release-notes/`는 버전별 변경 설명을 소유합니다. + +## 현재 배포 경계 + +- 공식 실행 경로는 `demo`와 `prod`입니다. 둘 다 release 전에 빌드 가능해야 합니다. +- GitHub Actions는 PR에서 static analysis, unit tests, screenshot tests, debug assemble을 실행하고, `main`/`v*` tag push에서 `release-assemble`과 coverage를 추가 실행합니다. +- 저장소에는 Play Store 자동 배포, signing keystore, 배포 credential을 두지 않습니다. +- `prodRelease` APK/AAB signing은 저장소 밖의 keystore와 배포자 계정에서 처리합니다. +- `prod` 런타임에는 사용자 로컬 `opinet.apikey`가 필요합니다. 키는 `~/.gradle/gradle.properties` 또는 `-Popinet.apikey=`로 전달하고 저장소에 커밋하지 않습니다. + +## 릴리스 PR 준비 + +1. `main`에서 새 release branch를 만듭니다. +2. `app/build.gradle.kts`의 `versionCode`를 1 올리고 `versionName`을 다음 버전으로 갱신합니다. +3. `CHANGELOG.md`의 `Unreleased` 항목을 새 버전 섹션으로 이동하고, 새 `Unreleased`는 비워 둡니다. +4. `docs/release-notes/YYYY-MM-DD-vX.Y.Z.md`를 작성합니다. +5. `README.md`의 현재 앱 버전과 릴리즈 인덱스를 갱신합니다. +6. 배포/검증 절차가 바뀌면 이 문서와 `docs/verification-matrix.md`를 같은 PR에서 갱신합니다. + +## 필수 검증 + +문서와 버전 메타데이터를 갱신한 릴리스 PR의 최소 확인입니다. + +```bash +git diff --check -- README.md CHANGELOG.md CONTRIBUTING.md app/build.gradle.kts docs/deployment.md docs/verification-matrix.md docs/release-notes/*.md +./gradlew :app:assembleDemoDebug :app:assembleProdDebug :benchmark:assemble +./gradlew :app:assembleProdRelease +``` + +릴리스 내용이 앱 동작, cache, 위치, watchlist, startup hook, benchmark 기준을 바꾸면 `docs/verification-matrix.md`의 머지 전 권장 회귀 세트까지 확장합니다. + +## Merge 후 tag + +PR이 merge된 뒤 `main`에서 태그를 만들고 push합니다. + +```bash +git switch main +git pull --ff-only +git tag vX.Y.Z +git push origin vX.Y.Z +``` + +`v*` tag push는 GitHub Actions에서 PR 범위 검증에 더해 `:app:assembleProdRelease`와 `koverXmlReport`를 실행합니다. 태그 push 자체가 Play Store 업로드를 수행하지는 않습니다. + +## Android 산출물 + +로컬 release APK 확인: + +```bash +./gradlew :app:assembleProdRelease +ls -l app/build/outputs/apk/prod/release/ +``` + +현재 Gradle 설정은 release build에서 R8 minification을 켭니다. 공개 배포용 signed artifact가 필요하면 저장소 밖 keystore로 Android Studio 또는 별도 release job에서 서명합니다. keystore, store password, key password, service account JSON은 저장소에 두지 않습니다. + +## 공개 배포 전 보안 gate + +현재 `prod`는 Opinet API key를 Android 클라이언트 `BuildConfig`로 주입합니다. 이 방식은 reference/portfolio 범위에서는 단순하고 재현 가능하지만, APK에서 키를 완전히 숨기는 secret boundary가 아닙니다. + +아래 조건 중 하나라도 참이면 release 전에 backend proxy 승격을 먼저 설계합니다. + +- 공개 배포로 active install이나 API quota 비용이 의미 있게 커집니다. +- 키 abuse, quota exhaustion, key rotation 운영 요구가 생깁니다. +- 민감 데이터 엔드포인트나 사용자 식별 흐름이 추가됩니다. + +승격 기준과 Android 영향 범위는 [`docs/security-trade-offs.md`](security-trade-offs.md)와 [`docs/adr/2026-05-18-backend-proxy-escalation.md`](adr/2026-05-18-backend-proxy-escalation.md)를 따릅니다. diff --git a/docs/module-contracts.md b/docs/module-contracts.md index 59317d6d..5a0658ae 100644 --- a/docs/module-contracts.md +++ b/docs/module-contracts.md @@ -31,7 +31,7 @@ | `core:database` | Room DB, DAO, migration | Room | 도메인 정책 | | `core:datastore` | DataStore data source, serializer, storage-local settings DTO | Android DataStore | 화면 상태, 설정 정책, domain model | | `tools:demo-seed` | demo seed 재생성 CLI | `core:network`, `domain:station`, `core:model` | 앱 런타임 의존 | -| `benchmark` | 매크로벤치마크와 baseline profile | `app` | 기능 구현 | +| `benchmark` | `demo` hero macrobenchmark, baseline profile journey, physical-device performance evidence | `app` | 기능 구현 | ## 경계가 헷갈릴 때 보는 기준 diff --git a/docs/performance.md b/docs/performance.md index 909902a8..db54535c 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -50,7 +50,7 @@ The baseline profile generator covers: - Station-list scroll - Watchlist entry after saving a station -The generator and its companion `openWatchlistFrameTiming` benchmark depend on a station being saved and the watchlist card (`content-description = "관심 주유소 카드"`) appearing within 5 seconds. See Known Limitations for the current status of those two scenarios. +The generator and its companion `openWatchlistFrameTiming` benchmark depend on a station being saved and the watchlist card (`content-description = "관심 주유소 카드"`) appearing within 10 seconds. See Known Limitations for the current status of those two scenarios. ## Commands @@ -75,7 +75,7 @@ The `:app` `benchmark` build type forks `release` with `isDebuggable=false`, `is - Made the `WatchToggleButton` and station-list refresh `IconButton` declare `contentDescription` directly on the parent semantics node so UiAutomator does not depend on Compose `mergeDescendants` behavior (kept — also an accessibility improvement; `feature/station-list/src/main/kotlin/com/gasstation/feature/stationlist/StationListCards.kt`, `StationListScreen.kt`). - Added a `clickStable()` retry wrapper that re-resolves the `UiObject2` on `StaleObjectException` between `waitForObject` and `click` (kept — `GasStationBenchmarkActions.kt`). After these changes one run did push `BaselineProfileGenerator` further (selector match succeeded, `StaleObjectException` surfaced inside the click path instead of a `waitForObject` timeout). On the very next run the same two scenarios reverted to first-selector timeout. The same `refreshStationList()` call passes in `StationListBenchmark.refreshFrameTiming` (measure block) but fails in `StationListBenchmark.openWatchlistFrameTiming` setupBlock, which points at a macrobenchmark phase / device-state interaction we did not isolate. Further investigation candidates: (a) drive watchlist entry through an explicit `Intent` instead of UI traversal (requires production code to expose an internal deep link), (b) replace the demo-data dependency with a fixture that guarantees a single bookmarkable card under stable on-screen coordinates, (c) re-run on a Compose 1.7+ / AGP 9.2 line to see if `mergeDescendants` + macrobenchmark scope interplay has been fixed upstream. -- **Cooling and thermal state not enforced.** macrobenchmark warned about `SUSTAINED_PERFORMANCE_MODE` being unavailable; results below are the median over 10 startup iterations and 5 frame iterations, which mitigates but does not eliminate device-side thermal variance. Re-run on a cooled device before committing future numbers if comparisons span multiple firmware revisions. +- **Cooling and thermal state not enforced.** macrobenchmark warned about `SUSTAINED_PERFORMANCE_MODE` being unavailable; results above are the median over 10 startup iterations and 5 frame iterations, which mitigates but does not eliminate device-side thermal variance. Re-run on a cooled device before committing future numbers if comparisons span multiple firmware revisions. ## APK Size (demo flavor) diff --git a/docs/release-notes/2026-05-18-v1.1.3.md b/docs/release-notes/2026-05-18-v1.1.3.md new file mode 100644 index 00000000..b35c8bea --- /dev/null +++ b/docs/release-notes/2026-05-18-v1.1.3.md @@ -0,0 +1,49 @@ +# Release Notes - v1.1.3 (2026-05-18) + +이 문서는 v1.1.3에서 실제로 적용된 변경을 기록합니다. + +## 사용자 관점 변경 + +가까운 주유소 비교, stale cache fallback, watchlist 비교, 외부 지도 handoff 흐름은 v1.1.2와 같습니다. 이번 릴리스는 사용자가 보는 정보 위계를 유지하면서 startup reporting, benchmark evidence, release/deployment 문서화를 보강한 patch 릴리스입니다. + +## 성능과 측정 + +- station-list의 first usable content 기준으로 `reportFullyDrawn()` 보고가 연결되었습니다. +- startup, list scroll, refresh, watchlist, baseline profile journey가 macrobenchmark 경로로 분리되어 각 흐름을 독립적으로 확인할 수 있습니다. +- README와 `docs/performance.md`에 Samsung Galaxy S20+ 5G (Android 13 / API 33), `demoBenchmark` variant 기준 physical-device 측정값을 게시했습니다. +- `demoBenchmark`와 `demoDebug` APK 크기를 문서화해 R8 minify 효과를 확인할 수 있게 했습니다. + +## 개발자 품질 기반 + +- `:app`에 `androidx.profileinstaller`를 포함해 생성된 baseline profile이 설치된 APK에서 적용될 수 있는 런타임 경로를 준비했습니다. +- `benchmark` build type은 release 기반 minified/profileable APK를 debug signing으로 설치할 수 있게 유지합니다. +- benchmark source set은 `com.android.test` 모듈의 표준 `src/main/kotlin` 경로를 사용합니다. +- watchlist benchmark helper는 Compose recomposition 사이 stale `UiObject2`를 다시 찾는 retry 흐름을 사용합니다. + +## 배포와 보안 운영 + +- `docs/deployment.md`를 추가해 release branch, 버전 bump, PR/CI, tag push, `prodRelease` 산출물, signing/secret 경계를 정리했습니다. +- Opinet API key를 backend proxy로 승격해야 하는 조건과 Android 영향 범위를 ADR로 기록했습니다. +- 공개 배포, quota 비용, key abuse 리스크가 커지면 release 전에 backend proxy 설계를 먼저 진행합니다. + +## 알려진 제약 + +- `BaselineProfileGenerator`와 `openWatchlistFrameTiming`은 macrobenchmark phase / 디바이스 상태 상호작용으로 인해 실기기에서 일관되지 않은 selector 매치를 보이며 측정 표본을 수집하지 못했습니다. +- 이번 측정값은 baseline profile을 아직 설치하지 않은 상태의 수치입니다. +- 자세한 시도 내역과 남은 조사 후보는 `docs/performance.md`의 Known Limitations를 따릅니다. + +## 버전 + +| 항목 | 값 | +| --- | --- | +| `versionName` | `1.1.3` | +| `versionCode` | `7` | +| 릴리즈 태그 | `v1.1.3` | + +## 검증 + +```bash +git diff --check -- README.md CHANGELOG.md CONTRIBUTING.md app/build.gradle.kts docs/deployment.md docs/verification-matrix.md docs/release-notes/*.md +./gradlew :app:assembleDemoDebug :app:assembleProdDebug :benchmark:assemble +./gradlew :app:assembleProdRelease +``` diff --git a/docs/test-strategy.md b/docs/test-strategy.md index a4355326..efe7357e 100644 --- a/docs/test-strategy.md +++ b/docs/test-strategy.md @@ -31,7 +31,7 @@ | `feature:watchlist` | `WatchlistViewModelTest`, `WatchlistScreenTest`, `WatchlistItemUiModelTest` | 북마크 비교 화면 상태와 표시, `CompareViewed` event, brand label 보존, metric column alignment, 긴 저장 항목/큰 가격 clipping 방지 | | `app` | `AppStartupGraphTest`, `AppStartupRunnerTest`, `ExternalMapLauncherTest`, `SplashThemeResourceTest`, `AppIconResourceTest`, `NetworkSecurityConfigResourceTest`, `BackupPolicyResourceTest`, `ProdSecretsStartupHookTest` | startup hook 바인딩, prod key fail-fast, 앱 리소스, Opinet-only cleartext config, Android backup 비활성화, 외부 지도 인텐트 | | `demo` 전용 앱 경로 | `DemoSeedStartupHookTest`, `DemoSeedAssetLoaderTest`, `DemoLocationHookIntegrationTest`, `StationPortfolioFlowTest` | seed 적재, 고정 위치, 실제 북마크 플로우 | -| `benchmark` | `StationListBenchmark`, `BaselineProfileGenerator` | cold start, watchlist 이동, baseline profile | +| `benchmark` | `StationListBenchmark`, `BaselineProfileGenerator`, `GasStationBenchmarkActions` | startup-to-first-content, list scroll, refresh, watchlist 진입, baseline profile journey | | `tools:demo-seed` | `DemoSeedGeneratorTest` | seed 생성기와 질의 매트릭스 | ## flavor별 관점 @@ -81,6 +81,8 @@ 사용자 설정의 지도 앱 선택이 실제 외부 인텐트와 맞아야 합니다. - First usable content policy Startup metric은 첫 frame이 아니라 사용 가능한 목록/empty/failure content 기준으로 보고합니다. `StationListFirstContentPolicy`와 `StartupDrawReporter` 테스트가 이 기준을 보호합니다. +- Hero benchmark source set + `benchmark`는 `com.android.test` 모듈의 main source set(`benchmark/src/main/kotlin`)에서 scenario와 baseline profile generator를 컴파일합니다. 실기기 증거 수집은 `connectedBenchmarkAndroidTest` 경로가 단일 기준입니다. ## 의도적으로 약하게 보는 것 diff --git a/docs/verification-matrix.md b/docs/verification-matrix.md index 032d59c3..31a9e73e 100644 --- a/docs/verification-matrix.md +++ b/docs/verification-matrix.md @@ -13,7 +13,7 @@ 코드를 바꾸지 않고 architecture, state, offline, module contract 문서를 갱신했을 때 최소 확인입니다. ```bash -git diff --check -- README.md AGENTS.md .impeccable.md CHANGELOG.md CONTRIBUTING.md docs/agent-workflow.md docs/project-reading-guide.md docs/architecture.md docs/state-model.md docs/offline-strategy.md docs/test-strategy.md docs/verification-matrix.md docs/module-contracts.md docs/security-trade-offs.md docs/release-notes/*.md +git diff --check -- README.md AGENTS.md .impeccable.md CHANGELOG.md CONTRIBUTING.md docs/agent-workflow.md docs/project-reading-guide.md docs/architecture.md docs/state-model.md docs/offline-strategy.md docs/test-strategy.md docs/verification-matrix.md docs/module-contracts.md docs/security-trade-offs.md docs/performance.md docs/deployment.md docs/adr/*.md docs/release-notes/*.md ``` `docs/superpowers/specs/`와 `docs/superpowers/plans/`는 과거 설계/계획 이력이므로 current contract 확인 명령에는 기본 포함하지 않습니다. 해당 이력 문서를 직접 수정했다면 수정한 파일 경로를 위 명령에 명시적으로 추가합니다. @@ -117,6 +117,18 @@ GitHub Actions는 PR 피드백 시간을 줄이기 위해 PR과 release 성격 `prodRelease` assemble과 coverage는 기본 PR matrix에 포함하지 않습니다. R8/minify 회귀나 coverage report가 PR마다 필요하다고 판단하면, 이 문서와 `.github/workflows/android.yml`을 같은 변경에서 갱신합니다. `assemble` job은 GitHub runner의 메모리 피크를 낮추기 위해 demo debug, prod debug, benchmark assemble을 별도 Gradle 호출로 실행합니다. +## 릴리스/배포 확인 + +새 버전을 발행할 때는 [`docs/deployment.md`](deployment.md)의 절차를 따른 뒤 아래 명령을 최소 확인으로 사용합니다. + +```bash +git diff --check -- README.md CHANGELOG.md CONTRIBUTING.md app/build.gradle.kts docs/deployment.md docs/verification-matrix.md docs/release-notes/*.md +./gradlew :app:assembleDemoDebug :app:assembleProdDebug :benchmark:assemble +./gradlew :app:assembleProdRelease +``` + +physical-device 성능 수치를 갱신하는 릴리스라면 "Hero Benchmark Evidence" 명령을 추가로 실행하고 `docs/performance.md`와 해당 릴리즈 노트에 기기/variant/측정일을 남깁니다. + ## 기기 기반 UI 확인 demo 실제 흐름을 기기나 에뮬레이터에서 확인합니다. @@ -145,14 +157,17 @@ API 33+ Geocoder callback path를 실제 기기나 에뮬레이터에서 확인 매크로벤치마크와 baseline profile 수집이 필요할 때 사용합니다. ```bash -./gradlew :benchmark:connectedDebugAndroidTest +./gradlew :app:assembleDemoBenchmark :benchmark:assembleBenchmark +ANDROID_SERIAL= ./gradlew :benchmark:connectedBenchmarkAndroidTest ``` 현재 benchmark는 다음 흐름을 기준으로 합니다. -- cold start -- watchlist 열기 -- baseline profile 수집 시 새로고침과 watchlist 진입 +- startup to first station-list content +- station-list scroll +- seeded refresh +- station save 후 watchlist 진입 +- baseline profile 수집 시 startup, refresh, scroll, watchlist 진입 ## Hero Benchmark Evidence diff --git a/feature/station-list/src/main/kotlin/com/gasstation/feature/stationlist/StationListQuerySummary.kt b/feature/station-list/src/main/kotlin/com/gasstation/feature/stationlist/StationListQuerySummary.kt index c168c35c..f00ee2c5 100644 --- a/feature/station-list/src/main/kotlin/com/gasstation/feature/stationlist/StationListQuerySummary.kt +++ b/feature/station-list/src/main/kotlin/com/gasstation/feature/stationlist/StationListQuerySummary.kt @@ -2,10 +2,16 @@ package com.gasstation.feature.stationlist import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.MyLocation +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -18,11 +24,13 @@ import com.gasstation.core.model.FuelType import com.gasstation.core.model.SearchRadius internal const val STATION_LIST_QUERY_CONTEXT_TAG = "station-list-query-context" +internal const val STATION_LIST_QUERY_CONTEXT_LOCATION_ICON_TAG = "station-list-query-context-location-icon" @Composable internal fun QueryContextSummary(uiState: StationListUiState, modifier: Modifier = Modifier) { val spacing = GasStationTheme.spacing val typography = GasStationTheme.typography + val iconSize = GasStationTheme.iconSize val addressLabel = uiState.currentAddressLabel ?.trim() ?.takeIf(String::isNotEmpty) @@ -43,13 +51,28 @@ internal fun QueryContextSummary(uiState: StationListUiState, modifier: Modifier verticalArrangement = Arrangement.spacedBy(spacing.space4), ) { if (addressLabel != null) { - Text( - text = addressLabel, - style = typography.body.copy(fontWeight = FontWeight.Bold), - color = ColorBlack, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing.space4), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Rounded.MyLocation, + contentDescription = null, + tint = ColorGray2, + modifier = Modifier + .size(iconSize.status) + .testTag(STATION_LIST_QUERY_CONTEXT_LOCATION_ICON_TAG), + ) + Text( + text = addressLabel, + style = typography.body.copy(fontWeight = FontWeight.Bold), + color = ColorBlack, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } } Text( text = conditionLabel, diff --git a/feature/station-list/src/test/kotlin/com/gasstation/feature/stationlist/StationListScreenTest.kt b/feature/station-list/src/test/kotlin/com/gasstation/feature/stationlist/StationListScreenTest.kt index 2f8c321e..7820df91 100644 --- a/feature/station-list/src/test/kotlin/com/gasstation/feature/stationlist/StationListScreenTest.kt +++ b/feature/station-list/src/test/kotlin/com/gasstation/feature/stationlist/StationListScreenTest.kt @@ -53,6 +53,7 @@ class StationListScreenTest { } composeRule.onNodeWithTag(STATION_LIST_QUERY_CONTEXT_TAG).assertExists() + composeRule.onNodeWithTag(STATION_LIST_QUERY_CONTEXT_LOCATION_ICON_TAG).assertExists() composeRule.onNodeWithText("서울 영등포구 당산동").assertExists() composeRule.onNodeWithText("3km · 휘발유 기준").assertExists() composeRule.onNodeWithText("현재 조건").assertDoesNotExist() @@ -99,6 +100,7 @@ class StationListScreenTest { } composeRule.onNodeWithTag(STATION_LIST_QUERY_CONTEXT_TAG).assertExists() + composeRule.onNodeWithTag(STATION_LIST_QUERY_CONTEXT_LOCATION_ICON_TAG).assertDoesNotExist() composeRule.onNodeWithText("3km · 휘발유 기준").assertExists() composeRule.onNodeWithText("현재 조건").assertDoesNotExist() composeRule.onNodeWithText("반경과 유종 기준으로 정렬합니다.").assertDoesNotExist() diff --git a/feature/station-list/src/test/snapshots/loading-with-cache.png b/feature/station-list/src/test/snapshots/loading-with-cache.png index 5af9508a..9a9fb2eb 100644 Binary files a/feature/station-list/src/test/snapshots/loading-with-cache.png and b/feature/station-list/src/test/snapshots/loading-with-cache.png differ diff --git a/feature/station-list/src/test/snapshots/stale.png b/feature/station-list/src/test/snapshots/stale.png index a5fb36b6..01d448ae 100644 Binary files a/feature/station-list/src/test/snapshots/stale.png and b/feature/station-list/src/test/snapshots/stale.png differ