주소 양식에 Place Autocomplete 추가하기

배송지 주소, 결제 정보 또는 이벤트 정보를 작성할 때 장소 자동 완성으로 양식을 사용 설정하면 사용자가 주소 정보를 입력할 때 키 입력 횟수와 실수를 줄일 수 있습니다. 이 튜토리얼에서는 Place Autocomplete를 사용하여 입력란을 사용 설정하고, 사용자가 선택한 주소의 주소 구성요소로 주소 양식 필드를 채우고, 선택한 주소를 지도에 표시하여 시각적으로 확인하는 데 필요한 단계를 설명합니다.

동영상: Place Autocomplete으로 주소 양식 개선하기

주소 양식

Android

iOS

Google Maps Platform은 Place Autocomplete 위젯을 모바일 플랫폼과 웹에 제공합니다. 위의 그림에서도 보았지만 이 위젯은 위치 범위를 지정한 검색에도 최적화할 수 있는 자동 완성 기능이 내장된 검색 대화상자를 제공합니다.

코드 가져오기

GitHub에서 Android 데모용 Google 장소 SDK 저장소를 클론하거나 다운로드합니다.

활동의 Java 버전을 확인하세요.

    /*
 * Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.placesdemo;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewStub;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

import com.example.placesdemo.databinding.AutocompleteAddressActivityBinding;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMapOptions;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.libraries.places.api.Places;
import com.google.android.libraries.places.api.model.AddressComponent;
import com.google.android.libraries.places.api.model.AddressComponents;
import com.google.android.libraries.places.api.model.Place;
import com.google.android.libraries.places.api.model.TypeFilter;
import com.google.android.libraries.places.api.net.PlacesClient;
import com.google.android.libraries.places.widget.Autocomplete;
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static com.google.maps.android.SphericalUtil.computeDistanceBetween;

/**
 * Activity for using Place Autocomplete to assist filling out an address form.
 */
@SuppressWarnings("FieldCanBeLocal")
public class AutocompleteAddressActivity extends AppCompatActivity implements OnMapReadyCallback {

    private static final String TAG = "ADDRESS_AUTOCOMPLETE";
    private static final String MAP_FRAGMENT_TAG = "MAP";
    private LatLng coordinates;
    private boolean checkProximity = false;
    private SupportMapFragment mapFragment;
    private GoogleMap map;
    private Marker marker;
    private PlacesClient placesClient;
    private View mapPanel;
    private LatLng deviceLocation;
    private static final double acceptedProximity = 150;

    private AutocompleteAddressActivityBinding binding;

    View.OnClickListener startAutocompleteIntentListener = view -> {
        view.setOnClickListener(null);
        startAutocompleteIntent();
    };

