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

Study/Android

[์•ˆ๋“œ๋กœ์ด๋“œ ์ฝ”ํ‹€๋ฆฐ] ํ”„๋กœ์ ํŠธ - ๋„ค์ด๋ฒ„ ์ฑ… api ํ†ต์‹ 

2023.7.28

 

1. ๋„ค์ด๋ฒ„ ์ฑ… api ํ†ต์‹ ์— ํ•„์š”ํ•œ ๊ฒƒ

- ๋„ค์ด๋ฒ„ ์˜คํ”ˆ์†Œ์Šค

https://developers.naver.com/main/

- ์ฑ… api ์ •๋ณด

https://developers.naver.com/docs/serviceapi/search/book/book.md#%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0

 

- Application > ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋“ฑ๋ก

 

- ๋‚ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

    - Clinet ID(ํด๋ผ์ด์–ธํŠธ id), Client Secret(ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ)์€ api ์ฝ”๋“œ ์ž‘์„ฑ์— ํ•„์š”ํ•จ

 

2. ์ค€๋น„

- MainActivity.kt / activity_main.xml

- item_book.xml (๋ฆฌ์‚ฌ์ดํ˜๋Ÿฌ๋ทฐ ์•„์ดํ…œ)

- BookSearchAdapter.kt (๋ฆฌ์‚ฌ์ดํ˜๋Ÿฌ๋ทฐ ์–ด๋Œ‘ํ„ฐ)

- NaverBookApi.kt (interface)

- NaverBookItem.kt (data class)

- NaverBookResponse.kt (data class)

 

 

3. ์ฝ”๋“œ ์ž‘์„ฑ ์ „

- AndroidManifest.xml

    - <uses-permission android:name="android.permission.INTERNET" /> ์ถ”๊ฐ€

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

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

    ...

</manifest>

 

- build.gradle(:app)

dependencies {

    ...

    // ์ด๋ฏธ์ง€ glide
    implementation 'com.github.bumptech.glide:glide:4.12.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'

    // ๋ฆฌ์‚ฌ์ดํ˜๋Ÿฌ๋ทฐ
    implementation("androidx.recyclerview:recyclerview:1.2.1")
    implementation("androidx.recyclerview:recyclerview-selection:1.1.0")

    // api ํ†ต์‹ 
    implementation 'com.google.code.gson:gson:2.8.8'
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

 

- settings.gradle

    - ์‚ฌ์‹ค ์ด๊ฑด ์ž˜ ๋ชจ๋ฅด๊ฒ ๋‹ค. ์˜์กด์„ฑ ์ถฉ๋Œ์ด ๋‚˜์„œ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

    - https://angelplayer.tistory.com/263

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()

        // ์˜์กด์„ฑ ์ถฉ๋Œ ์—๋Ÿฌ ๋ฐฉ์ง€
        maven { url "https://jitpack.io" }
        jcenter() // Warning: this repository is going to shut down soon
    }
}

 

 

4. 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/edtSearch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="60dp"
        android:ems="10"
        android:inputType="text"/>

    <Button
        android:id="@+id/btnSearch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="Button" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerViewBook"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible"
        app:layout_constraintTop_toBottomOf="@+id/btnSearch" />

</LinearLayout>

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#F6CAC7"
    android:layout_margin="10dp">

    <ImageView
        android:id="@+id/imageViewBook"
        android:layout_width="82dp"
        android:layout_height="116dp"
        android:layout_margin="5dp"
        android:src="@drawable/book"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center_vertical"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp">

        <TextView
            android:id="@+id/textViewTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="์ฑ…์ œ๋ชฉ"
            android:textSize="16sp"
            android:textStyle="bold"
            android:maxLines="2"
            android:ellipsize="end"/>

        <TextView
            android:id="@+id/textViewAuthor"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="์ €์ž |"
            android:textSize="14sp"/>

        <TextView
            android:id="@+id/textViewPublisher"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="์ถœํŒ์‚ฌ |"
            android:textSize="14sp"/>

        <TextView
            android:id="@+id/textViewPublishedDate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="์ถœ๊ฐ„์ผ |"
            android:textSize="14sp"/>

    </LinearLayout>
</LinearLayout>

 

 

5. kt ์ž‘์„ฑ

- NaverBookResponse.kt

data class NaverBookResponse(
    val items: List<NaverBookItem>
)

 

