2023.7.28
1. ๋ค์ด๋ฒ ์ฑ api ํต์ ์ ํ์ํ ๊ฒ
- ๋ค์ด๋ฒ ์คํ์์ค
https://developers.naver.com/main/
- ์ฑ api ์ ๋ณด
- 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
}
}