    private final ActivityResultLauncher<Intent> startAutocomplete = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent intent = result.getData();
                    if (intent != null) {
                        Place place = Autocomplete.getPlaceFromIntent(intent);

                        // Write a method to read the address components from the Place
                        // and populate the form with the address components
                        Log.d(TAG, "Place: " + place.getAddressComponents());
                        fillInAddress(place);
                    }
                } else if (result.getResultCode() == Activity.RESULT_CANCELED) {
                    // The user canceled the operation.
                    Log.i(TAG, "User canceled autocomplete");
                }
            });

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);
        binding.autocompleteAddress1.setOnClickListener(startAutocompleteIntentListener);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = AutocompleteAddressActivityBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Retrieve a PlacesClient (previously initialized - see MainActivity)
        placesClient = Places.createClient(this);

        // Attach an Autocomplete intent to the Address 1 EditText field
        binding.autocompleteAddress1.setOnClickListener(startAutocompleteIntentListener);

        // Update checkProximity when user checks the checkbox
        CheckBox checkProximityBox = findViewById(R.id.checkbox_proximity);
        checkProximityBox.setOnCheckedChangeListener((view, isChecked) -> {
            // Set the boolean to match user preference for when the Submit button is clicked
            checkProximity = isChecked;
        });

        // Submit and optionally check proximity
        Button saveButton = findViewById(R.id.autocomplete_save_button);
        saveButton.setOnClickListener(v -> saveForm());

        // Reset the form
        Button resetButton = findViewById(R.id.autocomplete_reset_button);
        resetButton.setOnClickListener(v -> clearForm());
    }

    private void startAutocompleteIntent() {

        // Set the fields to specify which types of place data to
        // return after the user has made a selection.
        List<Place.Field> fields = Arrays.asList(Place.Field.ADDRESS_COMPONENTS,
                Place.Field.LAT_LNG, Place.Field.VIEWPORT);

        // Build the autocomplete intent with field, country, and type filters applied
        Intent intent = new Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields)
                .setCountries(Arrays.asList("US"))
                .setTypesFilter(new ArrayList<String>() {{
                    add(TypeFilter.ADDRESS.toString().toLowerCase());
                }})
                .build(this);
        startAutocomplete.launch(intent);
    }

    @Override
    public void onMapReady(@NonNull GoogleMap googleMap) {
        map = googleMap;
        try {
            // Customise the styling of the base map using a JSON object defined
            // in a string resource.
            boolean success = map.setMapStyle(
                    MapStyleOptions.loadRawResourceStyle(this, R.raw.style_json));

            if (!success) {
                Log.e(TAG, "Style parsing failed.");
            }
        } catch (Resources.NotFoundException e) {
            Log.e(TAG, "Can't find style. Error: ", e);
        }
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(coordinates, 15f));
        marker = map.addMarker(new MarkerOptions().position(coordinates));
    }

    private void fillInAddress(Place place) {
        AddressComponents components = place.getAddressComponents();
        StringBuilder address1 = new StringBuilder();
        StringBuilder postcode = new StringBuilder();

        // Get each component of the address from the place details,
        // and then fill-in the corresponding field on the form.
        // Possible AddressComponent types are documented at https://goo.gle/32SJPM1
        if (components != null) {
            for (AddressComponent component : components.asList()) {
                String type = component.getTypes().get(0);
                switch (type) {
                    case "street_number": {
                        address1.insert(0, component.getName());
                        break;
                    }

                    case "route": {
                        address1.append(" ");
                        address1.append(component.getShortName());
                        break;
                    }

                    case "postal_code": {
                        postcode.insert(0, component.getName());
                        break;
                    }

                    case "postal_code_suffix": {
                        postcode.append("-").append(component.getName());
                        break;
                    }

                    case "locality":
                        binding.autocompleteCity.setText(component.getName());
                        break;

                    case "administrative_area_level_1": {
                        binding.autocompleteState.setText(component.getShortName());
                        break;
                    }

                    case "country":
                        binding.autocompleteCountry.setText(component.getName());
                        break;
                }
            }
        }

        binding.autocompleteAddress1.setText(address1.toString());
        binding.autocompletePostal.setText(postcode.toString());

        // After filling the form with address components from the Autocomplete
        // prediction, set cursor focus on the second address line to encourage
        // entry of sub-premise information such as apartment, unit, or floor number.
        binding.autocompleteAddress2.requestFocus();

        // Add a map for visual confirmation of the address
        showMap(place);
    }

    private void showMap(Place place) {
        coordinates = place.getLatLng();

        // It isn't possible to set a fragment's id programmatically so we set a tag instead and
        // search for it using that.
        mapFragment = (SupportMapFragment)
                getSupportFragmentManager().findFragmentByTag(MAP_FRAGMENT_TAG);

        // We only create a fragment if it doesn't already exist.
        if (mapFragment == null) {
            mapPanel = ((ViewStub) findViewById(R.id.stub_map)).inflate();
            GoogleMapOptions mapOptions = new GoogleMapOptions();
            mapOptions.mapToolbarEnabled(false);

            // To programmatically add the map, we first create a SupportMapFragment.
            mapFragment = SupportMapFragment.newInstance(mapOptions);

            // Then we add it using a FragmentTransaction.
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.confirmation_map, mapFragment, MAP_FRAGMENT_TAG)
                    .commit();
            mapFragment.getMapAsync(this);
        } else {
            updateMap(coordinates);
        }
    }

    private void updateMap(LatLng latLng) {
        marker.setPosition(latLng);
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f));
        if (mapPanel.getVisibility() == View.GONE) {
            mapPanel.setVisibility(View.VISIBLE);
        }
    }

    private void saveForm() {
        Log.d(TAG, "checkProximity = " + checkProximity);
        if (checkProximity) {
            checkLocationPermissions();
        } else {
            Toast.makeText(
                            this,
                            R.string.autocomplete_skipped_message,
                            Toast.LENGTH_SHORT)
                    .show();
        }
    }

    private void clearForm() {
        binding.autocompleteAddress1.setText("");
        binding.autocompleteAddress2.getText().clear();
        binding.autocompleteCity.getText().clear();
        binding.autocompleteState.getText().clear();
        binding.autocompletePostal.getText().clear();
        binding.autocompleteCountry.getText().clear();
        if (mapPanel != null) {
            mapPanel.setVisibility(View.GONE);
        }
        binding.autocompleteAddress1.requestFocus();
    }

    // Register the permissions callback, which handles the user's response to the
    // system permissions dialog. Save the return value, an instance of
    // ActivityResultLauncher, as an instance variable.
    private final ActivityResultLauncher<String> requestPermissionLauncher =
            registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
                if (isGranted) {
                    // Since ACCESS_FINE_LOCATION is the only permission in this sample,
                    // run the location comparison task once permission is granted.
                    // Otherwise, check which permission is granted.
                    getAndCompareLocations();
                } else {
                    // Fallback behavior if user denies permission
                    Log.d(TAG, "User denied permission");
                }
            });

    private void checkLocationPermissions() {
        if (ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            getAndCompareLocations();
        } else {
            requestPermissionLauncher.launch(
                    ACCESS_FINE_LOCATION);
        }
    }

    @SuppressLint("MissingPermission")
    private void getAndCompareLocations() {
        // TODO: Detect and handle if user has entered or modified the address manually and update
        // the coordinates variable to the Lat/Lng of the manually entered address. May use
        // Geocoding API to convert the manually entered address to a Lat/Lng.
        LatLng enteredLocation = coordinates;
        map.setMyLocationEnabled(true);

        FusedLocationProviderClient fusedLocationClient =
                LocationServices.getFusedLocationProviderClient(this);

        fusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, location -> {
                    // Got last known location. In some rare situations this can be null.
                    if (location == null) {
                        return;
                    }

                    deviceLocation = new LatLng(location.getLatitude(), location.getLongitude());
                    Log.d(TAG, "device location = " + deviceLocation);
                    Log.d(TAG, "entered location = " + enteredLocation.toString());

                    // Use the computeDistanceBetween function in the Maps SDK for Android Utility Library
                    // to use spherical geometry to compute the distance between two Lat/Lng points.
                    double distanceInMeters = computeDistanceBetween(deviceLocation, enteredLocation);
                    if (distanceInMeters <= acceptedProximity) {
                        Log.d(TAG, "location matched");
                        // TODO: Display UI based on the locations matching
                    } else {
                        Log.d(TAG, "location not matched");
                        // TODO: Display UI based on the locations not matching
                    }
                });
    }
}
    

