๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ“ฑ/๐Ÿ“˜Project

[Kotlin] ๋„์„œ ๊ฒ€์ƒ‰ ์•ฑ ๋งŒ๋“ค๊ธฐ๏ผ’

ViewBinding๊ณผ Room, Retrofit2๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ค๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ฑ…์˜ ์ด๋ฏธ์ง€๋Š” url๋กœ ๋“ค์–ด์˜ค๋Š”๋ฐ ์ด url์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด Glide๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

build.gradle (Module)
plugins {
    ...
    id 'kotlin-kapt'
}

android {
    ...

    buildFeatures {
        viewBinding = true
    }
}

dependencies {

    ...

    // Retrofit2
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

    // Glide
    implementation 'com.github.bumptech.glide:glide:4.12.0'

    // Room
    def roomVersion = "2.4.2"

    implementation "androidx.room:room-ktx:$roomVersion"
    kapt "androidx.room:room-compiler:$roomVersion"
}

 

Retrofit2๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ธํ„ฐ๋„ท ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด์ค˜์•ผ ํ•œ๋‹ค.

 

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.ta2gi.searchbook">

    <!-- ์ธํ„ฐ๋„ท ๊ถŒํ•œ -->
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        ...
    </application>

</manifest>

 

MainActivity.kt
package com.ta2gi.searchbook

import android.app.Activity
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.snackbar.Snackbar
import com.ta2gi.searchbook.adapter.HistoryAdapter
import com.ta2gi.searchbook.adapter.SearchAdapter
import com.ta2gi.searchbook.databinding.ActivityMainBinding
import com.ta2gi.searchbook.fragment.HomeFragment
import com.ta2gi.searchbook.fragment.SearchFragment
import com.ta2gi.searchbook.retrofit2.BookDto
import com.ta2gi.searchbook.retrofit2.KakaoData
import com.ta2gi.searchbook.retrofit2.KakaoInfo.Companion.API_KEY
import com.ta2gi.searchbook.retrofit2.RetrofitService.kakaoService
import com.ta2gi.searchbook.room.HistoryDatabase
import com.ta2gi.searchbook.room.HistoryEntity
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainActivity : AppCompatActivity() {

    // ๋ทฐ๋ฐ”์ธ๋”ฉ
    lateinit var mainBinding : ActivityMainBinding

    // ํ”„๋ž˜๊ทธ๋จผํŠธ๋ฅผ ๋‹ด์„ ๋ณ€์ˆ˜
    lateinit var currentFragment : Fragment

    // ๊ฒ€์ƒ‰์–ด ์ €์žฅ ์šฉ๋„
    var searchWord = ""

    // ๊ฒ€์ƒ‰ ๊ธฐ๋ก, ๊ฒ€์ƒ‰ ๋ฆฌ์ŠคํŠธ
    var historyList = mutableListOf<HistoryEntity>()
    var searchList = mutableListOf<BookDto>()

    // ์–ด๋Žํ„ฐ
    val searchAdapter = SearchAdapter(this, searchList)

    // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
    lateinit var historyDatabase : HistoryDatabase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ๋ทฐ๋ฐ”์ธ๋”ฉ ์ดˆ๊ธฐํ™”
        mainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainBinding.root)

        // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”
        historyDatabase = HistoryDatabase.getInstance(this)!!

        // ์‹คํ–‰ ์‹œ ์ฒซ ํ™”๋ฉด HomeFragment
        fragmentController("home", false)
    }

    // ํ”„๋ž˜๊ทธ๋จผํŠธ
    fun fragmentController(view: String, add: Boolean) {
        // ๋“ค์–ด์˜ค๋Š” view ๊ฐ’์— ๋”ฐ๋ผ ํ”„๋ž˜๊ทธ๋จผํŠธ ๋ณ€๊ฒฝ
        when (view) {
            "home" -> currentFragment = HomeFragment(this)
            "search" -> currentFragment = SearchFragment(this)
        }

        // ํ”„๋ž˜๊ทธ๋จผํŠธ ๊ต์ฒด ํ•ด์ฃผ๊ธฐ
        val fragmentTransaction = supportFragmentManager.beginTransaction()
        fragmentTransaction.replace(R.id.act_main_screen, currentFragment)

        // ๋ฐฑ์Šคํ…์— ์ถ”๊ฐ€
        if (add) fragmentTransaction.addToBackStack(view)

        // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ ๋„ฃ๊ธฐ
        fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)

        // ํ”„๋ž˜๊ทธ๋จผํŠธ ์‹คํ–‰
        fragmentTransaction.commit()
    }

    // ์žํŒ ๋‚ด๋ฆฌ๊ธฐ
    fun hideKeyboard(act : Activity){
        val imm = act.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(act.currentFocus?.windowToken, 0)
    }

    // ๋ ˆํŠธ๋กœํ•
    fun searchResults(searchWord : String, fragment : SearchFragment) {
        val book = kakaoService.getSearchBook(API_KEY, searchWord)

        book.enqueue(object: Callback<KakaoData> {
            override fun onResponse(call: Call<KakaoData>, response: Response<KakaoData>) {
                // ํ†ต์‹ ์— ์„ฑ๊ณต ์‹œ ๋ฆฌ์ŠคํŠธ์— ์ฑ…๋“ค ๋‹ด๊ณ  ์–ด๋Žํ„ฐ์— ๋ณ€๊ฒฝ์‚ฌํ•ญ ์•Œ๋ ค์ฃผ๊ธฐ
                searchList.clear()
                searchList.addAll(response.body()!!.documents)
                searchAdapter.notifyDataSetChanged()

                // ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด empty ๋ณด์—ฌ์ฃผ๊ธฐ
                if(searchList.isEmpty()) {
                    fragment.searchBinding.fraSeaEmptySearchList.visibility = View.VISIBLE
                    fragment.searchBinding.fraSeaSearchList.visibility = View.GONE
                } else { // ๋ฆฌ์ŠคํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ๋ณด์—ฌ์ฃผ๊ธฐ
                    fragment.searchBinding.fraSeaSearchList.visibility = View.VISIBLE
                    fragment.searchBinding.fraSeaEmptySearchList.visibility = View.GONE
                }
            }

            override fun onFailure(call: Call<KakaoData>, t: Throwable) {
                Log.d("๋กœ๊ทธ", "ํ†ต์‹  ์‹คํŒจ : ${t.message}")
            }
        })
    }
}
HomeFragment.kt
package com.ta2gi.searchbook.fragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.snackbar.Snackbar
import com.ta2gi.searchbook.MainActivity
import com.ta2gi.searchbook.R
import com.ta2gi.searchbook.adapter.HistoryAdapter
import com.ta2gi.searchbook.databinding.FragmentHomeBinding
import com.ta2gi.searchbook.room.HistoryEntity
import kotlin.concurrent.thread

