Skip to content

feat: 메인 화면 ViewModel 구성 및 상태 관리 구현 #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

Benjamin8282
Copy link
Collaborator

이번 Pull Request는 앱의 날씨 데이터 처리 방식을 크게 개선합니다. 주요 변경 사항에는 인메모리 캐싱 기능이 포함된 날씨 리포지토리 구현, 도시 관리 기능 향상, 날씨 아이콘 및 로딩 상태를 표시하는 UI 업데이트가 포함됩니다. 또한 날씨 목록 화면을 리팩토링하여 실시간 데이터 로딩, 도시 일괄 삭제, 코드 구조 개선을 지원합니다.

날씨 데이터 가져오기 및 캐싱

  • WeatherRepository 인터페이스와 그 구현체 WeatherRepositoryImpl을 추가하였습니다. 이 구현체는 API로부터 날씨 데이터를 가져오고, 동일 요청을 방지하기 위해 30분 동안 메모리에 캐싱합니다. 언어 설정은 디바이스 로케일에 따라 자동으로 선택됩니다. (WeatherRepository.kt [1] WeatherRepositoryImpl.kt [2])
  • 날씨 응답과 타임스탬프를 함께 저장하기 위한 CachedWeather 데이터 클래스를 추가하였습니다. (CachedWeather.kt app/src/main/java/com/ben/simpleweather/data/CachedWeather.ktR1-R8)

도시 관리 기능 개선

  • CityStorageRepository와 그 구현체를 리팩토링하여 도시 일괄 삭제를 지원하고, 저장 로직을 유지보수에 유리하게 개선하였습니다. (CityStorageRepository.kt [1] CityStorageRepositoryImpl.kt [2])
  • City 객체를 기준으로 삭제 기능을 구현하여, UI에서 보다 안정적인 도시 삭제 기능을 제공하도록 했습니다. (WeatherListScreen.kt [1] [2] [3] [4]; WeatherListViewModel.kt [5])

UI 및 UX 개선

의존성 주입 및 모듈 업데이트

  • 새로운 WeatherRepository를 Dagger/Hilt DI 모듈에 등록하여 생명주기 관리를 적절히 수행하도록 했습니다. (RepositoryModule.kt [1] [2])

기타 개선 사항

closes #9

날씨 아이콘 코드를 사용하여 이미지를 표시하는 `WeatherIcon` Composable 함수를 `Utils.kt` 파일에 추가했습니다. 이 함수는 아이콘 URL을 생성하고 `AsyncImage`를 사용하여 이미지를 로드합니다. 아이콘 크기와 Modifier를 선택적으로 지정할 수 있습니다.
`WeatherApi.kt` 파일에서 현재 날씨 및 예보 API 호출 시 사용되는 언어(`lang`) 파라미터의 기본값을 "kr" (한국어)에서 "en" (영어)으로 수정했습니다.
`WeatherRepository` 인터페이스와 그 구현체인 `WeatherRepositoryImpl`을 추가했습니다. `WeatherRepositoryImpl`은 API로부터 가져온 날씨 정보를 30분 동안 캐싱하여, 동일한 도시 ID에 대한 반복적인 API 호출을 줄이도록 구현되었습니다. 현재 시스템 언어 설정을 감지하여 한국어인 경우 'kr'로, 그 외에는 'en'으로 API 호출 시 언어 파라미터를 설정합니다.
`CityStorageRepository` 인터페이스 및 `CityStorageRepositoryImpl` 구현체에 여러 도시를 한 번에 삭제하는 `removeCities` 함수를 추가했습니다. 또한, 도시 목록을 저장하는 로직을 `saveCities`라는 private 함수로 분리하여 중복 코드를 제거하고 가독성을 향상시켰습니다. 예외 처리 시 사용되지 않는 변수명을 `_`로 변경했습니다.
날씨 응답(`WeatherResponse`)과 타임스탬프를 포함하는 `CachedWeather` 데이터 클래스를 추가했습니다. 이 클래스는 날씨 데이터를 캐시하고 저장 시간을 추적하는 데 사용됩니다.
`WeatherItem.kt` 파일의 `WeatherItem` 데이터 클래스에 `city` (City 객체)와 `iconCode` (String) 필드를 추가했습니다. 이를 통해 날씨 항목에 도시 정보와 날씨 아이콘 코드를 포함할 수 있게 됩니다.
`WeatherListViewModel`을 수정하여 저장된 도시들의 현재 날씨 정보를 OpenWeatherMap API를 통해 가져오도록 구현했습니다. 또한, 선택된 도시들을 `CityStorageRepository`에서 삭제하고 화면 목록에서도 제거하는 기능을 추가했습니다. 로딩 상태를 나타내는 `isLoading` StateFlow와 현재 시스템 언어 설정에 따라 도시 이름을 표시하는 `displayName()` 확장 함수도 추가되었습니다.
- `WeatherListScreen`에 날씨 아이콘을 표시하도록 `WeatherIcon` Composable을 추가했습니다.
- 로딩 상태를 표시하기 위해 `CircularProgressIndicator`를 추가하고, 도시 목록이 비어 있을 때의 UI를 개선했습니다.
- 도시를 선택하여 삭제하는 기능과 드래그 앤 드롭으로 순서 변경 기능을 구현했습니다.
- `WeatherDetailScreen`에서 중복되던 `WeatherIcon` Composable을 제거하고 공통으로 사용하도록 수정했습니다.
- 도시 목록 화면으로 돌아올 때 저장된 도시의 날씨 정보를 다시 불러오도록 `LaunchedEffect`를 사용했습니다.
Hilt를 사용하여 WeatherRepository 인터페이스와 그 구현체인 WeatherRepositoryImpl을 바인딩하는 코드를 RepositoryModule.kt에 추가했습니다. 이를 통해 의존성 주입을 통해 WeatherRepository를 사용할 수 있게 됩니다.
`WeatherListViewModel`의 주요 기능들을 검증하는 `WeatherListViewModelTest.kt` 파일을 추가했습니다. 이 테스트는 `CityStorageRepository` 및 `WeatherRepository`의 Mock 객체를 사용하여 ViewModel의 로직을 검증합니다.