API 사용 설정

이러한 권장사항을 구현하려면 Google Cloud 콘솔에서 다음 API를 사용 설정해야 합니다.

설정에 관한 자세한 내용은 Google Cloud 프로젝트 설정을 참고하세요.

입력란에 자동 완성 추가하기

이 섹션에서는 주소 양식에 Place Autocomplete를 추가하는 방법을 설명합니다.

Place Autocomplete 위젯 추가하기

Android에서는 Autocomplete 인텐트를 사용해 자동완성 위젯을 추가할 수 있습니다. 그러면 사용자가 처음 주소를 입력하는 주소 입력란 1에서 Place Autocomplete이 실행됩니다. 사용자가 주소를 입력하기 시작하면 Autocomplete 예상 검색어 목록에서 주소를 선택할 수 있습니다.

먼저 앞서 시작된 활동의 결과를 수신 대기하는 ActivityResultLauncher를 사용해 활동 런처를 준비합니다. 결과 콜백에는 사용자가 Autocomplete 예상 검색어 목록에서 선택하는 주소에 해당하는 Place 객체가 포함됩니다.

    private final ActivityResultLauncher<Intent> startAutocomplete = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent intent = result.getData();
                    if (intent != null) {
                        Place place = Autocomplete.getPlaceFromIntent(intent);

                        // Write a method to read the address components from the Place
                        // and populate the form with the address components
                        Log.d(TAG, "Place: " + place.getAddressComponents());
                        fillInAddress(place);
                    }
                } else if (result.getResultCode() == Activity.RESULT_CANCELED) {
                    // The user canceled the operation.
                    Log.i(TAG, "User canceled autocomplete");
                }
            });