class HomeFragment(val mainActivity : MainActivity) : Fragment(), View.OnClickListener {

    // ๋ทฐ๋ฐ”์ธ๋”ฉ
    lateinit var homeBinding : FragmentHomeBinding

    // ์–ด๋Žํ„ฐ
    val historyAdapter = HistoryAdapter(mainActivity, mainActivity.historyList, this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // ๋ทฐ๋ฐ”์ธ๋”ฉ ์ดˆ๊ธฐํ™”
        homeBinding = FragmentHomeBinding.inflate(inflater)

        // ์‹คํ–‰ ์‹œ ์žํŒ ์˜ฌ๋ผ์˜ค๋Š” ํ˜„์ƒ ๋ง‰๊ธฐ
        homeBinding.fraHomSearchText.clearFocus()
        mainActivity.hideKeyboard(mainActivity)

        // ๊ฒ€์ƒ‰ ๊ธฐ๋ก ๋„์šฐ๊ธฐ
        thread {
            homeBinding.fraHomHistoryList.adapter = historyAdapter

            mainActivity.historyList.clear()
            mainActivity.historyList.addAll(mainActivity.historyDatabase.historyDao().getHistory())
            historyAdapter.notifyDataSetChanged()

            // ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด empty ๋ณด์—ฌ์ฃผ๊ธฐ
            if(mainActivity.historyList.isEmpty()) {
                mainActivity.runOnUiThread {
                    homeBinding.fraHomEmptyHistory.visibility = View.VISIBLE
                    homeBinding.fraHomHistory.visibility = View.GONE
                }
            } else { // ๋ฆฌ์ŠคํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ๋ณด์—ฌ์ฃผ๊ธฐ
                mainActivity.runOnUiThread {
                    homeBinding.fraHomHistory.visibility = View.VISIBLE
                    homeBinding.fraHomEmptyHistory.visibility = View.GONE
                }
            }
        }

        homeBinding.fraHomSearchButton.setOnClickListener(this)
        homeBinding.fraHomHistoryDeleteAll.setOnClickListener(this)

        return homeBinding.root
    }

