Skip to content

beyondwin/GasStation

Repository files navigation

주유주유소 (GasStation)

CI License: MIT Kotlin Compose BOM minSdk

GasStation is a Korean Android app that helps drivers compare nearby gas stations by current location, price, distance, brand, fuel type, and watchlist state, then hands off to the user's preferred external map for turn-by-turn navigation. The codebase ships an 18-module Clean Architecture setup with Jetpack Compose, Hilt, Room, and a deterministic demo flavor that mirrors the real Opinet API path.


주유주유소는 Jetpack Compose, Hilt, Coroutines, Flow, Room, ViewModel, Material Design, MVVM 아키텍처를 활용해 현재 위치 기반 주유소 탐색부터 stale 캐시 fallback, watchlist(북마크) 비교, 외부 지도 연동까지 하나의 흐름으로 구현한 멀티모듈 Android 프로젝트입니다. demo는 재현 가능한 고정 실행 경로를, prod는 실제 Opinet Open API 연동 경로를 제공합니다.

미리보기

prod 기준 주요 화면입니다.

주유주유소 가까운 주유소 목록 화면 주유주유소 한 번의 터치 길 안내 화면 주유주유소 찾기 설정 화면

빠른 요약

항목 내용
사용자 플로우 현재 위치 조회 -> 목록 확인 -> 북마크 저장 -> watchlist 비교 -> 외부 지도 열기
구조 app / feature / domain / data / core / tools / benchmark 멀티모듈
런타임 재현 가능한 demo, 실제 Opinet Open API 키 기반 prod
현재 앱 버전 1.1.3 (versionCode 7)
저장 station_cache, station_cache_snapshot, station_price_history, watched_station
데이터 prod는 실시간 Opinet API 응답, demo는 승인된 seed JSON 자산
검증 단위 테스트, Compose/Robolectric, 기기 UI 테스트, 매크로벤치마크

이 저장소가 보여주는 것

  • app은 조립만 담당하고, 화면 상태는 feature, 계약은 domain, 저장소 구현은 data, 공유 인프라는 core에 둡니다.
  • 위치 경계는 domain:location 계약과 core:location 구현으로 나눠, feature:station-list가 Android 위치 인프라를 직접 알지 않게 유지합니다.
  • 현재 위치 주소는 지오코더가 반환한 전체 주소를 그대로 노출하지 않고, domain:location의 순수 정규화 규칙으로 행정동 단위의 짧은 라벨을 만들어 목록 상단에 표시합니다.
  • station_cache_snapshotStationSearchResult.hasCachedSnapshot으로 "성공한 빈 결과"와 "캐시 자체가 없음"을 구분합니다.
  • 목록은 stale 결과를 유지하고, 일시적 Timeout/Network 실패는 data:station에서 1회 재시도합니다. 성공한 refresh는 7일 보관 기준으로 오래된 캐시를 정리하고, watchlist는 최신 캐시가 없어도 저장 항목과 가격 히스토리로 비교 화면을 복원합니다.
  • StationListViewModel은 최종 UI state/effect 조합에 집중하고, 위치 상태는 LocationStateMachine, query/cache/failure 판단은 StationSearchOrchestrator, refresh retry는 StationRetryPolicy가 맡습니다.
  • StationEventLogger는 refresh 성공, watch toggle, watchlist 비교 표시, 외부 지도 handoff 요청, refresh 실패, 위치 실패, retry 결과를 구조화된 이벤트로 남깁니다. CrashReporter 같은 비치명 예외 보고 계약은 core:observability가 소유하고, 앱이 flavor별 구현을 바인딩합니다.
  • 주유소 목록 카드는 가격과 거리 가독성을 우선하고, 유종 chip 옆에는 브랜드 텍스트 없이 브랜드 아이콘만 배치합니다.
  • Coordinates.distanceTo, Brand.fromCode, Brand, FuelType, SearchRadius 같은 값 객체 행동과 공유 vocabulary는 core:model에 두어 data, settings, network, designsystem이 domain:station을 거치지 않고 사용합니다.
  • core:designsystem의 metric, supporting-info, row, guidance primitive와 브랜드 표시 라벨을 station list, watchlist, settings가 공유해 같은 정보 위계를 반복합니다.
  • 설정 메인 화면과 상세 선택 화면은 route는 다르지만 같은 SettingsViewModel 상태를 공유하고, 쓰기는 explicit domain use case로만 흘립니다.
  • prod 검색 파이프라인은 로컬 KATEC 좌표 변환 + Opinet 호출만 사용하고, demo는 같은 규칙을 seed 데이터로 재현합니다.