그런 다음 Place Autocomplete 인텐트의 입력란, 위치 및 유형 속성을 정의한 후 Autocomplete.IntentBuilder를 사용해 빌드합니다. 마지막으로 위의 코드 샘플에서 정의한 ActivityResultLauncher를 사용해 인텐트를 실행합니다.

    private void startAutocompleteIntent() {

        // Set the fields to specify which types of place data to
        // return after the user has made a selection.
        List<Place.Field> fields = Arrays.asList(Place.Field.ADDRESS_COMPONENTS,
                Place.Field.LAT_LNG, Place.Field.VIEWPORT);

        // Build the autocomplete intent with field, country, and type filters applied
        Intent intent = new Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields)
                .setCountries(Arrays.asList("US"))
                .setTypesFilter(new ArrayList<String>() {{
                    add(TypeFilter.ADDRESS.toString().toLowerCase());
                }})
                .build(this);
        startAutocomplete.launch(intent);
    }

Place Autocomplete에서 반환한 주소 처리하기

앞에서 ActivityResultLauncher를 정의했기 때문에 활동 결과가 콜백에서 반환될 때 실행할 작업도 정의되었습니다. 사용자가 예상 목록에서 선택했다면 결과 객체에 포함된 인텐트에서 전송됩니다. 인텐트는 Autocomplete.IntentBuilder로 빌드되었기 때문에 메서드 Autocomplete.getPlaceFromIntent()가 인텐트에서 Place 객체를 추출할 수 있습니다.

    private final ActivityResultLauncher<Intent> startAutocomplete = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent intent = result.getData();
                    if (intent != null) {
                        Place place = Autocomplete.getPlaceFromIntent(intent);

                        // Write a method to read the address components from the Place
                        // and populate the form with the address components
                        Log.d(TAG, "Place: " + place.getAddressComponents());
                        fillInAddress(place);
                    }
                } else if (result.getResultCode() == Activity.RESULT_CANCELED) {
                    // The user canceled the operation.
                    Log.i(TAG, "User canceled autocomplete");
                }
            });

그런 다음 Place.getAddressComponents()를 호출하여 각 주소 구성요소를 주소 양식에 있는 해당 입력란과 비교하면서 사용자가 선택한 Place의 값을 사용해 입력란을 채웁니다.

주소 양식 필드를 채우는 구현 예는 이 페이지의 코드 가져오기 섹션에 제공된 샘플 코드의 fillInAddress 메서드에서 공유됩니다.

수동으로 입력한 주소가 아닌 예상 목록에서 주소 데이터를 캡처하기 때문에 주소의 정확성이 보장될 뿐만 아니라 주소가 파악되어 해당 주소로 확실히 상품이 배송될 수 있으며 사용자의 키 입력이 줄어듭니다.

Place Autocomplete 구현 시 고려사항

Place Autocomplete에는 위젯 그 이상을 사용하려는 경우 유연하게 구현하기 위한 다양한 옵션이 있습니다. 일치하는 위치를 적절한 방법으로 찾는 데 필요한 정확한 데이터를 얻기 위해 여러 서비스를 조합하여 사용할 수 있습니다.

  • 주소 양식의 경우 유형 매개변수를 address로 설정하면 상세 주소 전체가 일치하는 주소만 가져올 수 있습니다. Place Autocomplete 요청 시 지원되는 유형에 대해 자세히 알아보세요.

  • 전 세계를 검색할 필요가 없다면 적절한 제한 및 상세 검색을 설정합니다. 특정 지역과 일치하는 데이터만 가져오기 위한 상세 검색 또는 제한에 사용할 수 있는 다양한 매개변수가 있습니다.

    • 특정 지역으로 검색 범위를 제한하기 위해 직사각형 경계를 설정하려면 RectangularBounds를 사용하고, 해당 지역의 주소만 반환되도록 하려면 setLocationRestriction()을 사용합니다.

    • 응답을 몇 개의 특정 국가로만 제한하려면 setCountries()를 사용합니다.

  • 특정 필드가 검색에서 누락될 경우를 대비하여 필드를 수정 가능한 상태로 두고 고객이 필요한 경우 주소를 업데이트할 수 있게 합니다. Place Autocomplete에서 반환되는 대부분의 주소에는 아파트, 건물 번호, 호수 등의 세부 주소가 포함되지 않으므로 필요한 경우 해당 정보를 작성하도록 권장하기 위해 주소 입력란 2에 중점을 둡니다.