    override fun onClick(view : View?) {
        when(view) {
            // ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ
            homeBinding.fraHomSearchButton -> {
                // ๊ฒ€์ƒ‰์–ด ๋ณ€์ˆ˜์— ๋‹ด๊ธฐ
                val searchText = homeBinding.fraHomSearchText.text.toString().trim()

                // ๊ณต๋ฐฑ ์ž…๋ ฅ ์‹œ ๋ฆฌํ„ด
                if(searchText.isEmpty()) {
                    Snackbar.make(homeBinding.root, "โ—๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”", Snackbar.LENGTH_SHORT).show()
                    return
                }

                // ๋ฉ”์ธ์— ๊ฒ€์ƒ‰์–ด ๋‹ด์•„๋‘๊ธฐ
                mainActivity.searchWord = searchText

                // ๋Œ์•„์™”์„ ๊ฒฝ์šฐ ๊ฒ€์ƒ‰์–ด ๋น„์›Œ๋‘๊ธฐ
                homeBinding.fraHomSearchText.setText("")

                // ์žํŒ ๋‚ด๋ฆฌ๊ธฐ
                mainActivity.hideKeyboard(mainActivity)

                thread {
                    val historyEntity = HistoryEntity(null, searchText)
                    mainActivity.historyDatabase.historyDao().insertHistory(historyEntity)

                    // ํ”„๋ž˜๊ทธ๋จผํŠธ ์ „ํ™˜
                    mainActivity.fragmentController("search", true)
                }
            }

            // ๊ฒ€์ƒ‰ ๊ธฐ๋ก ๋น„์šฐ๊ธฐ
            homeBinding.fraHomHistoryDeleteAll -> {
                thread {
                    mainActivity.historyDatabase.historyDao().deleteAllHistory()

                    mainActivity.runOnUiThread {
                        mainActivity.historyList.clear()
                        historyAdapter.notifyDataSetChanged()

                        homeBinding.fraHomEmptyHistory.visibility = View.VISIBLE
                        homeBinding.fraHomHistory.visibility = View.GONE
                    }

                    Snackbar.make(homeBinding.root, "โ—๊ฒ€์ƒ‰ ๊ธฐ๋ก์ด ๋น„์›Œ์กŒ์Šต๋‹ˆ๋‹ค", Snackbar.LENGTH_SHORT).show()
                }
            }
        }
    }
}

 

SearchFragment.kt
package com.ta2gi.searchbook.fragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.snackbar.Snackbar
import com.ta2gi.searchbook.MainActivity
import com.ta2gi.searchbook.R
import com.ta2gi.searchbook.databinding.FragmentSearchBinding
import com.ta2gi.searchbook.room.HistoryEntity
import kotlin.concurrent.thread

class SearchFragment(val mainActivity : MainActivity) : Fragment(), View.OnClickListener {

    lateinit var searchBinding : FragmentSearchBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        searchBinding = FragmentSearchBinding.inflate(inflater)

        // ๊ฒ€์ƒ‰์–ด ์‚ฝ์ž…
        searchBinding.fraSeaSearchText.setText(mainActivity.searchWord)

        // ์–ด๋Žํ„ฐ ์žฅ์ฐฉ
        searchBinding.fraSeaSearchList.adapter = mainActivity.searchAdapter

        // ์‚ฝ์ž…๋œ ๊ฒ€์ƒ‰์–ด๋กœ ๊ฒ€์ƒ‰
        mainActivity.searchResults(mainActivity.searchWord, this)

        // ํด๋ฆญ ๋ฆฌ์Šค๋„ˆ
        searchBinding.fraSeaBack.setOnClickListener(this)
        searchBinding.fraSeaSearchButton.setOnClickListener(this)