아키텍처 한눈에

아래 그래프는 Gradle 프로젝트 간 직접 의존성을 기준으로 그린 모듈 그래프입니다.

flowchart LR
    app["app"] --> fstation["feature:station-list"]
    app --> fsettings["feature:settings"]
    app --> fwatch["feature:watchlist"]
    app --> dstation["data:station"]
    app --> dsettings["data:settings"]
    app --> cdesign["core:designsystem"]
    app --> clocation["core:location"]
    app --> cnetwork["core:network"]
    app --> cdatabase["core:database"]
    app --> cmodel["core:model"]
    app --> cobserve["core:observability"]
    app --> domSettings["domain:settings"]
    app --> domStation["domain:station"]

    fstation --> domSettings
    fstation --> domStation
    fstation --> domLocation["domain:location"]
    fstation --> cdesign
    fstation --> cmodel

    fsettings --> domSettings
    fsettings --> cdesign
    fsettings --> cmodel

    fwatch --> domStation
    fwatch --> cmodel
    fwatch --> cdesign
    cdesign --> cmodel

    dstation --> domStation
    dstation --> cnetwork
    dstation --> cdatabase
    dstation --> cmodel
    dstation --> cobserve

    dsettings --> domSettings
    dsettings --> cstore["core:datastore"]

    cnetwork --> cmodel

    clocation --> domLocation
    clocation --> cmodel
    clocation --> cobserve
    domSettings --> cmodel
    domLocation --> cmodel
    domStation --> cmodel

    tools["tools:demo-seed"] --> cnetwork
    tools --> domStation
    tools --> cmodel
    benchmark["benchmark"] --> app
Loading

구조와 데이터 흐름 상세 설명은 아키텍처 문서에 정리했습니다.

핵심 사용자 플로우

  1. StationListRoute가 권한 상태를 전달하고 foreground 구간에서 위치 availability를 수집해 StationListViewModel에 반영합니다.
  2. ViewModel은 domain:location 유스케이스와 UserPreferences를 조합해 검색 입력만 담는 StationQuery를 만들고 저장소 읽기 모델을 구독합니다. 현재 좌표가 유지된 상태에서 반경, 유종, 브랜드, 정렬 조건이 바뀌면 active query를 새 조건으로 갱신하고 refresh를 요청합니다.
  3. 현재 주소 라벨은 domain:locationAddressLabelNormalizer가 행정동 중심으로 정규화하고, core:location은 Android 지오코더 후보를 그 규칙에 통과시킵니다.
  4. prod 새로고침 성공 시 Room 스냅샷과 가격 히스토리가 갱신되고 오래된 캐시는 정리되며, 실패 시 기존 스냅샷은 유지됩니다. Timeout/Network 실패는 500ms 뒤 한 번 재시도하고, demo는 고정 좌표 + seed 기반 remote source로 같은 갱신 규칙을 재현합니다.
  5. 목록에서 저장한 주유소는 watchlist 화면에서 가격 변화와 거리 기준으로 다시 비교할 수 있습니다.
  6. 주유소 카드 클릭 시 사용자가 선택한 외부 지도 앱으로 길찾기 handoff를 요청합니다.

실행 모드

모드 목적 런타임 특징 빌드
demo 같은 시작 상태를 반복 재현 앱 시작 시 seed DB 적재, 선호 초기화, 강남역 2번 출구 고정 좌표. API 키가 필요 없습니다. ./gradlew :app:assembleDemoDebug
prod 실제 API 키와 기기 상태로 동작 앱 시작 시 사용자 로컬 opinet.apikey 존재 확인, 실제 위치/네트워크 사용 ./gradlew :app:assembleProdDebug

