2022. 10. 8. 18:00ㆍandroid/날씨공공데이터앱
2편에 이어 이번엔 Repository 편입니다.
Data package의 전체적인 구조입니다.
먼저 DefaultLocationTracker입니다.
class DefaultLocationTracker @Inject constructor(
private val locationClient: FusedLocationProviderClient,
private val application: Application
) : LocationTracker {
override suspend fun getCurrentLocation(): Pair<Location?, Address?>? {
val geo = Geocoder(application.applicationContext, Locale.KOREA)
val hasAccessFineLocationPermission = ContextCompat.checkSelfPermission(
application,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val hasAccessCoarseLocationPermission = ContextCompat.checkSelfPermission(
application,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val locationManager =
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val isGpsEnabled = locationManager
.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ||
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
if (!hasAccessCoarseLocationPermission || !hasAccessFineLocationPermission || !isGpsEnabled) {
return Pair(null, null)
}
return suspendCancellableCoroutine { cont ->
locationClient.lastLocation.apply {
if (isComplete) {
if (isSuccessful) {
val address = geo.getFromLocation(result.latitude, result.longitude, 1)
cont.resume(Pair(result, address[0]))
} else {
cont.resume(null)
}
return@suspendCancellableCoroutine
}
addOnSuccessListener {
val address = geo.getFromLocation(it.latitude, it.longitude, 1)
cont.resume(Pair(it, address[0]))
}
addOnFailureListener {
cont.resume(null)
}
addOnCanceledListener {
cont.cancel()
}
}
}
}
}
위치와 주소를 반환합니다.
주소는 Geocoder를 이용해서 처리하고
val geo = Geocoder(application.applicationContext, Locale.KOREA)
return에서 반환합니다.
위치 퍼미션에 대한 부분이 이곳에 있어서 처음 앱이 실행될 때 나오게 됩니다.
두 번째로 WeatherApi입니다
두 개를 Get 하도록 했는데 이유는
1. 단기예보를 받을 경우 2시 5시 8시 이렇게 3시간 간격으로 받아오는데 이때 받아오면 한 시간 후에 날씨부터 받아오기 때문
2. 초단기예보가 지금 날씨와 가장 오차가 적지 않을까라는 생각
3. 여러 개의 통신 상황이 궁금해서
그래서
위에 @GET 은 단기 예보
아래 @GET은 초단기 예보입니다.
이제 Weather입니다.
data class Weather(
@SerializedName("response")
val response : Response
)
data class Response(
@SerializedName("header")
val header: Header,
@SerializedName("body")
val body: Body
)
data class Header(
@SerializedName("resultCode")
val resultCode: Int,
@SerializedName("resultMsg")
val resultMsg: String
)
data class Body(
@SerializedName("dataType")
val dataType: String,
@SerializedName("items")
val items: Items
)
data class Items(
@SerializedName("item")
val item: List<Item>
)
data class Item(
@SerializedName("baseDate")
val baseDate: String,
@SerializedName("baseTime")
val baseTime: String,
@SerializedName("category")
val category: String,
@SerializedName("fcstDate")
val fcstDate: String,
@SerializedName("fcstTime")
val fcstTime: String,
@SerializedName("fcstValue")
val fcstValue: String?,
@SerializedName("nx")
val nx: Int,
@SerializedName("ny")
val ny: Int
)
안드로이드 스튜디오 플러그인에 검색하면 JSON 변환할 수 있게 해주는 녀석이 있습니다.
파란 부분 보이시나요?? 저게 JSON에서 데이터 클래스로 포맷을 맞춰줍니다.
저걸 쓰면
초단기예보의 응답 메시지의 아래와 같은 예시를
이렇게 첨부하고 아래에 advanced를 눌러 설정을 해주면
아래처럼 바꿔줍니다.
저는 조금 다르게 했습니다.
data class example(
@SerializedName("response")
val response: Response
) {
data class Response(
@SerializedName("header")
val header: Header,
@SerializedName("body")
val body: Body
) {
data class Header(
@SerializedName("resultCode")
val resultCode: String,
@SerializedName("resultMsg")
val resultMsg: String
)
data class Body(
@SerializedName("dataType")
val dataType: String,
@SerializedName("items")
val items: Items,
@SerializedName("pageNo")
val pageNo: Int,
@SerializedName("numOfRows")
val numOfRows: Int,
@SerializedName("totalCount")
val totalCount: Int
) {
data class Items(
@SerializedName("item")
val item: List<Item>
) {
data class Item(
@SerializedName("baseDate")
val baseDate: String,
@SerializedName("baseTime")
val baseTime: String,
@SerializedName("category")
val category: String,
@SerializedName("fcstDate")
val fcstDate: String,
@SerializedName("fcstTime")
val fcstTime: String,
@SerializedName("fcstValue")
val fcstValue: String,
@SerializedName("nx")
val nx: Int,
@SerializedName("ny")
val ny: Int
)
}
}
}
}
잠깐 딴 길로 빠졌지만 다시 돌아와서!! 혹시 모르시는 분들을 위해 작성했습니다.
RepositoryImpl입니다.
사진에는 하나의 함수만 있습니다.
Resource로 성공했을 때 아닐 때 나누어 주었습니다.
Resource를 사용한 이유는
Retrofit을 이용한 여러 가지 예제를 보면
간단하게 응답이 어떻게 오는지만 확인하기 때문에 그리고 거의 대부분 Call을 사용하여 처리를 하더라구요.
그래서 Call 을 찾아보니
Call 은 Retrofit에서 자체적으로 비동기 처리를 해주는 라이브러리였습니다.
근데 비동기 실행을 위한 Coroutine이라는 강력한 게 있는데 Call을 써야 하나 싶었습니다.
그래서 또 찾아보니 Response로 받아오는 것도 있었습니다.
Response는 성공했을 경우에 결과를 가져올 수 있습니다.
근데 성공했을 때만 Return으로 보내면 실패했을 때를 넘겨줄 수가 없는 겁니다.
그래서 여러 가지 시도하고 참고한 결과 이게 가장 좋다고 결론을 내렸습니다.
아래는 Constants입니다
class Constants {
companion object{
const val BASE_URL = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/"
const val API_KEY = "여기에 본인의 키를 넣으면 됩니다"
}
}
마지막으로 Wrapper입니다.
여기서 받아온 data를 싹 탈바꿈해서 보내줍니다.
일단 순서를 말씀드리면
1. 데이터를 키값 날짜별 데이터 값 리스트인 맵으로 변경
2. 변경된 각 날짜별 맵의 데이터를 시간별 맵으로 변경
3. 시간별 맵에서 지난 시간은 필터링
4. 위 시간별 맵을 시간별 날씨 data 리스트로 변경해줌
5. 결과는 날짜별 날씨 Data 리스트
위에 과정이 바로 아래 Body.itemsToWeatherData 에 있습니다.
데이터 행이 580 정도 되어야 모든 카테고리별 데이터 이틀 치가 받아지길래 그냥 580을 넣었습니다.
데이터를 받아오면
fun Items.toMapGroupByDate(): Map<String, List<Item>> {
val cal: Calendar = Calendar.getInstance()
val currentHour: String = SimpleDateFormat("HH", Locale.getDefault()).format(cal.time)
val currentMinute: String = SimpleDateFormat("mm", Locale.getDefault()).format(cal.time)
val currentDate = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(cal.time)
val currentTime = currentHour + currentMinute
val map = item.groupBy { it.fcstDate }.toMutableMap()
if (currentTime.toInt() in 2300..2359) {
map.remove(key = currentDate)
} else {
if (map.keys.size == 3) {
map.remove(key = map.keys.last())
}
}
return map
}
위에서 먼저 날짜별 맵으로 변경합니다.
바꿀 때 시간이 오후 열한 시-열두 시 사이에 있으면
오늘 날짜의 데이터는 그냥 지웁니다 거의 하루가 끝나가니까 딱히 필요 없으니까요
그리고 그 시간이 아닌데 3일 치가 들어왔다 그러면 뒤를 자릅니다.
왜냐하면 이틀 치만 보여주려고 하거든요
이제 이 데이터를 가지고
fun mapToWeatherDataList(mapGroupedByTime: Map<String, List<Item>>): List<WeatherData> {
val result = mapGroupedByTime.mapValues {
listToWeatherData(it.value)
}
return result.values.toList()
}
위에 보시면 각 mapvalue 값을 listToWeatherData에 넣어주는데
mapGroupedByTime 함수 인자 map의 value 값이 List <Item>으로 되어있는 걸 보실 수 있습니다.
mapGroupedByTime map 의 키 값은 시간별로 들어가 있고
value 값은 그 시간에 가지고 있는 데이터들을 리스트로 가지고 있습니다.
공공데이터는 아래처럼 category로 값을 다 가지고 있어서 저 fcstValue 값을 가지고 와서 써야 하기 때문에
날짜별 -> 시간별 -> 시간별 WeatherData로 변경하는 과정을 거칩니다.
이제 아래 코드에서
시간별로 되어있는 Item 들에서 필요한 category와 그 값만 빼주기 위한 함수가 있습니다.
여기서 Item 하나하나를 보고 값만 뽑아옵니다.
그리고 값에 맞는 제가 화면에 보여주고 싶은 그림이나 말이나 이런 걸 정해줍니다.
fun listToWeatherData(itemList: List<Item>): WeatherData {
val time = itemList[0].fcstTime
val temperature = itemList.find { it.category == "T1H" || it.category == "TMP" }!!.fcstValue!!
val precipitation = when(itemList.find { it.category == "PTY" }?.fcstValue) {
"0" -> if (time.toInt() in 1800..2359 || time.toInt() == 0 || time.toInt() in 1..600) "밤맑음" else "맑음"
"1" -> "비"
"2" -> "비/눈"
"3" -> "눈"
"4" -> "소나기"
"5" -> "빗방울"
"6" -> "빗방울눈날림"
else -> "눈날림"
}
val precipitationAmount = itemList.find { it.category == "RN1" || it.category == "PCP" }!!.fcstValue!!
val sky = when(itemList.find { it.category == "SKY" }?.fcstValue) {
"1" -> if (time.toInt() in 1800..2359 || time.toInt() == 0 || time.toInt() in 1..600) "밤맑음" else "맑음"
"3" -> if (time.toInt() in 1800..2359 || time.toInt() == 0 || time.toInt() in 1..600) "밤구름 많음" else "구름 많음"
else -> "흐림"
}
val humidity = when(itemList.find { it.category == "REH" }?.fcstValue){
"0", "강수없음", null -> "0"
else -> itemList.find { it.category == "SKY" }!!.fcstValue!!
}
val wind = itemList.find { it.category == "WSD" }?.fcstValue!!
val weatherClassification =
if (precipitation == "밤맑음" || precipitation == "맑음") WeatherClassification.fromCategory(sky)
else WeatherClassification.fromCategory(precipitation)
return WeatherData(
time,
temperature,
precipitation,
precipitationAmount,
sky,
humidity,
wind,
weatherClassification
)
}
마지막으로 여기서는 현재 데이터, 즉 초단기예보로 받아온 데이터를 걸러주는데
fun Items.toWeatherInfo(): WeatherData {
return listToWeatherData(
itemList = item
.groupBy { it.fcstDate }
.values.first()
.sortedBy { it.fcstTime }
.subList(0,10)
)
}
listToWeatherData로 들어가기 전에 미리 item을 측정 날짜를 오늘로 해야 하기 때문에
fcstDate를 기준으로 묶고 ( 이유는 오후 7시부터는 다음날 데이터도 있어서 )
여기도 category가 전부 다 들어오려면 일단 60개 이상은 행을 정해서 불러오기 때문에 그 정도 받고
그 시간별 정리된 맵의 첫 번째 값 = 오늘 날짜 데이터만
.first로 빼줍니다.
그리고 또 시간별로 정렬을 하고 앞에 10개의 데이터만 자릅니다.
( 앞에 10개가 현재 시간 초단기 예보 결과 데이터들이라)
package com.dev_musashi.weather.data.mapper
import com.dev_musashi.weather.data.remote.Body
import com.dev_musashi.weather.data.remote.Item
import com.dev_musashi.weather.data.remote.Items
import com.dev_musashi.weather.domain.data.WeatherClassification
import com.dev_musashi.weather.domain.data.WeatherData
import com.dev_musashi.weather.domain.data.WeatherMap
import java.text.SimpleDateFormat
import java.util.*
fun Body.itemsToWeatherData(): WeatherMap {
val cal: Calendar = Calendar.getInstance()
val currentHour: String = SimpleDateFormat("HH", Locale.getDefault()).format(cal.time)
val currentMinute: String = SimpleDateFormat("mm", Locale.getDefault()).format(cal.time)
val currentDate = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(cal.time)
val currentTime = currentHour + currentMinute
val weatherMap = items
.toMapGroupByDate()
.mapValues {
it.value.groupBy { it.fcstTime }
}.mapValues {
if (it.key == currentDate) {
val filterMap = it.value.filter {
it.key.toInt() > currentTime.toInt()
}
mapToWeatherDataList(filterMap)
} else {
mapToWeatherDataList(it.value)
}
}
return WeatherMap(weatherMap = weatherMap)
}
fun Items.toMapGroupByDate(): Map<String, List<Item>> {
val cal: Calendar = Calendar.getInstance()
val currentHour: String = SimpleDateFormat("HH", Locale.getDefault()).format(cal.time)
val currentMinute: String = SimpleDateFormat("mm", Locale.getDefault()).format(cal.time)
val currentDate = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(cal.time)
val currentTime = currentHour + currentMinute
val map = item.groupBy { it.fcstDate }.toMutableMap()
if (currentTime.toInt() in 2300..2359) {
map.remove(key = currentDate)
} else {
if (map.keys.size == 3) {
map.remove(key = map.keys.last())
}
}
return map
}
fun mapToWeatherDataList(weatherMapByDate: Map<String, List<Item>>): List<WeatherData> {
val result = weatherMapByDate.mapValues {
listToWeatherData(it.value)
}
return result.values.toList()
}
fun listToWeatherData(itemList: List<Item>): WeatherData {
val time = itemList[0].fcstTime
val temperature = itemList.find { it.category == "T1H" || it.category == "TMP" }!!.fcstValue!!
val precipitation = when(itemList.find { it.category == "PTY" }?.fcstValue) {
"0" -> if (time.toInt() in 1800..2359 || time.toInt() == 0 || time.toInt() in 1..600) "밤맑음" else "맑음"
"1" -> "비"
"2" -> "비/눈"
"3" -> "눈"
"4" -> "소나기"
"5" -> "빗방울"
"6" -> "빗방울눈날림"
else -> "눈날림"
}
val precipitationAmount = itemList.find { it.category == "RN1" || it.category == "PCP" }!!.fcstValue!!
val sky = when(itemList.find { it.category == "SKY" }?.fcstValue) {
"1" -> if (time.toInt() in 1800..2359 || time.toInt() == 0 || time.toInt() in 1..600) "밤맑음" else "맑음"
"3" -> if (time.toInt() in 1800..2359 || time.toInt() == 0 || time.toInt() in 1..600) "밤구름 많음" else "구름 많음"
else -> "흐림"
}
val humidity = when(itemList.find { it.category == "REH" }?.fcstValue){
"0", "강수없음", null -> "0"
else -> itemList.find { it.category == "SKY" }!!.fcstValue!!
}
val wind = itemList.find { it.category == "WSD" }?.fcstValue!!
val weatherClassification =
if (precipitation == "밤맑음" || precipitation == "맑음") WeatherClassification.fromCategory(sky)
else WeatherClassification.fromCategory(precipitation)
return WeatherData(
time,
temperature,
precipitation,
precipitationAmount,
sky,
humidity,
wind,
weatherClassification
)
}
fun Items.toWeatherInfo(): WeatherData {
return listToWeatherData(
itemList = item
.groupBy { it.fcstDate }
.values.first()
.sortedBy { it.fcstTime }
.subList(0,10)
)
}
이상으로 Data 패키지를 마치겠습니다.
혹시 궁금하신 거나 더 좋게 짤 수 있는 방법 혹은 조언이 있으시다면 댓글로 알려주시기 바랍니다 감사히 듣겠습니다~
'android > 날씨공공데이터앱' 카테고리의 다른 글
Retrofit + OkHttp + Coroutine 날씨 공공데이터 API 예제 - 3(Domain) (0) | 2022.10.08 |
---|---|
Retrofit이란 무엇이고, Retrofit을 이용한 공공데이터 API ( 날씨 ) + Hilt로 DI + MVVM 구조로 만드는 기본적인 과정의 이해 (0) | 2022.09.28 |