        return searchBinding.root
    }

    override fun onClick(view : View?) {
        when(view) {
            // ๋Œ์•„๊ฐ€๊ธฐ
            searchBinding.fraSeaBack -> mainActivity.supportFragmentManager.popBackStack()

            // ๊ฒ€์ƒ‰
            searchBinding.fraSeaSearchButton -> {
                mainActivity.hideKeyboard(mainActivity)

                val searchText = searchBinding.fraSeaSearchText.text.toString().trim()

                // ๊ณต๋ฐฑ ์ž…๋ ฅ ์‹œ ๋ฆฌํ„ด
                if(searchText.isEmpty()) {
                    Snackbar.make(searchBinding.root, "โ—๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”", Snackbar.LENGTH_SHORT).show()
                    return
                }

                // ๊ฒ€์ƒ‰ ๊ธฐ๋ก์— ์ถ”๊ฐ€
                thread {
                    val historyEntity = HistoryEntity(null, searchText)
                    mainActivity.historyDatabase.historyDao().insertHistory(historyEntity)
                }

                // ์žํŒ ๋‚ด๋ฆฌ๊ณ  ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋„์šฐ๊ธฐ
                mainActivity.searchResults(searchText, this)
            }
        }
    }
}

 

HistoryAdapter.kt
package com.ta2gi.searchbook.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import com.ta2gi.searchbook.MainActivity
import com.ta2gi.searchbook.R
import com.ta2gi.searchbook.fragment.HomeFragment
import com.ta2gi.searchbook.room.HistoryEntity
import kotlin.concurrent.thread

class HistoryAdapter(val mainActivity: MainActivity, val historyList : MutableList<HistoryEntity>, val fragment : HomeFragment) : RecyclerView.Adapter<HistoryAdapter.ViewHolderClass>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryAdapter.ViewHolderClass {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.history_list_item, parent, false)
        return ViewHolderClass(view)
    }

    override fun onBindViewHolder(holder: ViewHolderClass, position: Int) {
        holder.searchWord.text = historyList[position].searchWord

        // ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์‚ญ์ œ
        holder.searchWordDelete.setOnClickListener {
            thread {
                val historyEntity = HistoryEntity(historyList[position].bookUid, holder.searchWord.text.toString())
                mainActivity.historyDatabase.historyDao().deleteHistory(historyEntity)

                mainActivity.historyList.clear()
                mainActivity.historyList.addAll(mainActivity.historyDatabase.historyDao().getHistory())

                mainActivity.runOnUiThread {
                    notifyDataSetChanged()

                    // ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด empty ๋ณด์—ฌ์ฃผ๊ธฐ
                    if(mainActivity.historyList.isEmpty()) {
                        mainActivity.runOnUiThread {
                            fragment.homeBinding.fraHomEmptyHistory.visibility = View.VISIBLE
                            fragment.homeBinding.fraHomHistory.visibility = View.GONE
                        }
                    }
                }

                Snackbar.make(mainActivity.mainBinding.root, "โ—๊ฒ€์ƒ‰ ๊ธฐ๋ก์„ ์ง€์› ์Šต๋‹ˆ๋‹ค", Snackbar.LENGTH_SHORT).show()
            }
        }
    }

    override fun getItemCount(): Int {
        return historyList.size
    }

    inner class ViewHolderClass(view : View) : RecyclerView.ViewHolder(view) {
        val searchWord = view.findViewById<TextView>(R.id.history_list_item_search_word)
        val searchWordDelete = view.findViewById<ImageView>(R.id.history_list_item_delete)
    }
}

 

SearchAdapter.kt
package com.ta2gi.searchbook.adapter

import android.content.Intent
import android.net.Uri
import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.android.material.snackbar.Snackbar
import com.ta2gi.searchbook.MainActivity
import com.ta2gi.searchbook.R
import com.ta2gi.searchbook.retrofit2.BookDto
import kotlin.concurrent.thread