prod 앱을 실제로 실행하려면 발급받은 opinet.apikey가 필요합니다. demo 실행에는 키가 필요 없고, prod 빌드는 빈 값으로도 가능하지만 앱 시작 시 ProdSecretsStartupHook가 누락을 바로 실패로 처리합니다. 키는 버전 관리되는 프로젝트 루트 gradle.properties에 쓰지 말고 사용자별 ~/.gradle/gradle.properties에 두거나 Gradle 실행 시 -Popinet.apikey=<issued-key>로 전달합니다. 참고할 공식 페이지는 오피넷 홈페이지오피넷 Open API 소개입니다.

prod 키는 Android 클라이언트 BuildConfig로 주입되며, 그 한계와 승격 조건은 docs/security-trade-offs.md에 정리되어 있습니다. 앱은 로컬 캐시/설정을 Android backup 대상으로 내보내지 않습니다.

# ~/.gradle/gradle.properties
opinet.apikey=

demo seed를 다시 생성하려면 아래 태스크를 사용합니다.

./gradlew :tools:demo-seed:generateDemoSeed

seed 생성과 prod 런타임 검색은 모두 opinet.apikey만 사용합니다.

릴리즈

  • CHANGELOG: 버전별 주요 변경 사항을 요약합니다.
  • 배포 절차: release branch, 검증, tag push, prodRelease 산출물, signing/secret 경계를 정리합니다.
  • Unreleased: v1.1.3 이후 변경 사항을 추적합니다.
  • 1.1.3 릴리즈 노트: hero benchmark evidence, first usable content startup reporting, backend proxy ADR, physical-device performance snapshot, 배포 절차 문서화를 정리합니다.
  • 1.1.2 릴리즈 노트: build/test 속도 개선, CI 메모리 안정화, 검증 경로 분리를 정리합니다.
  • 1.1.1 릴리즈 노트: clean architecture remediation, observability 경계, station-list/data 분리, CI scope 조정을 정리합니다.
  • 1.1.0 릴리즈 노트: production baseline, CI, i18n, screenshot regression, coverage 기반 변경과 검증 결과를 정리합니다.
  • 1.0.2 릴리즈 노트: 2026-05-05 deep analysis required fixes와 검증 결과를 정리합니다.
  • 1.0.1 릴리즈 노트: 2026-05-05 backlog risk resolution 변경의 상세 내용과 검증 결과를 정리합니다.

문서 지도

  • 작업자 운영 계약: 모든 변경에 적용되는 짧은 운영 계약입니다.
  • 기여 가이드: 새 기여자가 처음 실행할 명령, 머지 전 검증, 커밋 메시지 기준을 설명합니다.
  • 디자인 컨텍스트: yellow/black/white 정보 위계, UI 유지 기준을 설명합니다.
  • 프로젝트 읽기 가이드: 처음 읽을 때 어떤 문서와 어떤 코드부터 볼지 정리합니다.
  • 작업 절차: 변경 목적별 작업 순서, 테스트 선택, 문서 갱신 기준을 설명합니다.
  • 아키텍처: 모듈 책임, 런타임 흐름, flavor 차이를 설명합니다.
  • 모듈 계약: 각 모듈의 소유 범위와 변경 경계를 고정합니다.
  • 상태 모델: 영속 상태, 세션 상태, 읽기 모델, UI effect를 구분해 설명합니다.
  • 오프라인 전략: 캐시 스냅샷, stale 판정, refresh 실패, watchlist fallback을 다룹니다.
  • 테스트 전략: 어떤 층을 어떤 테스트로 검증하는지 설명합니다.
  • 검증 매트릭스: 실제로 어떤 Gradle 명령을 돌리면 되는지 정리합니다.
  • 배포 절차: 릴리스 준비, GitHub PR/tag 흐름, Android release 산출물과 공개 배포 전 보안 gate를 설명합니다.
  • 성능: hero macrobenchmark 정의, 실기기 측정값, baseline profile 경로와 제약을 정리합니다.
  • Backend proxy ADR: Opinet API key를 backend proxy로 승격해야 하는 조건을 기록합니다.
  • 심층 분석 리포트: 완료된 필수 수정과 조건부 승격 항목을 요약합니다.
  • 개선 분석: 완료된 backlog 항목과 남은 개선 후보의 기준을 보관합니다.
  • docs/superpowers/specs/, docs/superpowers/plans/: 완료되었거나 진행했던 설계/구현 계획의 이력을 보관합니다. 현재 구조와 실행 명령의 기준은 위 live 문서와 코드입니다.

