와챠의 우당탕탕 개발 기록장
[안드로이드] 기상청 동네예보 API 활용하기 본문
아래의 앱을 만들어 볼 것이다.
원래는 흰 배경에 검정 글씨인데
지금 내 폰이 다크모드라 저렇게 됐다. 신기...

공공 데이터 포탈에서 아래 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 | |
} | |
} |
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 써보니까 넘 재밌다...
별 거 안 했는데 유용한 정보가 쏙쏙 나오는 이 기분...
땅을 팠는데... 고구마가 나온 기분
'코딩 일기장 > Android(Kotlin)' 카테고리의 다른 글
[안드로이드] 웹 크롤링 (0) | 2021.05.14 |
---|---|
[안드로이드] 안드로이드 XML 데이터 파싱 (0) | 2021.05.14 |
[안드로이드] 카카오 로그인(2) (0) | 2021.05.07 |
[안드로이드] 카카오 로그인 api(1) (0) | 2021.05.07 |
[안드로이드] Firebase 연동(2) (0) | 2021.05.06 |