class SearchAdapter(val mainActivity: MainActivity, val searchList : MutableList<BookDto>) : RecyclerView.Adapter<SearchAdapter.ViewHolderClass>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchAdapter.ViewHolderClass {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.search_list_item, parent, false)
        return ViewHolderClass(view)
    }

    override fun onBindViewHolder(holder: SearchAdapter.ViewHolderClass, position: Int) {
        // ์ด๋ฏธ์ง€, ์ œ๋ชฉ, ์ €์ž, ์ค„๊ฑฐ๋ฆฌ, ์ •๊ฐ€, ํŒ๋งค์—ฌ๋ถ€, ๋งํฌ
        Glide.with(mainActivity).load(searchList[position].thumbnail).into(holder.thumbnail)
        holder.title.text = searchList[position].title
        holder.authors.text = searchList[position].authors.toString()
        holder.contents.text = searchList[position].contents
        holder.price.text = "${searchList[position].price}์›"
        holder.status.text = searchList[position].status
        holder.url.setOnClickListener {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(searchList[position].url))
            mainActivity.startActivity(intent)
        }
    }

    override fun getItemCount(): Int {
        return searchList.size
    }

    inner class ViewHolderClass(view : View) : RecyclerView.ViewHolder(view) {
        val thumbnail = view.findViewById<ImageView>(R.id.search_list_item_thumbnail)
        val title = view.findViewById<TextView>(R.id.search_list_item_title)
        val authors = view.findViewById<TextView>(R.id.search_list_item_authors)
        val contents = view.findViewById<TextView>(R.id.search_list_item_contents)
        val price = view.findViewById<TextView>(R.id.search_list_item_price)
        val status = view.findViewById<TextView>(R.id.search_list_item_status)
        val url = view.findViewById<TextView>(R.id.search_list_item_url)
    }
}

 

์ด์ œ Room์— ๊ด€๋ จ๋œ ํŒŒ์ผ๋“ค์ž…๋‹ˆ๋‹ค.

 

HistoryEntity.kt
package com.ta2gi.searchbook.room

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "HistoryTable")
data class HistoryEntity(
    @PrimaryKey(autoGenerate = true)
    val bookUid : Int?,
    @ColumnInfo
    val searchWord : String
)

 

HistoryDao.kt
package com.ta2gi.searchbook.room

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query

@Dao
interface HistoryDao {
    // ์ฐœ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
    @Query("SELECT * FROM HistoryTable")
    fun getHistory() : MutableList<HistoryEntity>

    // ์ฐœ ๋ชฉ๋ก์— ์‚ฝ์ž…
    @Insert
    fun insertHistory(book : HistoryEntity)

    // ์ฐœ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ
    @Delete
    fun deleteHistory(book : HistoryEntity)

    @Query("DELETE from HistoryTable")
    fun deleteAllHistory()

}

 

HistoryDatabase.kt
package com.ta2gi.searchbook.room

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [HistoryEntity::class], version = 1)
abstract class HistoryDatabase : RoomDatabase() {
    abstract fun historyDao() : HistoryDao

    companion object {
        private var instance : HistoryDatabase? = null

        @Synchronized
        fun getInstance(context: Context): HistoryDatabase? {
            if (instance == null) {
                synchronized(HistoryDatabase::class) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        HistoryDatabase::class.java,
                        "history.db"
                    ).build()
                }
            }
            return instance
        }
    }
}

 

๋‹ค์Œ์œผ๋กœ Retrofit2๋ฅผ ์“ฐ๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•๊ณผ ํŒŒ์ผ๋“ค์ž…๋‹ˆ๋‹ค.

 

https://developers.kakao.com/

 

Kakao Developers

์นด์นด์˜ค API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•ด๋ณด์„ธ์š”. ์นด์นด์˜ค ๋กœ๊ทธ์ธ, ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ, ์นœ๊ตฌ API, ์ธ๊ณต์ง€๋Šฅ API ๋“ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

developers.kakao.com

 

๋งํฌ๋กœ ์ด๋™ํ•ด ์นด์นด์˜ค์— ๋กœ๊ทธ์ธ์„ ํ•ด์ฃผ๊ณ  ์‹œ์ž‘ํ•˜๊ธฐ๋ฅผ ๋ˆŒ๋Ÿฌ์ค๋‹ˆ๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ถ”๊ฐ€ํ•˜๊ธฐ๋ฅผ ํด๋ฆญ ํ›„ ์•ฑ ์ด๋ฆ„๊ณผ ์‚ฌ์—…์ž๋ช…(์•„๋ฌด๋ ‡๊ฒŒ๋‚˜ ์ž…๋ ฅ ๊ฐ€๋Šฅ)์„ ์ž…๋ ฅํ•˜๊ณ  ์ €์žฅ์„ ๋ˆ„๋ฅด๋ฉด ์ถ”๊ฐ€๊ฐ€ ๋˜๊ณ 

