간단한 안드로이드 앱 만들기 (날씨 앱)

10. 지도 위에 날씨 이미지 표시하기 (카카오맵 라벨 생성)

리저브콜드브루 2025. 2. 21. 16:28
728x90
반응형

카카오맵의 라벨 기능을 이용해 맵 위에 현재 날씨 아이콘을 띄우도록 하려고 한다

먼저 Map 매니저 클래스에서 UI 담당 클래스를 분리작업을 했다

 

MapViewMager > MapController 변경

Map의 초기화 및 라이프사이클 관리, 카메라 이동 기능만을 담당하도록 코드를 수정하였다

package com.example.weatherapp.map

import ...

/***
 * 맵의 초기화, 라이프사이클 관리, 카메라 이동
 */
class MapController(
    private val activity: AppCompatActivity,
    private val mapView: MapView,
) {

    private var kakaoMap: KakaoMap? = null

    fun init(onMapReady: (KakaoMap) -> Unit)
    {
        KakaoMapSdk.init(activity, "47267159ba04526c395cbc28462c33ea")
        Log.d("MapManager", KakaoMapSdk.INSTANCE.toString() )

        mapView.start(object : MapLifeCycleCallback() {
            override fun onMapDestroy() {
                // 지도 API 가 정상적으로 종료될 때 호출됨
            }

            override fun onMapError(error: Exception) {
                // 인증 실패 및 지도 사용 중 에러가 발생할 때 호출됨
                Log.e("MapManager", "Map error: ${error.localizedMessage}")
            }
        }, object : KakaoMapReadyCallback() {
            override fun onMapReady(map: KakaoMap) {
                // 인증 후 API 가 정상적으로 실행될 때 호출됨
                kakaoMap = map
                onMapReady(map)
            }
        })
    }

    fun onResume()
    {
        mapView.resume()
    }

    fun onPause()
    {
        mapView.pause()
    }

	//카메라 이동
    fun moveCamera(position: LatLng, zoomLevel: Int = 10) {
        kakaoMap?.moveCamera(CameraUpdateFactory.newCenterPosition(position, zoomLevel))
    }

    fun getKakaoMap(): KakaoMap? {
        return kakaoMap
    }
}

 

MapUIManager 추가

import ...

/***
 * 맵의 UI 요소를 지도에 추가/갱신
 */
class MapUIManager(
    private val activity: AppCompatActivity,
    private val kakaoMap: KakaoMap,
    private val mapController: MapController,
    private val locationDataStore: LocationDataStore,
    private val weatherUIMapper: WeatherUIMapper
) {
    private var currentLatLng: LatLng? = null // 현 위치 캐싱
    private var currentLabel: Label? = null // 라벨 값 캐싱

    init {
    	//현 위치와 날씨 아이콘 초기화
        updateCurPosition()
        observeWeatherIconChanges()
    }

    private fun observeWeatherIconChanges() {
    	// weatherIcon 관찰 및 업데이트
        weatherUIMapper.weatherIcon.observe(activity) { newIconRes ->
            updateWeatherLabel(newIconRes)
        }
    }

    private fun updateCurPosition() {
        activity.lifecycleScope.launch {
            val curLatitude = locationDataStore.cur_latitude.first()
            val curLongitude = locationDataStore.cur_longitude.first()
            if (curLatitude != null && curLongitude != null) {
                currentLatLng = LatLng.from(curLatitude, curLongitude)
                // 맵의 카메라를 현재 위치로 이동
                mapController.moveCamera(currentLatLng!!, 13)
            }
        }
    }


    private fun updateWeatherLabel(iconResId: Int? = null) {
        currentLatLng?.let {

            // 새로운 라벨 스타일 설정 (맵퍼에서 가져온 아이콘 적용)
            val iconRes = iconResId ?: weatherUIMapper.weatherIcon.value ?: R.drawable.sunny_128
            val styles = kakaoMap.labelManager?.addLabelStyles(
                LabelStyles.from(LabelStyle.from(iconRes).setAnchorPoint(0.5f, 1.0f))
            )

            // 새로운 라벨 추가
            val options = LabelOptions.from(it).setStyles(styles)
            currentLabel = kakaoMap.labelManager?.layer?.addLabel(options)
        }
    }
}
  • 추가된 updateWeatherLabel()을 통해 날씨 아이콘 업데이트

그 외에 변경되거나 추가된 클래스

  • WeatherViewModel
    • 날씨 API Response 부분 > WeatherRepository
    • 날씨 아이콘 관찰 및 업데이트 > WeatherUIMapper
    • 그 외의 기능 유지
  • WeatherRepository 추가
    • 날씨 API Response 담당
  • WeatherUIManager 추가
    • 날씨 API 데이터 관찰을 통한 날씨 관련 UI 업데이트
  • WeatherViewModelFactory 추가
    • WeatherViewModel이 WeatherRepository를 매개변수로 사용하기 위해 추가

 

WeatherRepository 추가

package com.example.weatherapp.weather

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData

/***
 * Weather API response 관리
 */
class WeatherRepository {
    private val _nowcastData = MutableLiveData<NcstWeatherResponse>() // 실황 데이터 캐싱
    val nowcastData: LiveData<NcstWeatherResponse> get() = _nowcastData