- NaverBookItem.kt

    - ๊ฐ€์ ธ์™€์•ผํ•  ์ •๋ณด ์ด๋ฆ„์€ api ์‚ฌ์ดํŠธ์— ๊ฐ€๋ฉด ์žˆ๋‹ค. 

    - ์ถœ๊ฐ„์ผ์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹ถ์„ ๋•Œ ๋„ค์ด๋ฒ„ ์ฑ… api์˜ ์ด๋ฆ„์€ pubdate์ธ๋ฐ, ์ด๊ฒƒ์„ ๋งˆ์Œ๋Œ€๋กœ ๋ฐ”๊พธ๋ฉด ๊ฐ’์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ•œ๋‹ค.

data class NaverBookItem(
    val title: String,              // ์ œ๋ชฉ
    val author: String,             // ์ €์ž
    val publisher: String,          // ์ธจํŒ์‚ฌ
    val pubdate: String,            // ์ถœ๊ฐ„์ผ
    val image: String,              // ์„ฌ๋„ค์ผ ์ด๋ฏธ์ง€ URL
    val description: String         // ์ฑ… ์†Œ๊ฐœ
    // ISBN
)

 

- NaverBookApi.kt

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

interface NaverBookApi {
    @GET("v1/search/book.json")
    fun searchBooks(
        @Header("X-Naver-Client-Id") clientId: String,          // ๋„ค์ด๋ฒ„ ์ฑ… api ํด๋ผ์ด์–ธํŠธ id
        @Header("X-Naver-Client-Secret") clientSecret: String,  // ๋„ค์ด๋ฒ„ ์ฑ… api ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ
        @Query("query") query: String,      // ๊ฒ€์ƒ‰์–ด. UTF-8๋กœ ์ธ์ฝ”๋”ฉ๋˜์–ด์•ผํ•จ
        @Query("display") display: Int,     // ํ•œ ๋ฒˆ์— ํ‘œ์‹œํ•  ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜(๊ธฐ๋ณธ๊ฐ’: 10, ์ตœ๋Œ“๊ฐ’: 100)
        @Query("start") start: Int,         // ๊ฒ€์ƒ‰ ์‹œ์ž‘ ์œ„์น˜(๊ธฐ๋ณธ๊ฐ’: 1, ์ตœ๋Œ“๊ฐ’: 1000)
        @Query("sort") sort: String         // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ •๋ ฌ ๋ฐฉ๋ฒ•(sim: ์ •ํ™•๋„์ˆœ์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ(๊ธฐ๋ณธ๊ฐ’),date: ์ถœ๊ฐ„์ผ์ˆœ์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ)
    ): Call<NaverBookResponse>
}

 

- BookSearchAdapter.kt

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

class BookSearchAdapter(private var bookList: List<NaverBookItem>) : RecyclerView.Adapter<BookSearchAdapter.BookViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_book, parent, false)
        return BookViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: BookViewHolder, position: Int) {
        val book = bookList[position]       // position๋ฒˆ์งธ ๋ฆฌ์ŠคํŠธ ์ •๋ณด๋ฅผ book ๋ณ€์ˆ˜์— ์ „๋‹ฌ
        holder.bind(book)       // ๋ฆฌ์‚ฌ์ดํ˜๋Ÿฌ๋ทฐ์— ์ถœ๋ ฅํ•˜๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜ ํ˜ธ์ถœ
    }

    override fun getItemCount(): Int = bookList.size

    // ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋กœ ๊ธฐ์กด bookList๋ฅผ ๊ฐฑ์‹ ํ•˜๊ณ  ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ํ•จ์ˆ˜
    fun updateData(newList: List<NaverBookItem>) {
        bookList = newList
        notifyDataSetChanged()   // ์–ด๋Œ‘ํ„ฐ์— ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์•Œ๋ฆผ
    }

    class BookViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        // ์œ„์ ฏ ์—ฐ๊ฒฐ
        private val titleTextView: TextView = itemView.findViewById(R.id.textViewTitle)
        private val authorTextView: TextView = itemView.findViewById(R.id.textViewAuthor)
        private val publisherTextView: TextView = itemView.findViewById(R.id.textViewPublisher)
        private val publishedDateTextView: TextView = itemView.findViewById(R.id.textViewPublishedDate)
        private val bookImageView: ImageView = itemView.findViewById(R.id.imageViewBook)

        // ๋ฆฌ์‚ฌ์ดํ˜๋Ÿฌ๋ทฐ์— ์ถœ๋ ฅ
        fun bind(book: NaverBookItem) {
            titleTextView.text = book.title                                      // ์ œ๋ชฉ
            authorTextView.text = "์ €์ž | " + book.author                         // ์ €์ž
            publisherTextView.text = "์ถœํŒ์‚ฌ | " + book.publisher                 // ์ธจํŒ์‚ฌ
            publishedDateTextView.text = "์ถœ๊ฐ„์ผ | " + formatDate(book.pubdate)   // ์ถœ๊ฐ„์ผ

            // ์„ฌ๋„ค์ผ ์ด๋ฏธ์ง€ URL
            Glide.with(itemView.context)
                .load(book.image)
                .into(bookImageView)
        }

        // ์ถœ๊ฐ„์ผ ๋ณ€๊ฒฝ ํ•จ์ˆ˜ (ex. 20230728 -> 0223๋…„ 07์›” 28์ผ)
        fun formatDate(dateStr: String): String {
            val year = dateStr.substring(0, 4)
            val month = dateStr.substring(4, 6)
            val day = dateStr.substring(6, 8)

            return "${year}๋…„ ${month}์›” ${day}์ผ"
        }
    }
}

 

