[Android/Kotlin] kakao api ImageSearch(보완본)

정민수·2024년 2월 2일

다시 처음부터 시작하는 카카오 api 이미지 받아오기
보완 영상에서는 배웠던거랑 다르게 나왔다.?

KaKao api

Constants

우선 보완영상에서는 URL, HEADER, Preference key/Name이 Constants로 별도로 관리하였다.

object Constants {

    // Kakao Image Search API의 기본 URL입니다.
    const val BASE_URL = "https://dapi.kakao.com"

    // Kakao API를 사용하기 위한 인증 헤더입니다.
    const val AUTH_HEADER = "KakaoAK {API KEY}"

    // 앱의 Shared Preferences 파일 이름입니다.
    const val PREFS_NAME = "com.jblee.imagesearch.prefs"

    // 마지막 검색어를 저장하기 위한 키 값입니다.
    const val PREF_KEY = "IMAGE_SEARCH_PREF"
}

Retrofit작성하기

object retrofit_client {

    // API 서비스 객체를 반환한다.
    val apiService: Retrofit_interface
        get() = instance.create(Retrofit_interface::class.java)

    // Retrofit 인스턴스를 초기화하고 반환한다.
    private val instance: Retrofit
        private get() {
            // Gson 객체 생성. setLenient()는 JSON 파싱이 좀 더 유연하게 처리되도록 한다.
            val gson = GsonBuilder().setLenient().create()

            // Retrofit 빌더를 사용하여 Retrofit 인스턴스 생성
            return Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)  // 기본 URL 설정
                .addConverterFactory(GsonConverterFactory.create(gson))  // JSON 파싱을 위한 컨버터 추가
                .build()
        }
}

interface

interface Retrofit_interface {

    @GET("v2/search/image")
    fun image_search(
        @Header("Authorization") apiKey: String?,
        @Query("query") query: String?,
        @Query("sort") sort: String?,
        @Query("page") page: Int,
        @Query("size") size: Int
    ): Call<ImageModel?>?
}

ImageModel

data class ImageModel(
    @SerializedName("documents")
    val documents: ArrayList<Documents>,

    @SerializedName("meta")
    val meta: Meta
) {
    /**
     * 이미지 검색 응답에서 단일 문서 혹은 결과를 나타내는 클래스.
     */
    data class Documents(
        @SerializedName("collection")
        val collection: String,

        @SerializedName("thumbnail_url")
        val thumbnailUrl: String,

        @SerializedName("image_url")
        val imageUrl: String,

        @SerializedName("width")
        val width: Int,

        @SerializedName("height")
        val height: Int,

        @SerializedName("display_sitename")
        val displaySitename: String,

        @SerializedName("doc_url")
        val docUrl: String,

        @SerializedName("datetime")
        val datetime: String
    )

    /**
     * 이미지 검색 응답에 대한 메타 정보를 나타내는 클래스.
     */
    data class Meta(
        @SerializedName("is_end")
        val isEnd: Boolean,

        @SerializedName("pageable_count")
        val pageableCount: Int,

        @SerializedName("total_count")
        val totalCount: Int
    )
}

Utils

api에서 받아온 시간을 원하는 표시형식으로 바꾸기 위한 코드

    fun getDateFromTimestampWithFormat(
        timestamp: String?,
        fromFormatformat: String?,
        toFormatformat: String?
    ): String {
        var date: Date? = null
        var res = ""
        try {
            val format = SimpleDateFormat(fromFormatformat)
            date = format.parse(timestamp)
        } catch (e: ParseException) {
            e.printStackTrace()
        }
        val df = SimpleDateFormat(toFormatformat)
        res = df.format(date)
        return res
    }

preference : 마지막 검색어를 저장하기 위한 코드.

fun saveLastSearch(context: Context, query: String) {
        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
        prefs.edit().putString(PREF_KEY, query).apply()
    }
fun getLastSearch(context: Context): String? {
        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
        return prefs.getString(PREF_KEY, null)
    }

SearchItemModel

data class SearchItemModel(
    var title: String,
    var dateTime: String,
    var url: String,
    var isLike: Boolean = false
)

MainActivity

공유저장소

var likedItems : ArrayList<SearchItemModel> = ArrayList()

나중에 클릭된 아이템들이 likedItems에 들어갈 수 있도록 만듬.

addLikedItem

