와챠의 우당탕탕 개발 기록장

[안드로이드] 기상청 동네예보 API 활용하기 본문

코딩 일기장/Android(Kotlin)

[안드로이드] 기상청 동네예보 API 활용하기

minWachya 2021. 5. 9. 16:50
반응형

아래의 앱을 만들어 볼 것이다.

원래는 흰 배경에 검정 글씨인데

지금 내 폰이 다크모드라 저렇게 됐다. 신기...

실행 결과

 


공공 데이터 포탈에서 아래 API를 검색한 후 활용 신청하기!!

 

AndroidManifest.xml 설정하기

 

1)

manifest-application에
android:usesCleartextTraffic="true" 추가

 

2)

인터넷과 네트워크 연결 위해서 아래의 퍼미션

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

추가

 

그러면 일케 된다.

AndroidManifest.xml

 

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myweather">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyWeather">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

 

 

grable(:app) 설정하기

implementation 'com.squareup.retrofit2:retrofit:2.8.0'
implementation 'com.squareup.retrofit2:converter-gson:2.8.0'

추가!

 

 


 

 

일단 코드 먼저... 자세한 설명은 마지막에

 

activity_main.xml

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="오늘의 날씨"
android:textSize="40dp"/>
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TableRow>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="강수 확룔"
android:textSize="20dp"
android:paddingRight="30dp"/>
<TextView
android:id="@+id/tvRainRatio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="20dp"/>
</TableRow>
<TableRow>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="강수 형태"
android:textSize="20dp"
android:paddingRight="30dp"/>
<TextView
android:id="@+id/tvRainType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="20dp"/>
</TableRow>
<TableRow>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="습도"
android:textSize="20dp"
android:paddingRight="30dp"/>
<TextView
android:id="@+id/tvHumidity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="20dp"/>
</TableRow>
<TableRow>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="하늘 상태"
android:textSize="20dp"
android:paddingRight="30dp"/>
<TextView
android:id="@+id/tvSky"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="20dp"/>
</TableRow>
<TableRow>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="기온"
android:textSize="20dp"
android:paddingRight="30dp"/>
<TextView
android:id="@+id/tvTemp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="20dp"/>
</TableRow>
</TableLayout>
<Button
android:id="@+id/btnRefresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="새로고침"/>
</LinearLayout>

 

 

WeatherInterface.kt

 

package com.example.myweather
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
// 결과 xml 파일에 접근해서 정보 가져오기
interface WeatherInterface {
// getVilageFcst : 동네 예보 조회
@GET("getVilageFcst?serviceKey=서비스키 입력")
fun GetWeather(@Query("dataType") data_type : String,
@Query("numOfRows") num_of_rows : Int,
@Query("pageNo") page_no : Int,
@Query("base_date") base_date : String,
@Query("base_time") base_time : String,
@Query("nx") nx : String,
@Query("ny") ny : String)
: Call<WEATHER>
}

 

 

MainActivity.kt

 

