본문 바로가기

Android

[ 안드로이드 - Kotlin ] retrofit을 사용한 공공데이터 포털 데이터 받기 ( 코틀린 )

728x90

안녕하세요! 다들 코로나 조심하고 계신가요? 최근 들어 백신에 대한 이슈가 많아서 공공데이터 포털에 있는 "공공데이터활용지원센터_코로나19 예방접종 통계 데이터 조회 서비스" api를 활용하여 간단히 화면에 띄우는 코드를 올려보도록 하겠습니다.

 

바로 코드를 보시고 싶은 분들은 맨 아래로 이동하여서 [전체 코드] 탭이나 제 github링크를 통하여 확인하시면 됩니다.

[ 준비 사항 ]

준비

레트로핏 사용전 우선 api가 있어야겠죠?

공공 데이터 포털에서 원하시는 api를 신청하시면 키를 알려줍니다 해당 키는 api호출 시 꼭 필요함으로 미리 신청해주시면 됩니다.

예제로 바로 설명을 위해 여기서는 따로 키 받는 법에 대한 이야기는 하지 않도록 하겠습니다.

API 정보 저장

api key를 받았다면 local.properties에 저장을 해줍니다. 해당 키는 본인만 알고 써야 하기 때문에 따로 노출을 하면 안 됩니다.

추가로 api의 url과 endpoint 역시 추가해줍니다.

local.properties

build.gradle(:app)에 키 등록

local.properties에서 저장한 키들을 사용하기 위해서는 build.gradle(:app)에서 따로 지정해 주어야 합니다.

저는 코드에서 사용할 이름으로 URL_VACCINE, ENDPOINT_GET_VACCINE_STATUS, API_KEY 3가지를 android > defaultConfig에 아래와 같이 추가해줬습니다.

build.gradle(:app)

build.gradle(:app)에 retrofit사용을 위해 dependencies추가

retrofit 사용과 json형식의 데이터를 받기 위해 아래 두줄의 코드를 추가해줍니다.

build.gradle(:app)

AndroidManifest.xml에 권한 추가

인터넷 사용을 위해 INTERNET권한도 manifest.xml에 추가해줍니다.

AndroidManifest.xml

 

이렇게 위의 과정을 모두 끝내셨으면 이제 api를 사용할 준비가 됐습니다.

key 저장에 대한 자세한 정보는 여기서 보실 수 있습니다.


[데이터 클래스]

api로 들어오는 데이터의 형식을 받을 클래스를 만들 것입니다.

해당 api의 response 형식은 신청하신 데이터 홈페이지에 문서나 swagger를 통해서 나와있으니 참고를 하시면서 만들면 됩니다.

공공데이터 포털에 있는 api response정보

위의 항목을 참고하여 아래 사진과 같이 DataClass를 작성하였습니다.

DataClass.kt

DataClass에서 우선 body에 오는 데이터를 받는 VaccineBody와 배열로 오는 data를 받는 Vaccine data class 이렇게 2개를 만들어 주었습니다. 만들면서 나중에 데이터를 좀 더 보기 쉽게 표현하기 위하여 toString함수를 재정의 하였습니다.

 

JSON 데이터를 받을 클래스를 작성 시 @SerializedName를 통하여 코드에서 사용할 이름을 변경하여 사용할 수 있습니다. 꼭 이름을 변경하지 않더라도 @SerializedName를 써주어야 한다고 합니다.

저의 경우는 29번째 라인에 있는 sido만 area로 변경해 주었습니다.


[retrofit 인터페이스 & 객체]

ApiService.kt

ApiService interface에서는 @GET 어노테이션에는 전에 local.properties에 적어둔 endpoint를 적고 사용할 쿼리 파라미터들을 getInfo 함수 인자들로 적어주시면 됩니다. 마지막으로 반환 타입은 Call <전에 작성한 data class> 형식으로 적어주시면 됩니다.

RetrofitObject.kt

이제 retrofit을 사용하기 위해 RetrofitObject를 만들어보려고 합니다.

