문의 시, 디벨로퍼스 앱ID를 알려주세요.
1206938
- 개인정보 항목 심사는 운영하시는 서비스 일반회원가입에서 입력 받는 항목을 기준으로 제공됩니다.
- 코틀린에서 주소 API를 이용하고 있는데 계속 403 오류가 납니다.
문의 시, 디벨로퍼스 앱ID를 알려주세요.
1206938
안녕하세요.
앱 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이 인코딩 하고 있으니 앞서 인코딩 하지 마시고 사용해 보시겟어요?
개발자로 존경을 담아 보냅니다. 기본적인 것도 잘 몰라 귀찮게 해드려 정말 죄송합니다.
압도적 감사를 보냅니다.