주요 테스트 시나리오:
- `loadWeatherForSavedCities`: 저장된 도시 목록에 대한 날씨 정보를 불러와 `weatherList` LiveData를 올바르게 업데이트하는지 확인합니다.
- `deleteSelected`: 선택된 도시들을 `weatherList`에서 제거하고, `CityStorageRepository`의 `removeCities` 함수를 호출하여 저장소에서도 삭제하는지 확인합니다.
- `moveItem`: `weatherList` 내의 아이템 순서를 올바르게 변경하는지 확인합니다.
Copy link

sonarqubecloud bot commented Aug 7, 2025

@Benjamin8282 Benjamin8282 requested a review from Copilot August 7, 2025 13:04
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

이 PR은 메인 화면의 ViewModel 구성과 상태 관리를 구현하여 실제 날씨 API 데이터를 활용한 동적 날씨 목록 화면을 제공합니다.

  • WeatherRepository 인터페이스와 구현체를 추가하여 API 호출 및 30분 인메모리 캐싱 기능 구현
  • WeatherListViewModel을 리팩토링하여 더미 데이터에서 실제 API 데이터 사용으로 전환
  • UI 개선: 로딩 상태 표시, 날씨 아이콘 컴포넌트 분리, 도시 일괄 삭제 기능 추가

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
WeatherListViewModelTest.kt 새로운 ViewModel의 핵심 기능들을 테스트하는 단위 테스트 추가
WeatherApi.kt API 기본 언어 설정을 한국어에서 영어로 변경
WeatherListViewModel.kt 더미 데이터에서 실제 API 호출로 전환, 로딩 상태 및 도시 삭제 기능 구현
WeatherListScreen.kt 로딩 상태 UI 추가, 날씨 아이콘 표시, 도시 객체 기반 선택/삭제 로직 구현
WeatherDetailScreen.kt WeatherIcon 컴포넌트를 별도 파일로 분리
Utils.kt 재사용 가능한 WeatherIcon 컴포넌트 생성
RepositoryModule.kt WeatherRepository DI 바인딩 추가
WeatherRepositoryImpl.kt 날씨 API 호출 및 캐싱 로직을 포함한 구현체
WeatherRepository.kt 날씨 데이터 조회 인터페이스 정의
CityStorageRepositoryImpl.kt 도시 일괄 삭제 기능 및 코드 구조 개선
CityStorageRepository.kt removeCities 메서드 인터페이스 추가
WeatherItem.kt City 객체와 iconCode 필드 추가
CachedWeather.kt 날씨 응답과 타임스탬프를 저장하는 캐시 데이터 클래스

Comment on lines +87 to +94
fun City.displayName(): String {
return if (Locale.getDefault().language == "ko") {
nameKo
} else {
name
}
}

Copy link
Preview

Copilot AI Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extension function 'displayName()' is defined inside the ViewModel class, making it inaccessible from other classes. Consider moving this to a top-level extension function or a utility class for better reusability.

Suggested change
fun City.displayName(): String {
return if (Locale.getDefault().language == "ko") {
nameKo
} else {
name
}
}
}
fun City.displayName(): String {
return if (Locale.getDefault().language == "ko") {
nameKo
} else {
name
}

Copilot uses AI. Check for mistakes.


data class CachedWeather(
val weather: WeatherResponse,
val timestamp: Long // 저장된 시간 (System.currentTimeMillis())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trailing comma 붙여 주세요

val cityName: String,
val temperature: Int,
val weatherType: String,
val iconCode: String,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trailing comma 붙여 주세요

private val cacheDurationMillis = 30 * 60 * 1000L // 30분

override suspend fun getWeather(cityId: Int, latitude: Double, longitude: Double): WeatherResponse {
val lang = if (Locale.getDefault().language == "ko") "kr" else "en"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한 줄로 써도 좋지만

if {}
else {}

식으로 brace 붙이는 게 가독성에 유리할 거 같아요

iconCode = weather.weather?.firstOrNull()?.icon.orEmpty()
)
} catch (_: Exception) {
null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가능한한 null 을 던지는 건 지양하는 것이 좋을 거 같아요. nullable 을 없애고 emptyList 를 보내도 될 거 같습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Issue #9] 메인 화면 ViewModel 구현
2 participants