5분 코드 투어

처음 보는 사람이 코드 흐름을 빠르게 따라가는 권장 경로입니다.

  1. app/src/main/java/com/gasstation/App.kt — Hilt 진입과 startup hook.
  2. app/src/main/java/com/gasstation/MainActivity.kt — Compose host와 system bar 정책.
  3. app/src/main/java/com/gasstation/navigation/GasStationNavHost.kt — destination 그래프.
  4. feature/station-list/src/main/kotlin/com/gasstation/feature/stationlist/StationListRoute.kt -> StationListViewModel.kt — 화면 진입과 ViewModel.
  5. feature/station-list/src/main/kotlin/com/gasstation/feature/stationlist/LocationStateMachine.kt + StationSearchOrchestrator.kt — 위치 상태와 쿼리/캐시/실패 책임 분리.
  6. feature/station-list/src/main/kotlin/com/gasstation/feature/stationlist/StationListScreen.kt, StationListCards.kt, StationListStates.kt, StationListQuerySummary.kt, StationListBodyState.kt — 화면 scaffold, 카드, 상태 화면, query context 분리.
  7. data/station/src/main/kotlin/com/gasstation/data/station/DefaultStationRepository.kt, StationSearchResultAssembler.kt, WatchlistSummaryAssembler.kt — Room snapshot + remote fetch orchestration과 읽기 모델 조립.
  8. core/network/src/main/kotlin/com/gasstation/core/network/station/NetworkStationFetcher.kt — Opinet API와 KATEC 좌표 변환.

각 단계의 책임 분리 근거는 docs/architecture.md에 있습니다.

Performance Snapshot

GasStation publishes performance numbers from the deterministic demo flavor running hero macrobenchmarks on a physical device. Emulator measurements are used only as smoke checks. The numbers below are the latest committed physical-device run; scenario definitions, device information, and reproduction commands live in Performance.

Hero journey Primary metric p50 p95
Startup to first content startup (timeToInitialDisplayMs) 347 ms 393 ms
Startup to first content startup (timeToFullDisplayMs) 546 ms 622 ms
List scroll frame (frameDurationCpuMs) 3.84 ms/frame 6.83 ms/frame
Refresh frame (frameDurationCpuMs) 3.83 ms/frame 6.05 ms/frame

Measured on Samsung Galaxy S20+ 5G (SM-G986N, Android 13 / API 33) with the demoBenchmark variant on 2026-05-18. See Performance for the full table, frame-overrun numbers, known limitations (baseline profile + watchlist benchmark currently unavailable), and the exact commands to reproduce the run.

검증

빠른 로컬 확인:

./gradlew \
  :core:model:test \
  :domain:location:test \
  :core:observability:test \
  :core:designsystem:testDebugUnitTest \
  :feature:station-list:testDebugUnitTest \
  :feature:watchlist:testDebugUnitTest \
  :feature:settings:testDebugUnitTest \
  :app:assembleDemoDebug \
  :app:testDemoDebugUnitTest \
  :app:testProdDebugUnitTest \
  :benchmark:assemble

기기 기반 UI 확인:

./gradlew :app:connectedDemoDebugAndroidTest

전체 명령과 상황별 기준은 검증 매트릭스를 따릅니다. GitHub Actions Android CI는 PR에서 static-analysis, unit-tests, screenshot-tests, assemble을 실행합니다. assemble은 demo/prod debug와 benchmark를 확인하고, main/v* tag push에서만 release-assemblecoverage를 추가 실행합니다.

About

현재 위치 기반 주유소 조회, 북마크 비교, stale 캐시 fallback을 구현한 멀티모듈 Compose Android 프로젝트

Resources

License

Contributing

Stars

Watchers

Forks

Contributors

Languages