코틀린으로 주소 API 를 호출해서 결과를 받아오려하는데 403오류가 계속 발생합니다

문의 시, 디벨로퍼스 앱ID를 알려주세요.
1206938

  • 개인정보 항목 심사는 운영하시는 서비스 일반회원가입에서 입력 받는 항목을 기준으로 제공됩니다.
  • 코틀린에서 주소 API를 이용하고 있는데 계속 403 오류가 납니다.

앱 권한 신청이 반려되었습니다. FAQ


안녕하세요.

앱 1206938는 카카오맵 권한이 없습니다.
해당 권한을 보유하고 있는 앱 953810을 사용하시거나 앱 권한 신청 후 카카오맵 기능 활성화 하여 사용 부탁드립니다.

제가 개발하는 api는 카카오 주소검색 API 입니다.
앱 1206938는 카카오 주소검색 API를 위해 사용하는 앱입니다.

해당 API는 카카오맵 기능 활성화 되어야 사용할 수 있습니다.

그 부분을 살펴봤는데요.
|카카오맵| 카카오맵 API 사용 권한을 신청합니다. [신청] [신청 불필요] 라고 표시되는데 어떻게 신청을 합니까?

신청 불필요의 경우 카카오맵 기능 활성화만 하시면 됩니다.

그렇게 해서 200 성공 코드를 받았는데 본문이 오지 않습니다.
검색 시작: 쿼리=%EC%88%98%EC%9B%90%EC%8B%9C
17:39:34.121 D 응답 수신: 200
17:39:34.122 D addressResponse.documents: KakaoAddressResponse(documents=, meta=Meta(total_count=0, pageable_count=0, is_end=true))

문의 시, 사용하시는 개발환경과 디벨로퍼스 앱ID를 알려주세요.
앱아이디: 1206980

코틀린에서 주소검색 api를 호출해서 상태코드 200(성공인데) 인데 쿼리=482-4를 넘겼는데 response.body()에는 아래와 같이 아무것도 나오지 않습니다.

addressResponse.documents: KakaoAddressResponse(documents=[ ], meta=Meta(total_count=0, pageable_count=0, is_end=true))

자세한 확인을 위해 구현 코드 부탁드립니다.

package com.ygr.ygr_wholesale2.activity

import android.app.Activity
import android.app.ProgressDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.ygr.ygr_wholesale2.adapter.AddressAdapter
import com.ygr.ygr_wholesale2.api.RetrofitClient
import com.ygr.ygr_wholesale2.databinding.ActivityAddressSearchBinding
import com.ygr.ygr_wholesale2.viewmodels.KakaoAddressResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.URLEncoder

class AddressSearchActivity : AppCompatActivity() {

    private lateinit var binding: ActivityAddressSearchBinding
    private lateinit var addressAdapter: AddressAdapter
    private var progressDialog: ProgressDialog? = null
    private var isSearching = false // 검색 진행 중 상태 변수 추가