ํ•ด๋‹น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ˆŒ๋Ÿฌ๋ณด๋ฉด ๊ฐ์ข… ํ‚ค๊ฐ€ ๋ฐœ๊ธ‰์ด ๋ฉ๋‹ˆ๋‹ค.

 

์šฐ๋ฆฌ์—๊ฒŒ ํ•„์š”ํ•œ API๋Š” ์ฑ… ๊ฒ€์ƒ‰ API์ž…๋‹ˆ๋‹ค.

์•„๋ž˜ ๋งํฌ๋กœ ์ด๋™ ํ›„ ์šฐ์ธก์— ์ฑ… ๊ฒ€์ƒ‰์„ ํด๋ฆญํ•ด ๋ณด๋ฉด ์šฐ๋ฆฌ์—๊ฒŒ ํ•„์š”ํ•œ ์ •๋ณด๋“ค์ด ์ž˜ ์ ํ˜€์žˆ์Šต๋‹ˆ๋‹ค.

 

https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide

 

Kakao Developers

์นด์นด์˜ค API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•ด๋ณด์„ธ์š”. ์นด์นด์˜ค ๋กœ๊ทธ์ธ, ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ, ์นœ๊ตฌ API, ์ธ๊ณต์ง€๋Šฅ API ๋“ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

developers.kakao.com

 

Response๋ฅผ ๋ณด๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ค ํ˜•์‹์œผ๋กœ ๋“ค์–ด์˜ค๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” ์ด๋ฏธ์ง€, ์ œ๋ชฉ, ์ €์ž, ๊ฐ€๊ฒฉ, ํŒ๋งค ์ƒํƒœ, ๋„์„œ ์†Œ๊ฐœ, ์ƒ์„ธ์ฃผ์†Œ๋งŒ ํ•„์š”ํ•˜๋ฏ€๋กœ ์ด๊ฒƒ๋“ค๋งŒ ๊ฐ€์ ธ์™€ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์ฑ…์˜ ์ •๋ณด๋ฅผ ๋‹ด์„ Data Class์ž…๋‹ˆ๋‹ค.

 

BookDto.kt
package com.ta2gi.searchbook.retrofit2

import com.google.gson.annotations.SerializedName

data class BookDto(
    @SerializedName("thumbnail") val thumbnail : String, // ์ด๋ฏธ์ง€
    @SerializedName("title") val title : String, // ์ œ๋ชฉ
    @SerializedName("authors") val authors : ArrayList<String>, // ์ €์ž
    @SerializedName("price") val price : Int, // ์ •๊ฐ€
    @SerializedName("contents") val contents : String, // ์†Œ๊ฐœ
    @SerializedName("status") val status : String, // ํŒ๋งค ์ƒํƒœ
    @SerializedName("url") val url : String // ์ƒ์„ธ์ฃผ์†Œ
)

 

@SerializedName์€ Response์—์„œ ๋„˜์–ด์˜จ ๋ฐ์ดํ„ฐ ์ค‘ ๋˜‘๊ฐ™์€ ์ด๋ฆ„์„ ์ฐพ์•„ ์ •๋ณด๋ฅผ ๋นผ์ค๋‹ˆ๋‹ค.

๋‹จ, ๋ณ€์ˆ˜๋ช…์ด Response์—์„œ ๋„˜์–ด์˜จ ์ด๋ฆ„๊ณผ ๊ฐ™๋‹ค๋ฉด @SerializedName๋ฅผ ์จ์ฃผ์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

 

๋‹ค์Œ์œผ๋กœ BookDto์— ๋„˜์–ด์˜จ ์ฑ…๋“ค์„ ๋„˜๊ฒจ๋ฐ›์„ Data Class๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

KakaoData.kt
package com.ta2gi.searchbook.retrofit2

import com.google.gson.annotations.SerializedName

data class KakaoData(
    @SerializedName("documents")
    var documents : List<BookDto>
)

 

๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด Response์—๋Š” meta์™€ documents๋กœ ๋ฐ์ดํ„ฐ๋“ค์ด ๋“ค์–ด์˜ค๋Š”๋ฐ ์ €๋Š” documents๋งŒ ์žˆ์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

 

KakaoInfo.kt
package com.ta2gi.searchbook.retrofit2

class KakaoInfo {
    companion object {
        const val BASE_URL = "https://dapi.kakao.com"
        const val API_KEY = "KakaoAK 390b0ab961f1097083f1ef99093867a7"
    }
}

 

