다시 처음부터 시작하는 카카오 api 이미지 받아오기
보완 영상에서는 배웠던거랑 다르게 나왔다.?
우선 보완영상에서는 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"
}
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 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?>?
}
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
)
}
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) }
data class SearchItemModel(
var title: String,
var dateTime: String,
var url: String,
var isLike: Boolean = false
)
공유저장소
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) }
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}") } }) }
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" ) }
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 } }