먼저 getRetrofit()에서 사용할 기본 URL(local.properties에 작성했던 url) 설정과 json데이터를 받기 위해 GsonConverterFactory를 설정해 주고 반환해줍니다.

이제 api service를 받아서 사용할 수 있도록 getApiService 함수를 다음과 같이 작성하여 반환해 줍니다.


[MainActivity.kt]

MainActivity.kt 안에 getVaccineStatus()함수

* MainActivity는 너무 길어서 여기서는 api 호출하는 함수만 따로 캡처하였습니다. 아래쪽에 전체 코드로 가시면 전체 코드 확인하실 수 있습니다.

이제 api를 사용할 곳에서 ApiService인터페이스를 통해 요청을 보내고 Callback을 통해 응답을 받아서 처리하면 됩니다.

onResponse에서는 성공 시 진행할 작업을 작성하고 onFailure에서는 실패 시 작업을 작성하면 됩니다.


[실행화면]

초기에 앱 실행 날짜가 2021-09-16이고 16일 이후를 선택하면 아직 데이터가 없다고 나오는 것을 확인할 수 있습니다.

실행화면

[전체 코드]

DataClass.kt

import com.google.gson.annotations.SerializedName

data class VaccineBody(
    @SerializedName("currentCount") val currentCount:Int,   // 현재 검색된 데이터 수
    @SerializedName("data") val data:List<Vaccine>,         // 백신 현황 데이터
    @SerializedName("matchCount") val matchCount:Int,       // 검색과 일치하는 데이터 수
    @SerializedName("page") val page:Int,                   // 데이터 페이지
    @SerializedName("perPage") val perPage:Int,             // 한번에 불러올 데이터
    @SerializedName("totalCount") val totalCount:Int        // 데이터 전체 개수
){
    override fun toString(): String {
        return "$data\n\n" +
                "currentCount : $currentCount\n" +
                "matchCount : $matchCount\n" +
                "page : $page\n" +
                "perPage : $perPage\n" +
                "totalCount : $totalCount"
    }
}

data class Vaccine(
    @SerializedName("accumulatedFirstCnt") val accumulatedFirstCnt:Int,     // 전일까지의 누적 통계 1차
    @SerializedName("accumulatedSecondCnt") val accumulatedSecondCnt:Int,   // 전일까지의 누적 통계 2차
    @SerializedName("baseDate") val baseDate:String,                        // 통계 기준일자
    @SerializedName("firstCnt") val firstCnt:Int,                           // 당일 통계 1차
    @SerializedName("secondCnt") val secondCnt:Int,                         // 당일 통계 2차
    @SerializedName("sido") val area:String,                                // 지역명칭
    @SerializedName("totalFirstCnt") val totalFirstCnt:Int,                 // 전체 누적 통계 1차
    @SerializedName("totalSecondCnt") val totalSecondCnt:Int                // 전체 누적 통계 2차
){
    override fun toString(): String {
        return "Vaccine : [\n" +
                "    accumulatedFirstCnt : ${accumulatedFirstCnt}\n" +
                "    accumulatedSecondCnt : ${accumulatedSecondCnt}\n" +
                "    baseDate : ${baseDate}\n" +
                "    firstCnt : ${firstCnt}\n" +
                "    secondCnt : ${secondCnt}\n" +
                "    area : ${area}\n" +
                "    totalFirstCnt : ${totalFirstCnt}\n" +
                "    totalSecondCnt : ${totalSecondCnt}]\n\n"
    }
}

ApiService.kt

import retrofit2.http.GET
import retrofit2.Call
import retrofit2.http.Query

interface ApiService {
    @GET(BuildConfig.ENDPOINT_GET_VACCINE_STATUS)
    fun getInfo(
        @Query("perPage")PerPage:Int,
        @Query("page")Page:Int,
        @Query("cond[baseDate::EQ]")FindDate:String,
        @Query("serviceKey")ServiceKey:String = BuildConfig.API_KEY
    ):Call<VaccineBody>
}