fun addLikedItem(item: SearchItemModel) {
        if (!likedItems.contains(item)) {
            likedItems.add(item)
        }
    }

removeLikedItem

fun removeLikedItem(item: SearchItemModel) {
        likedItems.remove(item)
    }

Search

SearchFragment

setUpViews

private fun setupViews() {
        // RecyclerView 설정
        gridmanager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
        binding.rvSearch.layoutManager = gridmanager
        .
        adapter = SearchAdapter(mContext)
        binding.rvSearch.adapter = adapter
        binding.rvSearch.itemAnimator = null
        .
        // 최근 검색어를 가져와 EditText에 설정
        val lastSearch = Utils.getLastSearch(requireContext())
        binding.etSearch.setText(lastSearch)
    }

setUpListeners

private fun setupListeners() {
        val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        binding.btnSearch.setOnClickListener {
            val query = binding.etSearch.text.toString()
            if (query.isNotEmpty()) {
                Utils.saveLastSearch(requireContext(), query)
                adapter.clearItem()
                fetchImageResults(query)
            } else {
                Toast.makeText(mContext, "검색어를 입력해 주세요.", Toast.LENGTH_SHORT).show()
            }
            // 키보드 숨기기
            imm.hideSoftInputFromWindow(binding.etSearch.windowToken, 0)
        }
    }

fetchImageResults

private fun fetchImageResults(query: String) {
        apiService.image_search(Constants.AUTH_HEADER, query, "recency", 1, 80)
            ?.enqueue(object : Callback<ImageModel?> {
                override fun onResponse(call: Call<ImageModel?>, response: Response<ImageModel?>) {
                    response.body()?.meta?.let { meta ->
                        if (meta.totalCount > 0) {
                            response.body()!!.documents.forEach { document ->
                                val title = document.displaySitename
                                val datetime = document.datetime
                                val url = document.thumbnailUrl
                                resItems.add(SearchItemModel(title, datetime, url))
                            }
                        }
                    }
                    adapter.items = resItems
                    adapter.notifyDataSetChanged()
                }
                override fun onFailure(call: Call<ImageModel?>, t: Throwable) {
                    Log.e("#jblee", "onFailure: ${t.message}")
                }
            })
    }

SearchAdapter

onBindViewHolder

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val currentItem = items[position]
        Glide.with(mContext)
            .load(currentItem.url)
            .into(holder.iv_thum_image)
        holder.iv_like.visibility = if (currentItem.isLike) View.VISIBLE else View.INVISIBLE
        holder.tv_title.text = currentItem.title
        holder.tv_datetime.text = getDateFromTimestampWithFormat(
            currentItem.dateTime,
            "yyyy-MM-dd'T'HH:mm:ss.SSS+09:00",
            "yyyy-MM-dd HH:mm:ss"
        )
    }

Bookmark

BookmarkFragment

class BookmarkFragment : Fragment() {
.
    private lateinit var mContext: Context
.
    // 바인딩 객체를 null 허용으로 설정 (프래그먼트의 뷰가 파괴될 때 null 처리하기 위함)
    private var binding: FragmentBookMarkBinding? = null
    private lateinit var adapter: BookmarkAdapter
.
    // 사용자의 좋아요를 받은 항목을 저장하는 리스트
    private var likedItems: List<SearchItemModel> = listOf()
.
    override fun onAttach(context: Context) {
        super.onAttach(context)
        mContext = context
    }
.
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // MainActivity로부터 좋아요 받은 항목을 가져옴
        val mainActivity = activity as MainActivity
        likedItems = mainActivity.likedItems
.
        Log.d("BookmarkFragment", "#jblee likedItems size = ${likedItems.size}")
.
        // 어댑터 설정
        adapter = BookmarkAdapter(mContext).apply {
            items = likedItems.toMutableList()
        }
.
        // 바인딩 및 RecyclerView 설정
        binding = FragmentBookMarkBinding.inflate(inflater, container, false).apply {
            rvBookmark.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
            rvBookmark.adapter = adapter
        }
.
        return binding?.root
    }
.
    override fun onDestroyView() {
        super.onDestroyView()
        // 메모리 누수를 방지하기 위해 뷰가 파괴될 때 바인딩 객체를 null로 설정
        binding = null
    }
}
profile
응애...아무것도 모르는 개발자 흉내라도 내고 싶은 비전공자입니다.

0개의 댓글