    // REST API 키
    private val apiKey = "3e98ecdf893fabcc536a786f974b1d83"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityAddressSearchBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setupToolbar()
        setupRecyclerView()
        setupSearchButton()
    }

    private fun setupToolbar() {
        setSupportActionBar(binding.toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        supportActionBar?.title = "주소 검색"

        binding.toolbar.setNavigationOnClickListener {
            finish()
        }
    }

    private fun setupRecyclerView() {
        addressAdapter = AddressAdapter { document ->
            // 주소 선택 시 처리
            val selectedAddress = document.road_address?.address_name ?: document.address.address_name
            val intent = Intent().apply {
                putExtra("selected_address", selectedAddress)
            }
            setResult(Activity.RESULT_OK, intent)
            finish()
        }

        binding.rvAddressList.apply {
            layoutManager = LinearLayoutManager(this@AddressSearchActivity)
            adapter = addressAdapter
        }
    }

    private fun setupSearchButton() {
        binding.btnSearch.setOnClickListener {
            // 이미 검색 중이라면 중복 검색 방지
            if (isSearching) {
                return@setOnClickListener
            }

            val query = URLEncoder.encode(binding.etAddressInput.text.toString().trim(), "UTF-8")

            if (query.isNotEmpty()) {
                searchAddress(query)
            } else {
                Toast.makeText(this, "검색어를 입력해주세요", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun searchAddress(query: String) {
        // 이미 검색 중이면 리턴
        if (isSearching) {
            return
        }



        isSearching = true

        // 기존 진행 중인 다이얼로그가 있다면 닫기
        progressDialog?.dismiss()

        // 프로그레스 표시
        progressDialog = ProgressDialog(this).apply {
            setMessage("주소를 검색 중입니다...")
            setCancelable(false)
            show()
        }

        try {
            // API 호출
            val authHeader = "KakaoAK $apiKey"
            Log.d("AddressSearch", "검색 시작: 쿼리=$query, API Key=$authHeader")

            val encodedQuery = URLEncoder.encode(query, "UTF-8")

            val call = RetrofitClient.kakaoApiService.searchAddress(
                authorization = authHeader,
                query = encodedQuery
            )

            call.enqueue(object : Callback<KakaoAddressResponse> {
                override fun onResponse(call: Call<KakaoAddressResponse>, response: Response<KakaoAddressResponse>) {
                    // 검색 완료 상태로 변경
                    isSearching = false

                    // 프로그레스 다이얼로그 닫기
                    progressDialog?.dismiss()
                    progressDialog = null

                    Log.d("AddressSearch", "응답 수신: ${response.code()}")

                    if (response.isSuccessful) {
                        val addressResponse = response.body()
                        if (addressResponse != null) {
                            Log.d("AddressSearch", "addressResponse: $addressResponse")
                            if (addressResponse.documents.isEmpty()) {
                                // 검색 결과가 없는 경우
                                Toast.makeText(this@AddressSearchActivity, "검색 결과가 없습니다. 다른 검색어를 시도해보세요.", Toast.LENGTH_SHORT).show()
                                addressAdapter.clearAddresses() // 어댑터의 기존 데이터를 지우는 메소드 (필요시 구현)
                            } else {
                                // 검색 결과가 있는 경우
                                addressAdapter.setAddresses(addressResponse.documents)
                            }
                        } else {
                            Toast.makeText(this@AddressSearchActivity, "응답 데이터를 처리할 수 없습니다.", Toast.LENGTH_SHORT).show()
                        }
                    } else {
                        val errorMsg = "주소 검색에 실패했습니다(${response.code()})"
                        Log.e("AddressSearch", "에러 코드: ${response.code()}, 메시지: ${response.message()}")
                        Toast.makeText(this@AddressSearchActivity, errorMsg, Toast.LENGTH_SHORT).show()
                    }
                }


                override fun onFailure(call: Call<KakaoAddressResponse>, t: Throwable) {
                    // 검색 완료 상태로 변경
                    isSearching = false

                    // 프로그레스 다이얼로그 닫기
                    progressDialog?.dismiss()
                    progressDialog = null

                    Log.e("AddressSearch", "통신 오류", t)
                    Toast.makeText(this@AddressSearchActivity, "오류가 발생했습니다: ${t.message}", Toast.LENGTH_SHORT).show()
                }
            })
        } catch (e: Exception) {
            // 예외 발생 시 처리
            isSearching = false
            progressDialog?.dismiss()
            progressDialog = null

            Log.e("AddressSearch", "API 호출 중 예외 발생", e)
            Toast.makeText(this, "API 호출 중 오류가 발생했습니다", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 액티비티 종료 시 다이얼로그 닫기
        progressDialog?.dismiss()
        progressDialog = null
    }

    override fun onSupportNavigateUp(): Boolean {
        onBackPressed()
        return true
    }
}

입니다.

제공해 주신 코드에서는 쿼리를 두번 인코딩 하고 있습니다.

방금 발견하고 인코딩을 한번만 하고 수정해도 동일합니다. 소스와 어댑터도 같이 보내드립니다. 자꾸 귀찮게 해드려 너무 죄송합니다.

package com.ygr.ygr_wholesale2.activity

import android.app.Activity
import android.app.ProgressDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.ygr.ygr_wholesale2.adapter.AddressAdapter
import com.ygr.ygr_wholesale2.api.RetrofitClient
import com.ygr.ygr_wholesale2.databinding.ActivityAddressSearchBinding
import com.ygr.ygr_wholesale2.viewmodels.KakaoAddressResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.URLEncoder

class AddressSearchActivity : AppCompatActivity() {

    private lateinit var binding: ActivityAddressSearchBinding
    private lateinit var addressAdapter: AddressAdapter
    private var progressDialog: ProgressDialog? = null
    private var isSearching = false // 검색 진행 중 상태 변수 추가

    // REST API 키
    private val apiKey = "3e98ecdf893fabcc536a786f974b1d83"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityAddressSearchBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setupToolbar()
        setupRecyclerView()
        setupSearchButton()
    }

    private fun setupToolbar() {
        setSupportActionBar(binding.toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        supportActionBar?.title = "주소 검색"

        binding.toolbar.setNavigationOnClickListener {
            finish()
        }
    }

    private fun setupRecyclerView() {
        addressAdapter = AddressAdapter { document ->
            // 주소 선택 시 처리
            val selectedAddress = document.road_address?.address_name ?: document.address.address_name
            val intent = Intent().apply {
                putExtra("selected_address", selectedAddress)
            }
            setResult(Activity.RESULT_OK, intent)
            finish()
        }

        binding.rvAddressList.apply {
            layoutManager = LinearLayoutManager(this@AddressSearchActivity)
            adapter = addressAdapter
        }
    }

    private fun setupSearchButton() {
        binding.btnSearch.setOnClickListener {
            // 이미 검색 중이라면 중복 검색 방지
            if (isSearching) {
                return@setOnClickListener
            }

            val query = binding.etAddressInput.text.toString().trim()

            if (query.isNotEmpty()) {
                searchAddress(query)
            } else {
                Toast.makeText(this, "검색어를 입력해주세요", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun searchAddress(query: String) {
        // 이미 검색 중이면 리턴
        if (isSearching) {
            return
        }



        isSearching = true

        // 기존 진행 중인 다이얼로그가 있다면 닫기
        progressDialog?.dismiss()

        // 프로그레스 표시
        progressDialog = ProgressDialog(this).apply {
            setMessage("주소를 검색 중입니다...")
            setCancelable(false)
            show()
        }

        try {
            // API 호출
            val authHeader = "KakaoAK $apiKey"
            Log.d("AddressSearch", "검색 시작: 쿼리=$query, API Key=$authHeader")

            val encodedQuery = URLEncoder.encode(query, "UTF-8")

            val call = RetrofitClient.kakaoApiService.searchAddress(
                authorization = authHeader,
                query = encodedQuery
            )

            call.enqueue(object : Callback<KakaoAddressResponse> {
                override fun onResponse(call: Call<KakaoAddressResponse>, response: Response<KakaoAddressResponse>) {
                    // 검색 완료 상태로 변경
                    isSearching = false

                    // 프로그레스 다이얼로그 닫기
                    progressDialog?.dismiss()
                    progressDialog = null

                    Log.d("AddressSearch", "응답 수신: ${response.code()}")

                    if (response.isSuccessful) {
                        val addressResponse = response.body()
                        if (addressResponse != null) {
                            Log.d("AddressSearch", "addressResponse: $addressResponse")
                            if (addressResponse.documents.isEmpty()) {
                                // 검색 결과가 없는 경우
                                Toast.makeText(this@AddressSearchActivity, "검색 결과가 없습니다. 다른 검색어를 시도해보세요.", Toast.LENGTH_SHORT).show()
                                addressAdapter.clearAddresses() // 어댑터의 기존 데이터를 지우는 메소드 (필요시 구현)
                            } else {
                                // 검색 결과가 있는 경우
                                addressAdapter.setAddresses(addressResponse.documents)
                            }
                        } else {
                            Toast.makeText(this@AddressSearchActivity, "응답 데이터를 처리할 수 없습니다.", Toast.LENGTH_SHORT).show()
                        }
                    } else {
                        val errorMsg = "주소 검색에 실패했습니다(${response.code()})"
                        Log.e("AddressSearch", "에러 코드: ${response.code()}, 메시지: ${response.message()}")
                        Toast.makeText(this@AddressSearchActivity, errorMsg, Toast.LENGTH_SHORT).show()
                    }
                }


                override fun onFailure(call: Call<KakaoAddressResponse>, t: Throwable) {
                    // 검색 완료 상태로 변경
                    isSearching = false

                    // 프로그레스 다이얼로그 닫기
                    progressDialog?.dismiss()
                    progressDialog = null

                    Log.e("AddressSearch", "통신 오류", t)
                    Toast.makeText(this@AddressSearchActivity, "오류가 발생했습니다: ${t.message}", Toast.LENGTH_SHORT).show()
                }
            })
        } catch (e: Exception) {
            // 예외 발생 시 처리
            isSearching = false
            progressDialog?.dismiss()
            progressDialog = null

            Log.e("AddressSearch", "API 호출 중 예외 발생", e)
            Toast.makeText(this, "API 호출 중 오류가 발생했습니다", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 액티비티 종료 시 다이얼로그 닫기
        progressDialog?.dismiss()
        progressDialog = null
    }

    override fun onSupportNavigateUp(): Boolean {
        onBackPressed()
        return true
    }
}

AddressAdpater.kt

package com.ygr.ygr_wholesale2.adapter


import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.ygr.ygr_wholesale2.R
import com.ygr.ygr_wholesale2.viewmodels.Document

class AddressAdapter(private val onItemClick: (Document) -> Unit) :
    RecyclerView.Adapter<AddressAdapter.AddressViewHolder>() {

    private val addresses = mutableListOf<Document>()

    // Add this method to clear the addresses
    fun clearAddresses() {
        addresses.clear()
        notifyDataSetChanged()
    }

    fun setAddresses(newAddresses: List<Document>) {
        addresses.clear()
        addresses.addAll(newAddresses)
        notifyDataSetChanged()
    }

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

    override fun onBindViewHolder(holder: AddressViewHolder, position: Int) {
        holder.bind(addresses[position])
    }

    override fun getItemCount(): Int = addresses.size

    inner class AddressViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val tvRoadAddress: TextView = itemView.findViewById(R.id.tvRoadAddress)
        private val tvAddress: TextView = itemView.findViewById(R.id.tvAddress)

        init {
            itemView.setOnClickListener {
                val position = adapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    onItemClick(addresses[position])
                }
            }
        }

        fun bind(document: Document) {
            tvRoadAddress.text = document.road_address?.address_name ?: "도로명 주소 없음"
            tvAddress.text = document.address.address_name
        }
    }
}

KakaoApiService.kt

package com.ygr.ygr_wholesale2.api

import retrofit2.Call  // 올바른 impor
import com.ygr.ygr_wholesale2.viewmodels.KakaoAddressResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query

interface KakaoApiService {
    @GET("v2/local/search/address.json")
    fun searchAddress(
        @Header("Authorization") authorization: String,
        @Query("query") query: String,
        @Query("page") page: Int = 1,
        @Query("size") size: Int = 10
    ): Call<KakaoAddressResponse>  // suspend 제거, Call 반환
}

Retrofit이 인코딩 하고 있으니 앞서 인코딩 하지 마시고 사용해 보시겟어요?

개발자로 존경을 담아 보냅니다. 기본적인 것도 잘 몰라 귀찮게 해드려 정말 죄송합니다.
압도적 감사를 보냅니다.