RetrofitObject

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitObject{
    private fun getRetrofit():Retrofit{
        return Retrofit.Builder()
            .baseUrl(BuildConfig.URL_VACCINE)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    fun getApiService():ApiService{
        return getRetrofit().create(ApiService::class.java)
    }
}

MainActivity.kt

import android.app.DatePickerDialog
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.DatePicker
import android.widget.Toast
import net.ddns.yline.vaccinestatus.databinding.ActivityMainBinding
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    // 오늘 날짜 설정
    private var findDate = SimpleDateFormat("yyyy-MM-dd 00:00:00", Locale.getDefault()).format(Date(System.currentTimeMillis()))

    // === 생명주기 ===
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        // 리스너 등록
        setListener()
        // 액티비티 생성시 오늘 날짜로 백신정보 불러옴
        getVaccineStatus()
        binding.textviewDate.text = findDate
    }

    // === 기능 ===
    // 리스너 등록
    private fun setListener(){
        with(binding){
            ClickListener().also {
                buttonGet.setOnClickListener(it)
                buttonDateDialog.setOnClickListener(it)
            }
        }
    }

    // 백신 정보 불러오기
    private fun getVaccineStatus(){
        Log.d("print findDate",findDate)
        RetrofitObject.getApiService().getInfo(20, 1, findDate).enqueue(object : Callback<VaccineBody>{
            // api 호출 성공시
            override fun onResponse(call: Call<VaccineBody>, response: Response<VaccineBody>) {
                setResponseText(response.code(), response.body())
                Toast.makeText(applicationContext, "success", Toast.LENGTH_SHORT).show()
            }

            // api 호출 실패시
            override fun onFailure(call: Call<VaccineBody>, t: Throwable) {
                Log.e("retrofit onFailure", "${t.printStackTrace()}")
                Toast.makeText(applicationContext, "fail", Toast.LENGTH_SHORT).show()
            }
        })
    }

    private fun setResponseText(resCode:Int, body:VaccineBody?){
        // 상태별 text 지정
        binding.textviewResponse.text = when(resCode){
            200 -> {
                if(body == null){
                    "api body가 비어있습니다."
                }else{
                    if(body.data.toString() == "[]"){
                        "api호출은 성공했으나 해당 날짜에 데이터가 없습니다."
                    }else{
                        body.toString()
                    }
                }
            }
            400 -> {
                "API 키가 만료됬거나 쿼리 파라미터가 잘못 됬습니다."
            }
            401 -> {
                "인증 정보가 정확하지 않습니다."
            }
            500 -> {
                "API 서버에 문제가 발생하였습니다."
            }
            else -> {
                "기타 문제발생..."
            }
        }
    }

    // 날짜 선택 다이얼로그 생성
    private fun dateDialog(){
        val setDate = findDate
            .substring(0, findDate.indexOf(" "))
            .split("-")
            .map { it.toInt() }.toIntArray()
        DatePickerDialog(this, DateSetListener(), setDate[0], setDate[1]-1, setDate[2]).show()
    }

    // === 이밴트 클래스 ===
    // 버튼 클릭 리스너
    inner class ClickListener:View.OnClickListener{
        override fun onClick(v: View?) {
            binding.run {
                when(v?.id){
                    buttonGet.id -> getVaccineStatus()
                    buttonDateDialog.id -> dateDialog()
                    else -> Log.e("click listener null", "onClick view is null")
                }
            }
        }
    }

    // 날자 선택 리스너
    inner class DateSetListener:DatePickerDialog.OnDateSetListener{
        override fun onDateSet(p0: DatePicker, p1: Int, p2: Int, p3: Int) {
            findDate = String.format("%d-%02d-%02d 00:00:00",p1, p2+1, p3)
            binding.textviewDate.text = findDate
            getVaccineStatus()
        }
    }
}

 

github 링크

 

GitHub - yline0808/VaccineStatus

Contribute to yline0808/VaccineStatus development by creating an account on GitHub.

github.com

제가 잘못 알고 있거나 잘못된 부분이 있을 경우 알려주시고 추가로 궁금한 점 있으신 분들도 댓글이나 메일 주시면 성실히 답변해 드리겠습니다.🧑🏻‍💻

감사합니다~😄

728x90