package com.example.myweather
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.text.SimpleDateFormat
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.*
// xml 파일 형식을 data class로 구현
data class WEATHER (val response : RESPONSE)
data class RESPONSE(val header : HEADER, val body : BODY)
data class HEADER(val resultCode : Int, val resultMsg : String)
data class BODY(val dataType : String, val items : ITEMS)
data class ITEMS(val item : List<ITEM>)
// category : 자료 구분 코드, fcstDate : 예측 날짜, fcstTime : 예측 시간, fcstValue : 예보 값
data class ITEM(val category : String, val fcstDate : String, val fcstTime : String, val fcstValue : String)
// retrofit을 사용하기 위한 빌더 생성
private val retrofit = Retrofit.Builder()
.baseUrl("http://apis.data.go.kr/1360000/VilageFcstInfoService/")
.addConverterFactory(GsonConverterFactory.create())
.build()
object ApiObject {
val retrofitService: WeatherInterface by lazy {
retrofit.create(WeatherInterface::class.java)
}
}
// 메인 액티비티
class MainActivity : AppCompatActivity() {
lateinit var tvRainRatio : TextView // 강수 확률
lateinit var tvRainType : TextView // 강수 형태
lateinit var tvHumidity : TextView // 습도
lateinit var tvSky : TextView // 하늘 상태
lateinit var tvTemp : TextView // 온도
lateinit var btnRefresh : Button // 새로고침 버튼
var base_date = "20210510" // 발표 일자
var base_time = "1400" // 발표 시각
var nx = "0" // 예보지점 X 좌표
var ny = "0" // 예보지점 Y 좌표
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvRainRatio = findViewById(R.id.tvRainRatio)
tvRainType = findViewById(R.id.tvRainType)
tvHumidity = findViewById(R.id.tvHumidity)
tvSky = findViewById(R.id.tvSky)
tvTemp = findViewById(R.id.tvTemp)
btnRefresh = findViewById(R.id.btnRefresh)
// nx, ny지점의 날씨 가져와서 설정하기
setWeather(nx, ny)
// <새로고침> 버튼 누를 때 날씨 정보 다시 가져오기
btnRefresh.setOnClickListener {
setWeather(nx, ny)
}
}
// 날씨 가져와서 설정하기
fun setWeather(nx : String, ny : String) {
// 준비 단계 : base_date(발표 일자), base_time(발표 시각)
// 현재 날짜, 시간 정보 가져오기
val cal = Calendar.getInstance()
base_date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(cal.time) // 현재 날짜
val time = SimpleDateFormat("HH", Locale.getDefault()).format(cal.time) // 현재 시간
// API 가져오기 적당하게 변환
base_time = getTime(time)
// 동네예보 API는 3시간마다 현재시간+4시간 뒤의 날씨 예보를 알려주기 때문에
// 현재 시각이 00시가 넘었다면 어제 예보한 데이터를 가져와야함
if (base_time >= "2000") {
cal.add(Calendar.DATE, -1).toString()
base_date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(cal.time)
}
// 날씨 정보 가져오기
// (응답 자료 형식-"JSON", 한 페이지 결과 수 = 10, 페이지 번호 = 1, 발표 날싸, 발표 시각, 예보지점 좌표)
val call = ApiObject.retrofitService.GetWeather("JSON", 10, 1, base_date, base_time, nx, ny)
// 비동기적으로 실행하기
call.enqueue(object : retrofit2.Callback<WEATHER> {
// 응답 성공 시
override fun onResponse(call: Call<WEATHER>, response: Response<WEATHER>) {
if (response.isSuccessful) {
// 날씨 정보 가져오기
var it: List<ITEM> = response.body()!!.response.body.items.item
var rainRatio = "" // 강수 확률
var rainType = "" // 강수 형태
var humidity = "" // 습도
var sky = "" // 하능 상태
var temp = "" // 기온
for (i in 0..9) {
when(it[i].category) {
"POP" -> rainRatio = it[i].fcstValue // 강수 기온
"PTY" -> rainType = it[i].fcstValue // 강수 형태
"REH" -> humidity = it[i].fcstValue // 습도
"SKY" -> sky = it[i].fcstValue // 하늘 상태
"T3H" -> temp = it[i].fcstValue // 기온
else -> continue
}
}
// 날씨 정보 텍스트뷰에 보이게 하기
setWeather(rainRatio, rainType, humidity, sky, temp)
// 토스트 띄우기
Toast.makeText(applicationContext, it[0].fcstDate + ", " + it[0].fcstTime + "의 날씨 정보입니다.", Toast.LENGTH_SHORT).show()
}
}
// 응답 실패 시
override fun onFailure(call: Call<WEATHER>, t: Throwable) {
Log.d("api fail", t.message.toString())
}
})
}
// 텍스트 뷰에 날씨 정보 보여주기
fun setWeather(rainRatio : String, rainType : String, humidity : String, sky : String, temp : String) {
// 강수 확률
tvRainRatio.text = rainRatio + "%"
// 강수 형태
var result = ""
when(rainType) {
"0" -> result = "없음"
"1" -> result = "비"
"2" -> result = "비/눈"
"3" -> result = "눈"
"4" -> result = "소나기"
"5" -> result = "빗방울"
"6" -> result = "빗방울/눈날림"
"7" -> result = "눈날림"
else -> "오류"
}
tvRainType.text = result
// 습도
tvHumidity.text = humidity + "%"
// 하능 상태
result = ""
when(sky) {
"1" -> result = "맑음"
"3" -> result = "구름 많음"
"4" -> result = "흐림"
else -> "오류"
}
tvSky.text = result
// 온도
tvTemp.text = temp + "°"
}
// 시간 설정하기
// 동네 예보 API는 3시간마다 현재시각+4시간 뒤의 날씨 예보를 보여줌
// 따라서 현재 시간대의 날씨를 알기 위해서는 아래와 같은 과정이 필요함. 자세한 내용은 함께 제공된 파일 확인
fun getTime(time : String) : String {
var result = ""
when(time) {
in "00".."02" -> result = "2000" // 00~02
in "03".."05" -> result = "2300" // 03~05
in "06".."08" -> result = "0200" // 06~08
in "09".."11" -> result = "0500" // 09~11
in "12".."14" -> result = "0800" // 12~14
in "15".."17" -> result = "1100" // 15~17
in "18".."20" -> result = "1400" // 18~20
else -> result = "1700" // 21~23
}
return result
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

 

 

 


 

 

 

1, MainActivity에서 32번째 줄에 사용된 링크는 공공 데이터 포탈에서 아래 주소를 복붙하면 된다.

baseUrl("http://apis.data.go.kr/1360000/VilageFcstInfoService/")

 

 

2, 나는 이중에 "동네 예보 조회"를 사용할 것이기 때문에 WeatherInterface의 10번째 줄에서 getVilageFcst를 적었다.

@GET("getVilageFcst?serviceKey=D")

 

 

3, 데이터를 요청할 때 아래의 정보가 필요한데 이 작업은 WeatherInterface에서 한다.

 

 

4, 요청받은 데이터는 아래의 응답을 주는데, 나는 노란 밑줄친 부분만 필요해서 MainActivity에 

data class ITEM(val category : String, val fcstDate : String, val fcstTime : String, val fcstValue : String)

위와 같이 적었다.

 

 

 

5, 응답 메시지의 xml은 아래와 같은데 이를 data class로 표현해주기 위해 아래와 같이 표현했다.

태그의 관계를 잘 살펴보면 이해하기 쉽다!

 

response 안에 header, body가 있고...heraer 안에 resultCode와 resultMsg가 있고... 이런 식이다.

data class WEATHER (val response : RESPONSE)
data class RESPONSE(val header : HEADER, val body : BODY)
data class HEADER(val resultCode : Int, val resultMsg : String)
data class BODY(val dataType : String, val items : ITEMS)
data class ITEMS(val item : List<ITEM>)

 

 

6, item에는 아래의 정보가 담겨져 오는데, 나는 노란색 부분만 필요해서

이렇게 표현해줬다.

 

for (i in 0..9) {
                        when(it[i].category) {
                            "POP" -> rainRatio = it[i].fcstValue    // 강수 기온
                            "PTY" -> rainType = it[i].fcstValue     // 강수 형태
                            "REH" -> humidity = it[i].fcstValue     // 습도
                            "SKY" -> sky = it[i].fcstValue          // 하늘 상태
                            "T3H" -> temp = it[i].fcstValue         // 기온
                            else -> continue
                        }

                    }

 

 

7, 아래의 정보로 강수 형태와 하늘 상태에 대한 string값을 정했다.

	// 강수 형태
	when(rainType) {
            "0" -> result = "없음"
            "1" -> result = "비"
            "2" -> result = "비/눈"
            "3" -> result = "눈"
            "4" -> result = "소나기"
            "5" -> result = "빗방울"
            "6" -> result = "빗방울/눈날림"
            "7" -> result = "눈날림"
            else -> "오류"
        }

        // 하늘 상태
        when(sky) {
            "1" -> result = "맑음"
            "3" -> result = "구름 많음"
            "4" -> result = "흐림"
            else -> "오류"
        }

 

 

8, 그리고 가장 헷갈렸던 건데 이 baseTime(예보자료 시각)과 fcstTime(발표 시간),,,

 

아래의 표를 보면 "발표시간 = 현재 시각 + 4"이란 것을 할 수 있다.

그니끼 현재 시간으로 데이터를 요청하면 현재시간대 + 4시간의 예보를 얻을 수 있는 것이다.

 

ex) 현재 시각이 0200이면,  +4시간이 된 0600의 예보를 받음

