본문 바로가기

major/Android

[Kotlin] 공공 데이터 사용하기 - 공적 마스크

전 세계가 코로나 바이러스로 난리입니다. 이런 전염병에서 자신을 보호하기 위해서는 외출후 손씻기와 외출시 마스크가 필수입니다.

하지만 1월 말 우한사태 이후로 마스크 가격이 폭등했고, 신천지 사태 이후로는 마스크 구하기가 불가능했습니다.

정부에서는 이 문제를 해결하기 위해 공장에서 생산한 마스크를 전량 매입 후 공적 마스크로 풀고있습니다.

초반에는 입고 시간이 일정치 않고 줄을 오래서야해서 힘들었지만, 지금은 공공 데이터로 공적 마스크 판매처, 입고 시간등을 풀고있어서 마스크 구입이 쉬워졌습니다.

저도 개발자로써 API가  어떤 정보를 제공하고 어떻게 가져오는건지 궁금해서 사용해 봤습니다.

포스팅에서는 코틀린 언어를 이용해 공적 마스크 데이터를 받아오고 앱으로 보여주는 방법을 정리했습니다.

1. API 문서

공공 데이터 포털의 공적 마스크 판매 정보 바로가기

위 사이트에 들어가면 참고 문서를 Link로 제공합니다.

 

API 상세 정보 바로가기

위 사이트에서는 API 정보를 상세하게 제공합니다.

공적 마스크 API는 stores, sales, storesbygeo, storesbyaddr 4개가 있는데,

stores는 마스크 판매처 정보, sales는 마스크 재고 정보, storesbygeo는 위도,경도,최대 반경 내에있는 마스크 판매처 및 재고정보, storesbyaddr는 주소를 기준으로 해당 동or구 내에 있는 마스크 판매처 및 재고 정보를 제공합니다.

 

stores 정보를 가져오는 url은 다음과 같습니다.

https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1/stores/json?page=1

결과 화면 캡쳐

태그별 정보의 의미를 참고 문서 link에 아래와 같이 나와있습니다.

 

태그 정보

이제 storesbygeo를 사용 해서 주소 정보를 입력하고 그 동or구 내의 판매처 및 재고정보를 받아오겠습니다.

 

2. 소스 코드

주소 정보를 입력하고, 버튼을 클릭하면 API에서 데이터를 받아 ListView에 보여주도록 소스코드를 짰습니다.

최종 실행 화면

<!-- 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edittext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="부산광역시 금정구 장전동"/>


    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        android:onClick="onClickButton"/>

    <ListView
        android:id="@+id/listview"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"/>

</LinearLayout>
<!-- listview.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/listview_text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="storeName"
        android:textSize="30dp"/>

    <TextView
        android:id="@+id/listview_text2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="remain_stat"/>

</LinearLayout>

ListViewItem.kt와 ListViewAdapter.kt 파일을 생성하고 아래의 코드를 복붙합니다.

// ListViewItem.kt
class ListViewItem {

    private var storeName: String? = null
    private var remain_stat: String? = null

    constructor(storeName: String, remain_stat: String){
        this.storeName = storeName
        this.remain_stat = remain_stat
    }

    fun setStoreName(storeName: String) {
        this.storeName = storeName
    }

    fun setRemainStat(remain_stat: String) {
        this.remain_stat = remain_stat
    }

    fun getStoreName(): String? {
        return this.storeName
    }

    fun getRemainStat(): String? {
        return this.remain_stat
    }
}
// ListViewAdapter.kt
class ListViewAdapter : BaseAdapter() {

    var list = ArrayList<ListViewItem>()

    override fun getCount(): Int {
        return list.size
    }

    override fun getItemId(position: Int) : Long {
        return position.toLong()
    }

    override fun getItem(position: Int): Any {
        return list.get(position)
    }

    fun getSize(): Int {
        return list.size
    }

    fun addItem(storeName: String, remain_stat: String) {
        var item = ListViewItem(storeName, remain_stat)
        list.add(item)

        println("in ListViewAdapter --> listSize : "+list.size)
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
        var context = parent?.getContext()
        var convertV = convertView

        if (convertView == null) {
            val systemService =
                context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            convertV = systemService.inflate(R.layout.listview,parent,false)
        }

        var storeName = convertV?.findViewById(R.id.listview_text1) as TextView
        var remain_stat = convertV?.findViewById(R.id.listview_text2) as TextView

        val listViewItem = list[position]

        storeName.text = listViewItem.getStoreName()
        remain_stat.text = listViewItem.getRemainStat()

        return convertV

    }

}