- MainActivity.kt

    - ์›๋ž˜๋Š” Fragment์— ์ž‘์„ฑํ–ˆ๋Š”๋ฐ ์ฝ”๋“œ ์„ค๋ช…์„ Activity๋กœ ํ•ด์„œ ์ฝ”๋“œ ์‹ค์ˆ˜๊ฐ€ ์žˆ์„ ์ˆ˜๋„ ์žˆ๋‹ค. (id ๊ฐ™์€..)

    - ํด๋ผ์ด์–ธํŠธ id์™€ ์‹œํฌ๋ฆฟ์€ ์ฒซ๋‹จ๊ณ„ ๋„ค์ด๋ฒ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋“ฑ๋ก์„ ํ†ตํ•ด ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ImageView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class MainActivity : AppCompatActivity() {

    // ๋„ค์ด๋ฒ„ api ํ†ต์‹ ์„ ์œ„ํ•œ ๋ณ€์ˆ˜
    private val clientId = "ํด๋ผ์ด์–ธํŠธ id"   		// ํด๋ผ์ด์–ธํŠธ id
    private val clientSecret = "ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ"        // ํด๋ผ์ด์–ธํŠธ ์‹œํฌ๋ฆฟ
    private val baseUrl = "https://openapi.naver.com/"  // ๋„ค์ด๋ฒ„ ๊ธฐ๋ณธ api url

    // ์œ„์ ฏ ๋ณ€์ˆ˜
    lateinit var edtSearch: EditText
    lateinit var btnSearch: ImageButton
    lateinit var recyclerViewBook: RecyclerView

    // ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ ์–ด๋Œ‘ํ„ฐ ๋ณ€์ˆ˜
    lateinit var bookAdapter: BookSearchAdapter

    // ๋ฐ›์•„์˜จ ์ •๋ณด ์ €์žฅํ•  ๋ฆฌ์ŠคํŠธ ๋ณ€์ˆ˜
    val bookList: MutableList<NaverBookItem> = mutableListOf()

    override onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // ์œ„์ ฏ ์—ฐ๊ฒฐ
        edtSearch = findViewById<EditText>(R.id.edtSearch)
        btnSearch = findViewById<ImageButton>(R.id.btnSearch)
        recyclerViewBook = findViewById<RecyclerView>(R.id.recyclerViewBook)

        // RecyclerView ์–ด๋Œ‘ํ„ฐ ์ƒ์„ฑ ๋ฐ ์„ค์ •
        bookAdapter = BookSearchAdapter(emptyList())
        recyclerViewBook.layoutManager = LinearLayoutManager(context)      //LinearLayoutManager: ์•„์ดํ…œ๋“ค์„ ์ˆ˜์ง ๋ฐฉํ–ฅ์œผ๋กœ ๋‚˜์—ด
        recyclerViewBook.adapter = bookAdapter
        recyclerViewBook.setHasFixedSize(true)      // setHasFixedSize(true): ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ ํฌ๊ธฐ ๊ณ ์ •

        // ๋ฆฌ์Šค๋„ˆ ์—ฐ๊ฒฐ
        btnSearch.setOnClickListener {  // ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ
            val query = edtSearch.text.toString().trim()    // editText์— ๊ฒ€์ƒ‰ํ•œ ๊ฒ€์ƒ‰์–ด
            if (query.isNotEmpty()) {  // ๊ฒ€์ƒ‰ํ•˜์ง€ ์•Š๊ณ  ๋ฒ„ํŠผ ํด๋ฆญ(๊ฒ€์ƒ‰์–ด๊ฐ€ ๋น„์–ด์žˆ์Œ)
                searchBooks(query)     // api๋ฅผ ํ†ตํ•ด ์ฑ… ์ •๋ณด ์–ป๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ
            }
        }
    }

    // ๋„ค์ด๋ฒ„ ์ฑ… api ์‚ฌ์šฉํ•ด ์ •๋ณด๋ฅผ ์–ป๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜
    fun searchBooks(query: String) {

        // Retrofi ๊ฐ์ฒด ์ƒ์„ฑ (๊ฐ์ฒด ์ด์šฉํ•˜์—ฌ api ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์„ฑ, ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด api ํ˜ธ์ถœ)
        val retrofit = Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        val naverBookApi = retrofit.create(NaverBookApi::class.java)    // NaverBookApi ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด api ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
        val call = naverBookApi.searchBooks(clientId, clientSecret, query, 100, 1, "sim")     // query(๊ฒ€์ƒ‰์–ด)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ api ํ˜ธ์ถœ

        call.enqueue(object : Callback<NaverBookResponse> {     // api ์‘๋‹ต ์ฒ˜๋ฆฌ
            override fun onResponse(call: Call<NaverBookResponse>, response: Response<NaverBookResponse>) {     // api ์‘๋‹ต ์„ฑ๊ณต
                if (response.isSuccessful) {    // ์‘๋‹ต ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ
                    val bookResponse = response.body()  // ์‘๋‹ต ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
                    val items = bookResponse?.items     // ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ ์•„์ดํ…œ
                    if(items != null) {     // ์•„์ดํ…œ์ด ๋น„์–ด์žˆ์ง€ ์•Š์„ ๊ฒฝ์šฐ
                        bookAdapter.updateData(items)   // RecyclerView์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธ
                    }
                } else {    // ์‹คํŒจํ•  ๊ฒฝ์šฐ
                    println("API call failed: ${response.code()}")  // ์‘๋‹ต ์‹คํŒจ ์ฝ”๋“œ
                }
            }

            override fun onFailure(call: Call<NaverBookResponse>, t: Throwable) {       // api ์‘๋‹ต ์‹คํŒจ
                println("API call failed: ${t.message}")
            }
        })
    }
}

 

 

