Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 경계 안에서 단일 출처를 갖도록 했습니다.
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)을 따릅니다.
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 테스트, 매크로벤치마크 |
Expand Down Expand Up @@ -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 기반 변경과 검증 결과를 정리합니다.
Expand All @@ -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 문서와 코드입니다.
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ android {

defaultConfig {
applicationId = "com.gasstation"
versionCode = 6
versionName = "1.1.2"
versionCode = 7
versionName = "1.1.3"
}

productFlavors {
Expand Down
3 changes: 2 additions & 1 deletion docs/agent-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`를 따른다.

Expand All @@ -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/`

Expand Down
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | 저장한 주유소 비교 화면 렌더링 |
Expand All @@ -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 측정 |

## 의존성 해석 기준

Expand Down
68 changes: 68 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -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=<issued-key>`로 전달하고 저장소에 커밋하지 않습니다.

## 릴리스 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)를 따릅니다.
2 changes: 1 addition & 1 deletion docs/module-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | 기능 구현 |

## 경계가 헷갈릴 때 보는 기준

Expand Down
4 changes: 2 additions & 2 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

Expand Down
49 changes: 49 additions & 0 deletions docs/release-notes/2026-05-18-v1.1.3.md
Original file line number Diff line number Diff line change
@@ -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
```
Loading
Loading