(2시 예보가 아닌 6시의 예보를 미리 받는 것)

 

그런데 나는 다음 시간대의 예보를 미리 받아보고 싶은 게 아닌 현재 시간대의 예보를 알고 싶어서 아래의 함수로 설정을 다시 해줬다.

만약 내가 6~8시 사이에 있고 6시 예보를 받고 싶다면 4시간 전인 2시로 baseTime을 설정해주어야 함...을 나타내는 표

base time fcst time 현재 시간대
0200 0600 06~08
0500 0900 09~11
0800 1200 12~14
1100 1500 15~17
1400 1800 18~20
1700 2100 21~23
2000 0000 00~02
2300 0300 03~05

 

fun getTime(time : String) : String {
        var result = ""
        when(time) {
            in "00".."02" -> result = "2000"    // 00~02
            in "03".."05" -> result = "2300"    // 03~05
            in "06".."08" -> result = "0200"    // 06~08
            in "09".."11" -> result = "0500"    // 09~11
            in "12".."14" -> result = "0800"    // 12~14
            in "15".."17" -> result = "1100"    // 15~17
            in "18".."20" -> result = "1400"    // 18~20
            else -> result = "1700"             // 21~23
        }
        return result
    }

 

 

 

그리고 현재 시각에 대한 정보를 얻으려면 현재시각 + 4시간을 해야하다보니까

오늘의 예보시각(fcst time) 0000의 예보를 보기 위해선 어제의 현재시각(base time) 2000 예보가 필요하다,,,

위 표의 이부분

base time (현재 시각) fcst time (예보 시각)
20000 0000
2300 0300

그래서 base_date도 하루를 빼주는....^^!!!

if (base_time >= "2000") {
            cal.add(Calendar.DATE, -1).toString()
            base_date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(cal.time)
        }

 

 

API 써보니까 넘 재밌다...

별 거 안 했는데 유용한 정보가 쏙쏙 나오는 이 기분...

땅을 팠는데... 고구마가 나온 기분

반응형
Comments