📌 Intro
카카오에서 제공하는 API중 REST API를 통한 장소 검색 기능이 있다. Retrofit 라이브러리를 이용하여 API 서버와 통신하고, 장소 검색 결과를 가져오는 방법을 정리하려고 한다.
📌 Retrofit 라이브러리 추가
build.gradle에 위 코드를 추가해주도록 한다. 글을 작성하는 시점에서 Retrofit의 최신 버전은 2.9.0버전이지만 새로운 최신 버전이 나왔다고 하더라도 안드로이스 스튜디오에서 아래와 같이 알려주기 때문에 맞게 고쳐주면 된다.
📌 Class & Interface 생성
검색 결과를 담을 클래스와 통신에 사용하는 인터페이스를 만들어줘야 한다. 검색 시 매개변수, 검색 결과 데이터에 대한 자세한 정보는 아래 링크를 통해 확인할 수 있다. https://developers.kakao.com/docs/latest/ko/local/dev-guide#search-by-keyword
Class
- ResultSearchKeyword.java
package com.example.kakaomap;
import java.util.List;
// 검색 결과를 담는 클래스
class ResultSearchKeyword {
PlaceMeta meta; // 장소 메타데이터
List<Place> documents; // 검색 결과
}
class PlaceMeta{
int total_count; // 검색어에 검색된 문서 수
int pageable_count; // total_count 중 노출 가능 문서 수, 최대 45 (API에서 최대 45개 정보만 제공)
Boolean is_end; // 현재 페이지가 마지막 페이지인지 여부, 값이 false면 page를 증가시켜 다음 페이지를 요청할 수 있음
RegionInfo same_name; // 질의어의 지역 및 키워드 분석 정보
}
class RegionInfo {
List<String> region; // 질의어에서 인식된 지역의 리스트, ex) '중앙로 맛집' 에서 중앙로에 해당하는 지역 리스트
String keyword; // 질의어에서 지역 정보를 제외한 키워드, ex) '중앙로 맛집' 에서 '맛집'
String selected_region; // 인식된 지역 리스트 중, 현재 검색에 사용된 지역 정보
}
class Place {
String id; // 장소 ID
String place_name; // 장소명, 업체명
String category_name; // 카테고리 이름
String category_group_code; // 중요 카테고리만 그룹핑한 카테고리 그룹 코드
String category_group_name; // 중요 카테고리만 그룹핑한 카테고리 그룹명
String phone; // 전화번호
String address_name; // 전체 지번 주소
String road_address_name; // 전체 도로명 주소
String x; // X 좌표값 혹은 longitude
String y; // Y 좌표값 혹은 latitude
String place_url; // 장소 상세페이지 URL
String distanc; // 중심좌표까지의 거리. 단, x,y 파라미터를 준 경우에만 존재. 단위는 meter
}
위 코드는 서버에서 받을 수 있는 정보를 모두 받을 수 있도록 만들어둔 상태다. 이 중에서 필요한 정보만 받아와 사용해도 괜찮다. 나는 place_name, address_name, road_address_name, x, y의 정보가 필요했기 때문에 ResultSearchKeyword를 아래와 같이 바꾸었다.
- ResultSearchKeyword.java(변경 후)
package com.example.kakaomap;
import java.util.List;
public class ResultSearchKeyword {
List<Place> documents; // 검색 결과
}
class Place{
String place_name; // 장소명
String address_name; // 전체 번지 주소
String road_address_name; // 전체 도로명 주소
String x; // X 좌표값 or longitude
String y; // Y 좌표값 or latitude
}
Interface
package com.example.kakaomap;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Query;
public interface KakaoAPI {
@GET("v2/local/search/keyword.json") // Keyword.json 의 정보를 받아옴
// 받아온 정보가 ResultSearchKeyword 클래스의 구조로 담김
Call<ResultSearchKeyword> getSearchKeyword(
@Header("Authorization") String key, // 카카오 API 인증키
@Query("query") String query); // 검색을 원하는 질의어
}
검색조건을 더 추가하려면 @Query(”매개변수 이름”)을 이용하여 추가할 수 있다. 추가 가능한 매개변수는 아래 링크를 참고하면 좋다.
https://developers.kakao.com/docs/latest/ko/reference/rest-api-reference#rest-api-list-local
소스코드
activity_main.xml
package com.example.kakaomap;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.Toast;
import net.daum.mf.map.api.MapPoint;
import net.daum.mf.map.api.MapView;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends AppCompatActivity implements MapView.CurrentLocationEventListener, MapView.MapViewEventListener {
private static final String LOG_TAG = "MainActivity";
private MapView mapView;
private ViewGroup mapViewContainer;
private static final int GPS_ENABLE_REQUEST_CODE = 2001;
private static final int PERMISSIONS_REQUEST_CODE = 100;
private String BASE_URL = "https://dapi.kakao.com/";
private String API_KEY = "KakaoAK a2d69db3c795c70fb4bcb73b7794abfb";
String[] REQUIRED_PERMISSIONS = {Manifest.permission.ACCESS_FINE_LOCATION};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 지도 띄우기
mapView = new MapView(this);
mapViewContainer = (ViewGroup)findViewById(R.id.map);
mapViewContainer.addView(mapView);
if(!checkLocationServicesStatus()){
// GPS 활성화
showDialogForLocationServiceSetting();
}
else{
// 런타임 퍼미션 처리
checkRunTimePermission();
// 현 위치 잡기
mapView.setCurrentLocationTrackingMode(MapView.CurrentLocationTrackingMode.TrackingModeOnWithoutHeading);
}
// 키워드 검색
searchKeyword("인하대 CU");
}
// KeyHash 얻는 코드
private void getAppKeyHash(){
PackageInfo packageInfo = null;
try{
packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
}catch(PackageManager.NameNotFoundException e){
e.printStackTrace();
}
if(packageInfo == null){
Log.e("KeyHash", "KeyHash : null");
}
for(Signature signature : packageInfo.signatures){
try{
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(signature.toByteArray());
Log.e("KeyHash", Base64.encodeToString(md.digest(), Base64.DEFAULT));
}catch(NoSuchAlgorithmException e){
e.printStackTrace();
}
}
}
@Override
public void onCurrentLocationUpdate(MapView mapView, MapPoint currentLocation, float accuracyInMeters) {
MapPoint.GeoCoordinate mapPointGeo = currentLocation.getMapPointGeoCoord();
Log.e(LOG_TAG, String.format("MapView onCurrentLocationUpdate (%f,%f) accuracy (%f)", mapPointGeo.latitude, mapPointGeo.longitude, accuracyInMeters));
}
@Override
public void onRequestPermissionsResult(int permsRequestCode,
@NonNull String[] permissions,
@NonNull int[] grandResults) {
super.onRequestPermissionsResult(permsRequestCode, permissions, grandResults);
if (permsRequestCode == PERMISSIONS_REQUEST_CODE && grandResults.length == REQUIRED_PERMISSIONS.length) {
// 요청 코드가 PERMISSIONS_REQUEST_CODE 이고, 요청한 퍼미션 개수만큼 수신되었다면
boolean check_result = true;
// 모든 퍼미션을 허용했는지 체크한다.
for (int result : grandResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
check_result = false;
break;
}
}
if (check_result) {
Log.d(LOG_TAG, "start");
//위치 값을 가져올 수 있음
} else {
// 거부한 퍼미션이 있다면 앱을 사용할 수 없는 이유를 설명해주고 앱을 종료한다.2 가지 경우가 있다
if (ActivityCompat.shouldShowRequestPermissionRationale(this, REQUIRED_PERMISSIONS[0])) {
Toast.makeText(MainActivity.this, "퍼미션이 거부되었습니다. 앱을 다시 실행하여 퍼미션을 허용해주세요.", Toast.LENGTH_LONG).show();
finish();
} else {
Toast.makeText(MainActivity.this, "퍼미션이 거부되었습니다. 설정(앱 정보)에서 퍼미션을 허용해야 합니다. ", Toast.LENGTH_LONG).show();
}
}
}
}
void checkRunTimePermission(){
// 런타임 퍼미션 처리
// 1. 위치 퍼미션을 가지고 있는지 체크한다.
int hasFineLocationPermission = ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.ACCESS_FINE_LOCATION);
if (hasFineLocationPermission == PackageManager.PERMISSION_GRANTED ) {
// 2. 이미 퍼미션을 가지고 있다면
// ( 안드로이드 6.0 이하 버전은 런타임 퍼미션이 필요없기 때문에 이미 허용된 걸로 인식한다.)
// 3. 위치 값을 가져올 수 있음
} else { //2. 퍼미션 요청을 허용한 적이 없다면 퍼미션 요청이 필요하다. 2가지 경우(3-1, 4-1)가 있다.
// 3-1. 사용자가 퍼미션 거부를 한 적이 있는 경우에는
if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, REQUIRED_PERMISSIONS[0])) {
// 3-2. 요청을 진행하기 전에 사용자가에게 퍼미션이 필요한 이유를 설명해줄 필요가 있다.
Toast.makeText(MainActivity.this, "이 앱을 실행하려면 위치 접근 권한이 필요합니다.", Toast.LENGTH_LONG).show();
// 3-3. 사용자게에 퍼미션 요청을 합니다. 요청 결과는 onRequestPermissionsResult에서 수신된다.
ActivityCompat.requestPermissions(MainActivity.this, REQUIRED_PERMISSIONS,
PERMISSIONS_REQUEST_CODE);
} else {
// 4-1. 사용자가 퍼미션 거부를 한 적이 없는 경우에는 퍼미션 요청을 바로 합니다.
// 요청 결과는 onRequestPermissionsResult에서 수신된다.
ActivityCompat.requestPermissions(MainActivity.this, REQUIRED_PERMISSIONS,
PERMISSIONS_REQUEST_CODE);
}
}
}
// 여기부터는 GPS 활성화를 위한 메소드들
private void showDialogForLocationServiceSetting() {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("위치 서비스 비활성화");
builder.setMessage("앱을 사용하기 위해서는 위치 서비스가 필요합니다.\n"
+ "위치 설정을 수정하시겠습니까?");
builder.setCancelable(true);
builder.setPositiveButton("설정", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
Intent callGPSSettingIntent
= new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivityForResult(callGPSSettingIntent, GPS_ENABLE_REQUEST_CODE);
}
});
builder.setNegativeButton("취소", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
builder.create().show();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case GPS_ENABLE_REQUEST_CODE:
//사용자가 GPS 활성 시켰는지 검사
if (checkLocationServicesStatus()) {
if (checkLocationServicesStatus()) {
Log.d(LOG_TAG, "onActivityResult : GPS 활성화 되있음");
checkRunTimePermission();
return;
}
}
break;
}
}
public boolean checkLocationServicesStatus(){
LocationManager locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}
@Override
public void onCurrentLocationDeviceHeadingUpdate(MapView mapView, float v) {
}
@Override
public void onCurrentLocationUpdateFailed(MapView mapView) {
}
@Override
public void onCurrentLocationUpdateCancelled(MapView mapView) {
}
@Override
public void onMapViewInitialized(MapView mapView) {
}
@Override
public void onMapViewCenterPointMoved(MapView mapView, MapPoint mapPoint) {
}
@Override
public void onMapViewZoomLevelChanged(MapView mapView, int i) {
}
@Override
public void onMapViewSingleTapped(MapView mapView, MapPoint mapPoint) {
}
@Override
public void onMapViewDoubleTapped(MapView mapView, MapPoint mapPoint) {
}
@Override
public void onMapViewLongPressed(MapView mapView, MapPoint mapPoint) {
}
@Override
public void onMapViewDragStarted(MapView mapView, MapPoint mapPoint) {
}
@Override
public void onMapViewDragEnded(MapView mapView, MapPoint mapPoint) {
}
@Override
public void onMapViewMoveFinished(MapView mapView, MapPoint mapPoint) {
}
ResultSearchKeyword.java
package com.example.kakaomap;
import java.util.List;
public class ResultSearchKeyword {
List<Place> documents; // 검색 결과
}
class Place{
String place_name; // 장소명
String address_name; // 전체 번지 주소
String road_address_name; // 전체 도로명 주소
String x; // X 좌표값 or longitude
String y; // Y 좌표값 or latitude
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.kakaomap">
<!-- 인터넷, 위치 권한 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.KakaoMap">
<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data android:name="com.kakao.sdk.AppKey"
android:value="707cd09e072663839a1e58ca07a08dc6"/>
</application>
</manifest>
KakaoAPI.java
package com.example.kakaomap;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Query;
public interface KakaoAPI {
@GET("v2/local/search/keyword.json") // Keyword.json 의 정보를 받아옴
// 받아온 정보가 ResultSearchKeyword 클래스의 구조로 담김
Call<ResultSearchKeyword> getSearchKeyword(
@Header("Authorization") String key, // 카카오 API 인증키
@Query("query") String query); // 검색을 원하는 질의어
}
📌 실행 결과
검색어를 “인하대 CU” 로 했을 때 인하대 근처 CU가 검색되어 나온 것을 확인할 수 있다.
📌 참고
[1] https://mechacat.tistory.com/15?category=449793
[2] https://developers.kakao.com/docs/latest/ko/reference/rest-api-reference#rest-api-list-local
[3] https://developers.kakao.com/docs/latest/ko/local/dev-guide#search-by-keyword