    private val _forecastData = MutableLiveData<FcstWeatherResponse>() // 예보 데이터 캐싱
    val forecastData: LiveData<FcstWeatherResponse> get() = _forecastData


    suspend fun fetchNowcastData(serviceKey: String, nx: Int, ny: Int) {
        val (baseDate, baseTime) = WeatherUtils.getCurrentNcstBaseTime()
        val response = RetrofitClient.instance.create(WeatherApi::class.java)
            .getWeather(serviceKey, baseDate = baseDate, baseTime = baseTime, nx = nx, ny = ny)

        Log.d("WeatherRepository", "Nowcast API Response: $response")

        _nowcastData.postValue(response) // LiveData 업데이트
    }

    suspend fun fetchForecastData(serviceKey: String, nx: Int, ny: Int) {
        val (baseDate, baseTime) = WeatherUtils.getCurrentFcstBaseTime()
        val response = RetrofitClient.instance.create(WeatherApi::class.java)
            .getForecast(serviceKey, baseDate = baseDate, baseTime = baseTime, fcstDate = baseDate, fcstTime = baseTime, nx = nx, ny = ny)

        Log.d("WeatherRepository", "Forecast API Response: $response")

        _forecastData.postValue(response) // LiveData 업데이트
    }
}

 

 

WeatherUIManager 추가

package com.example.weatherapp.weather

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer

class WeatherUIManager(weatherViewModel: WeatherViewModel) {

    private val _weatherIcon = MutableLiveData<Int>() // UI에서 사용할 날씨 아이콘
    val weatherIcon: LiveData<Int> get() = _weatherIcon

    init {
        // 날씨 데이터가 변경될 때 아이콘 업데이트
        val observer = Observer<Any> {
            updateWeatherIcon(
                weatherViewModel.skyCondition.value ?: 1,
                weatherViewModel.precipitationType.value ?: 0
            )
        }

        weatherViewModel.skyCondition.observeForever(observer)
        weatherViewModel.precipitationType.observeForever(observer)
    }

	// 날씨 아이콘 업데이트
    private fun updateWeatherIcon(skyCondition: Int, precipitationType: Int) {
        _weatherIcon.value = WeatherUtils.getWeatherIcon128(skyCondition, precipitationType)
    }
}

 

WeatherViewModelFactory 추가

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.weatherapp.weather.WeatherRepository
import com.example.weatherapp.weather.WeatherViewModel

class WeatherViewModelFactory(private val weatherRepository: WeatherRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(WeatherViewModel::class.java)) {
            return WeatherViewModel(weatherRepository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

 

 

WeatherViewModel

package com.example.weatherapp.weather

import ...

class WeatherViewModel(private val weatherRepository: WeatherRepository) : ViewModel() {

    private val _currentTemperature = MutableLiveData<String>() // 현재 온도 (T1H) - 초단기실황
    val currentTemperature: LiveData<String> get() = _currentTemperature

    private val _skyCondition = MutableLiveData<Int>() // 하늘 상태 (SKY) - 초단기예보
    val skyCondition: LiveData<Int> get() = _skyCondition

    private val _precipitationType = MutableLiveData<Int>() // 강수 형태 (PTY) - 초단기실황
    val precipitationType: LiveData<Int> get() = _precipitationType

    init {
        // Repository의 데이터를 구독하여 ViewModel의 데이터 업데이트
        weatherRepository.nowcastData.observeForever { updateNowcastData(it) }
        weatherRepository.forecastData.observeForever { updateForecastData(it) }
    }

    // 날씨 데이터 가져오기
    fun fetchWeatherData(serviceKey: String, nx: Int, ny: Int) {
        Log.d("WeatherViewModel", "Fetching weather data for nx: $nx, ny: $ny")

        viewModelScope.launch {
            weatherRepository.fetchNowcastData(serviceKey, nx, ny)
            weatherRepository.fetchForecastData(serviceKey, nx, ny)
        }
    }

    // 초단기실황 데이터 업데이트
    private fun updateNowcastData(response: NcstWeatherResponse) {
        Log.d("WeatherViewModel", "ncst API Response: $response")

        val body = response.response.body
        if (body != null && body.items.item.isNotEmpty()) {
            _precipitationType.value = body.items.item.find { it.category == "PTY" }?.obsrValue?.toIntOrNull() ?: 0
            _currentTemperature.value = body.items.item.find { it.category == "T1H" }?.obsrValue ?: "N/A"
        } else {
            Log.e("WeatherViewModel", "Nowcast response body is empty")
        }
    }

    // 초단기예보 데이터 업데이트
    private fun updateForecastData(response: FcstWeatherResponse) {
        Log.d("WeatherViewModel", "fcst API Response: $response")

        val body = response.response.body
        if (body != null && body.items.item.isNotEmpty()) {
            _skyCondition.value = body.items.item.find { it.category == "SKY" }?.fcstValue?.toIntOrNull() ?: 1
        } else {
            Log.e("WeatherViewModel", "Forecast response body is empty")
        }
    }
}

맵 내의 날씨 라벨이 추가된 모습

결과

728x90
반응형