주소에 대한 시각적 확인 제공하기

주소 입력 시 지도에서 주소를 시각적으로 확인할 수 있는 기능을 사용자에게 제공합니다. 이로써 사용자는 주소가 정확하게 입력된 것을 한 번 더 확인할 수 있습니다.

다음 그림은 입력된 주소에 핀이 표시된 지도가 주소 아래에 있는 모습을 보여줍니다.

다음 예에서는 Android에서 지도를 추가하기 위한 기본 단계를 따릅니다. 자세한 내용은 해당 문서를 참고하세요.

SupportMapFragment 추가하기

먼저 SupportMapFragment 프래그먼트를 레이아웃 XML 파일에 추가합니다.

    <fragment
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:id="@+id/confirmation_map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

이때 프래그먼트가 아직 없다면 프로그래매틱 방식으로 프래그먼트를 추가합니다.

    private void showMap(Place place) {
        coordinates = place.getLatLng();

        // It isn't possible to set a fragment's id programmatically so we set a tag instead and
        // search for it using that.
        mapFragment = (SupportMapFragment)
                getSupportFragmentManager().findFragmentByTag(MAP_FRAGMENT_TAG);

        // We only create a fragment if it doesn't already exist.
        if (mapFragment == null) {
            mapPanel = ((ViewStub) findViewById(R.id.stub_map)).inflate();
            GoogleMapOptions mapOptions = new GoogleMapOptions();
            mapOptions.mapToolbarEnabled(false);

            // To programmatically add the map, we first create a SupportMapFragment.
            mapFragment = SupportMapFragment.newInstance(mapOptions);

            // Then we add it using a FragmentTransaction.
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.confirmation_map, mapFragment, MAP_FRAGMENT_TAG)
                    .commit();
            mapFragment.getMapAsync(this);
        } else {
            updateMap(coordinates);
        }
    }

핸들을 프래그먼트로 가져오고 콜백 등록하기

  1. 프래그먼트에 핸들을 가져오려면 FragmentManager.findFragmentById 메서드를 호출하고 레이아웃 파일에서 프래그먼트의 리소스 ID에 전달합니다. 프래그먼트를 동적으로 추가했다면 이미 핸들을 검색했으므로 이 단계를 건너뜁니다.

  2. getMapAsync 메서드를 호출하여 프래그먼트에서 콜백을 설정합니다.

예를 들어 프래그먼트를 정적으로 추가한 경우:

Kotlin

val mapFragment = supportFragmentManager
    .findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)

      

자바

SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
    .findFragmentById(R.id.map);
mapFragment.getMapAsync(this);

      

스타일을 지정하고 지도에 마커 추가하기

지도가 준비되면 스타일을 설정하고, 카메라를 중앙으로 이동한 후 입력된 주소의 좌표에 마커를 추가합니다. 다음 코드에서는 JSON 객체에서 정의한 스타일 지정을 사용하지만 그 밖에 클라우드 기반 지도 스타일 지정에서 정의한 지도 ID를 로드하는 방법도 있습니다.

    @Override
    public void onMapReady(@NonNull GoogleMap googleMap) {
        map = googleMap;
        try {
            // Customise the styling of the base map using a JSON object defined
            // in a string resource.
            boolean success = map.setMapStyle(
                    MapStyleOptions.loadRawResourceStyle(this, R.raw.style_json));

            if (!success) {
                Log.e(TAG, "Style parsing failed.");
            }
        } catch (Resources.NotFoundException e) {
            Log.e(TAG, "Can't find style. Error: ", e);
        }
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(coordinates, 15f));
        marker = map.addMarker(new MarkerOptions().position(coordinates));
    }

(전체 코드 샘플 보기)

지도 컨트롤 사용 중지하기

추가되는 지도 컨트롤(나침반, 툴바, 기타 기본 지형지물) 없이 위치만 표시하여 지도를 간단하게 유지하려면 불필요한 컨트롤을 사용하지 않는 것이 좋습니다. Android에서는 라이트 모드를 사용 설정하여 양방향 기능을 제한적으로 제공하는 옵션도 있습니다.