๊ณตํ†ต์ ์œผ๋กœ ์“ฐ์ด๋Š” ์นœ๊ตฌ๋“ค์€ companion object์— ์„ ์–ธํ•ด ์คฌ์Šต๋‹ˆ๋‹ค.

์š”์ฒญ ์‹œ Host ์ฃผ์†Œ๋Š” BASE_URL์— ๋‹ด์•˜๊ณ 

API_KEY๋Š” ์‚ฌ์ดํŠธ์—์„œ ์ถ”๊ฐ€ํ–ˆ๋˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํด๋ฆญํ•ด ๋“ค์–ด๊ฐ€์„œ Rest API ํ‚ค๋ฅผ ๋ณต๋ถ™ ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋“œ์‹œ ๋งจ ์•ž์— "KakaoAK "๋ฅผ ๋ถ™์—ฌ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค!

 

KakaoService.kt
package com.ta2gi.searchbook.retrofit2

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

interface KakaoService {
    @GET("/v3/search/book")
    fun getSearchBook(
        @Header("Authorization")
        key : String,
        @Query("query")
        query : String
    ): Call<KakaoData>
}

 

@GET("/v3/search/book")์€ ๊ธฐ๋ณธ ์ •๋ณด๋ฅผ ๋ณด๋ฉด GET๋ฐฉ์‹์œผ๋กœ ๊ฐ€์ ธ์™€์•ผ ํ•œ๋‹ค๊ณ  ๋ช…์‹œ๋˜์–ด ์žˆ์œผ๋ฉฐ

๊ด„๊ณ  ์•ˆ์˜ ์ฃผ์†Œ๋Š” BASE_URL์˜ ๋’ค๋กœ ์ด์–ด์ง€๋Š” ๋ถ€๋ถ„์ด๊ธฐ ๋•Œ๋ฌธ์— "/"๋ฅผ ์ž˜ ๋งž๊ฒŒ ์ ์–ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

@Header("Authorization")์—๋Š” ๋ณธ์ธ์˜ Rest API ํ‚ค๋ฅผ ์ ์–ด๋†“์€ API_KEY๋ฅผ ๋„ฃ์–ด์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

@Query("query")๋Š” ๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด ํ˜ผ์ž์„œ๋งŒ ํ•„์ˆ˜๋กœ ๋„ฃ์–ด์ค˜์•ผ ํ•˜๋Š” ์นœ๊ตฌ์ด๋‹ˆ ์ž˜ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.

"query"์—๋Š” ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋“ค์–ด๊ฐˆ ๊ฒ๋‹ˆ๋‹ค.

 

 

์ด์ œ ๋งˆ์ง€๋ง‰์œผ๋กœ Kakao API URL๊ณผ ํ†ต์‹ ํ•˜๋Š” Retrofit2๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค๋Š” ํŒŒ์ผ์ž…๋‹ˆ๋‹ค.

object๋กœ ๋งŒ๋“ค๋ฉด singletonํ˜•์‹์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์ธ์Šคํ„ด์Šค๊ฐ€ ์ค‘๋ณต์œผ๋กœ ์ƒ์„ฑ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

RetrofitService.kt
package com.ta2gi.searchbook.retrofit2

import com.ta2gi.searchbook.retrofit2.KakaoInfo.Companion.BASE_URL
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitService {
    val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val kakaoService = retrofit.create(KakaoService::class.java)
}

 

์œ„ ํŒŒ์ผ์„ ๋Œ€์ถฉ ์„ค๋ช…ํ•˜์ž๋ฉด ๊ธฐ๋ณธ url์€ BASE_URL๋กœ ์“ฐ๊ณ  ๋‚ด์šฉ์„ GSON์œผ๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค๋Š” ์˜๋ฏธ๋กœ ํ•ด์„ํ•ฉ์‹œ๋‹ค!

 

์—ฌ๊ธฐ๊นŒ์ง€ ํŒŒ์ผ๋“ค์„ ์ž‘์„ฑ ํ›„ ์‹คํ–‰ ํ•ด๋ณด๋ฉด ์›ํ•˜๋Š” ์ฑ… ๊ฒ€์ƒ‰ ์‹œ ์ž˜ ๋œจ๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

The End..๐Ÿ˜ต