๊ฒฐ๊ณผ

 

 

+ editText์— ์ž…๋ ฅํ•  ๋•Œ ํ‚ค๋ณด๋“œ ์—”ํ„ฐ ๋˜๋Š” done ๋ฒ„ํŠผ ๋ˆ„๋ฅด๋ฉด ํ‚ค๋ณด๋“œ ๋‚ด๋ฆฌ๊ธฐ

- xml์˜ EditText์— ์ถ”๊ฐ€

android:imeOptions="actionDone"
android:singleLine="true"

 

- MainActivity.kt

    - onCreate() ์•ˆ์— ์ž‘์„ฑ

    - ์œ„์ ฏ id: edtSearch (์œ„์ ฏ ๋ณ€์ˆ˜ ์„ ์–ธ๊ณผ ์—ฐ๊ฒฐ์„ ์ƒ๋žตํ–ˆ๋‹ค)

    - fragment์šฉ์œผ๋กœ ์ž‘์„ฑํ•ด์„œ Activity์—์„œ๋Š” ์—๋Ÿฌ๊ฐ€ ๋‚  ์ˆ˜ ์žˆ๋‹ค.

edtSearch.setOnKeyListener { _, keyCode, event ->
    if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {    // ํ‚ค๋ณด๋“œ done ๋ฒ„ํŠผ ๋˜๋Š” ์—”ํ„ฐํ‚ค ๋ˆ„๋ฅผ ๊ฒฝ์šฐ
        // ํ‚คํŒจ๋“œ ๋‚ด๋ฆฌ๊ธฐ
        val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(edtSearch.windowToken, 0)
        true
    } else {
        false
    }
}