그리고 공적 마스크 데이터를 가져오기 위한 ApiPublicMask.kt 파일을 생성하고 아래의 코드를 복붙합니다.

class ApiPublicMask(addr: String){

    var addr: String

    init{
        this.addr = addr
    }

    var adapter : ListViewAdapter = ListViewAdapter()
    
    val storesURL = "https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1/stores/json"
    val salesURL = "https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1/sales/json"
    val storesbyaddrURL = "https://8oi9s0nnth.apigw.ntruss.com/corona19-masks/v1/storesByAddr/json"

    fun main(): ListViewAdapter{

        var text: String? = null
        try {
            text = URLEncoder.encode(addr, "UTF-8")
        } catch (e: UnsupportedEncodingException) {
            throw RuntimeException("검색어 인코딩 실패", e)
        }

        val url = "$storesbyaddrURL?address=$text"
        val responseBody = get(url)
        parseData(responseBody)

        return adapter
    }

    private operator fun get(apiUrl: String): String {
        var responseBody: String = ""
        try {
            val url = URL(apiUrl)
            val `in` = url.openStream()
            responseBody = readBody(`in`)

        } catch (e: MalformedURLException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }

        return responseBody
    }

    private fun readBody(body: InputStream): String {
        val streamReader = InputStreamReader(body)

        try {
            BufferedReader(streamReader).use({ lineReader ->
                val responseBody = StringBuilder()

                var line: String? = lineReader.readLine()
                while ( line != null) {
                    responseBody.append(line)
                    line = lineReader.readLine()
                }
                return responseBody.toString()
            })
        } catch (e: IOException) {
            throw RuntimeException("API 응답을 읽는데 실패했습니다.", e)
        }

    }

    private fun parseData(responseBody: String) {
        var storeName: String
        var remain_stat : String
        var jsonObject = JSONObject()
        try {
            jsonObject = JSONObject(responseBody)
            val jsonArray = jsonObject.getJSONArray("stores")

            for (i in 0 until jsonArray.length()) {
                val item = jsonArray.getJSONObject(i)
                storeName = item.getString("name")
                remain_stat = item.getString("remain_stat")
                println("storeName : $storeName")
                println("remain_stat : $remain_stat")

                adapter.addItem(storeName, remain_stat)

            }
        } catch (e: JSONException) {
            e.printStackTrace()
        }
    }
}

그리고 MainActivity.kt 파일을 다음과 같이 수정합니다.

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var editText: EditText
    private lateinit var listView : ListView
    var adapter : ListViewAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        listView = findViewById(R.id.listview)
        editText = findViewById(R.id.edittext)

    }

    fun onClickButton(view: View){
        val thread = Thread({
            var apiPublicMask = ApiPublicMask(editText.text.toString())
            adapter = apiPublicMask.main()
            runOnUiThread {
                listView.adapter = adapter
            }
        }).start()
    }

}

 

Kotlin을 공부한지 몇일 안되서 코드 짤 때 삽질을 많이 했습니다. 아직 Kotlin의 Class가 Java와 어떻게 다른건지 잘 몰라서 어려웠습니다.

코드는 ListView와 HttpURLConnection 관련해서 조금만 공부해도 쉽게 알 수 있을거라고 생각해서 설명을 더 적지 않았습니다.

저도 코드 짤 때 주석 다는걸 연습하고 있는데, 아직은 제가 이해한걸 머릿속에서 글로 꺼낸다는게 쉬운일이 아니어서 다 지웠습니다.

코드를 보고 이해안가는 부분이 있다면 물어봐주세요.

 

공공 데이터에서 위도, 경도 데이터도 제공하기 때문에, 받아온 정보를 구글 맵에 띄워보는 것도 해봤습니다.

구글 맵에 띄우는건 다음 포스팅에서 정리해보도록 하겠습니다.

 

++추가

AndroidManifest.xml 파일에 INTERNET permission을 줘야 합니다.

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

잘못된 내용이 있다면 언제든지 댓글이나 메일로 알려주시면 감사하겠습니다.

이 포스팅이 도움이 되었다면 공감 부탁드립니다.

궁금한 점은 언제든지 댓글 남겨주시면 답변해드리겠습니다:D