안드로이드 Daum 주소 검색 API 활용하기 [Android]
반응형

 

다음 주소 찾기 API 활용하기

회원가입이나 주문 과정에서, 배송 정보를 입력받기 위해서는 정확한 주소를 입력받아야 한다.

네이버 지도에선 주소 찾기 API를 제공하지 않아 다음의 우편번호 서비스 API를 활용하고자 한다.

KEY 발급이나, 사용량에 제한이 없고 언제나 무료로 사용할 수 있어서 정말 편한 것 같다.

 

Daum 우편번호 서비스

우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Daum 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.

postcode.map.daum.net

대략적인 방법은 다음과 같다.

  1. Daum API에서 제공하는 js 스크립트를 넣은 HTML 파일을 서버에 업로드
  2. 안드로이드에선 웹뷰로 해당 HTML 파일을 실행하기 -> 1번의 js 실행

 

안드로이드 세팅

일단, 주소 검색 버튼을 클릭했을 때 실행될 다이얼로그 프레그먼트를 설정해주도록 하자.

대충 웹 뷰를 만들고 대응하는 DialogFragment도 구현한 뒤 NavigationGraph에 등록하면 된다.

(XML 코드 보기 ▽)

더보기
더보기
<!--roadaddress_web_dialog.xml-->
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_marginTop="20dp"
    android:layout_marginBottom="50dp"
    android:layout_marginLeft="20dp"
    android:layout_marginRight="20dp">
    <com.google.android.material.card.MaterialCardView
        android:id="@+id/button_back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:clickable="true"
        app:cardBackgroundColor="#00FFFFFF"
        app:cardCornerRadius="27.5dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:rippleColor="@color/white">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:padding="10dp"
            android:src="@drawable/ic_baseline_arrow_back_24" />
    </com.google.android.material.card.MaterialCardView>

    <WebView
        android:id="@+id/daum_webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"/>

    <ProgressBar
        android:id="@+id/web_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

실행하는 방법은 onClick에서 인터넷 연결을 확인하고 -> 다이얼로그 프레그먼트가 실행될 수 있도록 코드를 구성하였다.

//사용하기 위해 버튼을 클릭하면 다이얼로그가 실행되게
binding.editTextSignUpRoadAddress.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        //인터넷 연결 확인
        int status = NetworkStatus.getConnectivityStatus(getContext());
        if(status == NetworkStatus.TYPE_MOBILE || status == NetworkStatus.TYPE_WIFI) {
            //주소검색 웹 뷰를 띄울 DialogFragment 선언
            navController.navigate(NavGraphDirections.actionGlobalRoadAddressSearchDialog());
        }else {
            Snackbar.make(binding.getRoot(), "인터넷 연결을 확인해주세요.", Snackbar.LENGTH_SHORT).show();
        }
    }
});

 

인터넷 연결은 다음과 같이 확인한다.

public class NetworkStatus {
    public static final int TYPE_WIFI = 1;
    public static final int TYPE_MOBILE = 2;
    public static final int TYPE_NOT_CONNECTED = 3;

    public static int getConnectivityStatus(Context context){
        ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

        NetworkInfo networkInfo = manager.getActiveNetworkInfo();
        if(networkInfo != null){
            int type = networkInfo.getType();
            if(type == ConnectivityManager.TYPE_MOBILE){ //모바일 (LTE, 5G)
                return TYPE_MOBILE;
            }else if(type == ConnectivityManager.TYPE_WIFI){ //WIFI
                return TYPE_WIFI;
            }
        }
        return TYPE_NOT_CONNECTED;  //연결 X
    }
}
 

주소 검색 API 호출

그럼 DialogFragment에서 어떻게 주소 검색 api를 호출하는지 살펴보자.

init_webView 함수를 살펴보면, 웹뷰를 설정하고 여러 js 설정을 진행한다.

webView.addJavascriptInterface(new AndroidBridge(), "MysosoApp"); 부분을 확인하면 , 모바일 웹과 안드로이드를 연결하기 위해 AndroidBridge라는 클래스를 만들어 사용하는 것을 확인할 수 있다.

우리는 "MysosoApp"으로 객체명을 정했으므로 -> JS에서도 MysosoApp을 불러 AndoridBridge 클래스 내부의 함수를 실행할 수 있게 된다.

 

그리고 주목해야 할 점은 webView.loadUrl("javascript:sample2_execDaumPostcode();"); HTML 내의 js 역시 안드로이드단에서 실행시킨다는 것이다 !! (그냥 로컬에서 html만 돌리면 안됨)

또한 호출하는 웹뷰 주소를 꼭 잘 확인하자! (서버에 올린 주소) webView.loadUrl("http://sososhopping.com:8080/roadSearch.html");

 

JS의 이벤트 (주소 검색)의 반응을 handler를 통해 별도의 스레드로 진행하도록 했는데,
내부에 선언한 AndroidBridge 클래스의 processDATA 함수를 확인해보면 데이터를 받아와 이전 BackStackEntry에 넣고 종료하는 것을 확인할 수 있다.

-> DialogFragment의 값을 BackStackEntry에 넣어 사용하는 법은 여기를 확인하면 된다.

public class RoadAddressSearchDialog extends DialogFragment {

    public static RoadAddressSearchDialog newInstance() {
        return new RoadAddressSearchDialog();
    }

    RoadaddressWebDialogBinding binding;
    NavController navController;
    WebView webView;
    Handler handler;

    @Override
    public void onStart() {
        super.onStart();
        Dialog dialog = getDialog();
        if (dialog != null) {
            dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        }
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        //binding 설정
        binding = RoadaddressWebDialogBinding.inflate(inflater, container, false);

        // 핸들러를 통한 JavaScript 이벤트 반응
        handler = new Handler();

        //Controller 설정
        navController = NavHostFragment.findNavController(this);

        init_webView();

        binding.buttonBack.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                getActivity().onBackPressed();
            }
        });

        return binding.getRoot();
    }

    public void init_webView() {
        // WebView 설정
        webView = binding.daumWebview;

        // JavaScript 허용
        webView.getSettings().setJavaScriptEnabled(true);

        // JavaScript의 window.open 허용
        webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);

        // JavaScript이벤트에 대응할 함수를 정의 한 클래스를 붙여줌
        webView.addJavascriptInterface(new AndroidBridge(), "MysosoApp");

        //DOMStorage 허용
        webView.getSettings().setDomStorageEnabled(true);

        //ssl 인증이 없는 경우 해결을 위한 부분
        webView.setWebChromeClient(new WebChromeClient() {
            @TargetApi(Build.VERSION_CODES.LOLLIPOP)
            @Override
            public void onPermissionRequest(final PermissionRequest request) {
                request.grant(request.getResources());
            }
        });

        webView.setWebViewClient(new WebViewClient() {

            @Override
            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                // SSL 에러가 발생해도 계속 진행
                handler.proceed();
            }

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }

            // 페이지 로딩 시작시 호출
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                Log.e("페이지 시작", url);
                binding.webProgress.setVisibility(View.VISIBLE);
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                binding.webProgress.setVisibility(View.GONE);

                Log.e("페이지 로딩", url);
                webView.loadUrl("javascript:sample2_execDaumPostcode();");
            }
        });

        // webview url load. php or html 파일 주소
        webView.loadUrl("http://sososhopping.com:8080/roadSearch.html");
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

    class AndroidBridge {
        @JavascriptInterface
        @SuppressWarnings("unused")
        public void processDATA(String roadAdd) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    navController.getPreviousBackStackEntry().getSavedStateHandle().set("roadAddress", roadAdd);
                    dismiss();
                }
            });
        }
    }
}

 

HTML 파일 업로드

올릴 HTML 파일은 가이드를 확인하거나, 서칭을 하다 보면 많이 뜬다.

나는 이 블로그를 참조했다.(가져왔다) (https://stickode.tistory.com/66)

구체적인 주소 검색 방법은 안에 삽입된 "postcode.v2.js" 스크립트가 해주는 것 같고, 우리는 이 받은 값을 안드로이드에 전달하는 부분을 집중하면 된다.

sample2_execDaumPostcode 함수의 마지막 부분 (window.MysosoApp.processDATA(fullRoadAddr);)을 살펴보면, 이전에 선언했던 AndridBridge 클래스의 processDATA를 호출하며 최종 주소를 전달하고 종료하는 것을 확인할 수 있다.

<!DOCTYPE html>
<html>
<head>
</head>
<body>

<!-- iOS에서는 position:fixed 버그가 있음, 적용하는 사이트에 맞게 position:absolute 등을 이용하여 top,left값 조정 필요 -->
<div id="layer" style="display:block;position:fixed;overflow:hidden;z-index:1;-webkit-overflow-scrolling:touch;">

</div>

<script src="http://dmaps.daum.net/map_js_init/postcode.v2.js"></script>
<script>


    window.addEventListener("message", onReceivedPostMessage, false);

    function onReceivedPostMessage(event){
        //..ex deconstruct event into action & params
        var action = event.data.action;
        var params = event.data.params;

        console.log("onReceivedPostMessage "+event);

    }

    function onReceivedActivityMessageViaJavascriptInterface(json){
        //..ex deconstruct data into action & params
        var data = JSON.parse(json);
        var action = data.action;
        var params = data.params;
        console.log("onReceivedActivityMessageViaJavascriptInterface "+event);
    }


    // 우편번호 찾기 화면을 넣을 element
    var element_layer = document.getElementById('layer');

    function sample2_execDaumPostcode() {
        new daum.Postcode({
            oncomplete: function(data) {

                // 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.

                // 각 주소의 노출 규칙에 따라 주소를 조합한다.
                // 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
                var fullAddr = data.address; // 최종 주소 변수
                var extraAddr = ''; // 조합형 주소 변수

                // 기본 주소가 도로명 타입일때 조합한다.
                if(data.addressType === 'R'){
                    //법정동명이 있을 경우 추가한다.
                    if(data.bname !== ''){
                        extraAddr += data.bname;
                    }
                    // 건물명이 있을 경우 추가한다.
                    if(data.buildingName !== ''){
                        extraAddr += (extraAddr !== '' ? ', ' + data.buildingName : data.buildingName);
                    }
                    // 조합형주소의 유무에 따라 양쪽에 괄호를 추가하여 최종 주소를 만든다.
                    fullAddr += (extraAddr !== '' ? ' ('+ extraAddr +')' : '');
                }


                var fullRoadAddr = data.roadAddress; // 도로명 주소 변수
                var extraRoadAddr = ''; // 도로명 조합형 주소 변수

                // 법정동명이 있을 경우 추가한다. (법정리는 제외)
                // 법정동의 경우 마지막 문자가 "동/로/가"로 끝난다.
                if(data.bname !== '' && /[동|로|가]$/g.test(data.bname)){
                    extraRoadAddr += data.bname;
                }
                // 건물명이 있고, 공동주택일 경우 추가한다.
                if(data.buildingName !== '' && data.apartment === 'Y'){
                    extraRoadAddr += (extraRoadAddr !== '' ? ', ' + data.buildingName : data.buildingName);
                }
                // 도로명, 지번 조합형 주소가 있을 경우, 괄호까지 추가한 최종 문자열을 만든다.
                if(extraRoadAddr !== ''){
                    extraRoadAddr = ' (' + extraRoadAddr + ')';
                }
                // 도로명, 지번 주소의 유무에 따라 해당 조합형 주소를 추가한다.
                if(fullRoadAddr !== ''){
                    fullRoadAddr += extraRoadAddr;
                }

                // 주소만 필요하므로 이것만 전송한다.
                window.MysosoApp.processDATA(fullRoadAddr);
            },
            width : '100%',
            height : '100%'
        }).embed(element_layer);

        // iframe을 넣은 element를 보이게 한다.
        element_layer.style.display = 'block';

        // iframe을 넣은 element의 위치를 화면의 가운데로 이동시킨다.
        initLayerPosition();
    }
    // 브라우저의 크기 변경에 따라 레이어를 가운데로 이동시키고자 하실때에는
    // resize이벤트나, orientationchange이벤트를 이용하여 값이 변경될때마다 아래 함수를 실행 시켜 주시거나,
    // 직접 element_layer의 top,left값을 수정해 주시면 됩니다.
    function initLayerPosition(){
        var width = (window.innerWidth || document.documentElement.clientWidth); //우편번호서비스가 들어갈 element의 width
        var height = (window.innerHeight || document.documentElement.clientHeight); //우편번호서비스가 들어갈 element의 height
        var borderWidth = 5; //샘플에서 사용하는 border의 두께

        // 위에서 선언한 값들을 실제 element에 넣는다.
        element_layer.style.width = width + 'px';
        element_layer.style.height = height + 'px';
        element_layer.style.border = borderWidth + 'px solid';
        // 실행되는 순간의 화면 너비와 높이 값을 가져와서 중앙에 뜰 수 있도록 위치를 계산한다.
        element_layer.style.left = (((window.innerWidth || document.documentElement.clientWidth) - width)/2 - borderWidth) + 'px';
        element_layer.style.top = (((window.innerHeight || document.documentElement.clientHeight) - height)/2 - borderWidth) + 'px';
    }
</script>
</body>
</html>

 

프로젝트에선 백엔드를 Spring Boot로 구성하여 진행하였고, Spring Boot는 정적 페이징을 지원하기 때문에 그냥 부트 프로젝트에선 resources 안에 html 파일을 넣어서 서버에 업로드하면 된다.

 

결과

전체적인 어플리케이션 코드는 다음 레포를 확인하면 된다.

 

GitHub - howtolivelikehuman/sososhopping_app_customer: 소소한 장보기 고객용 애플리케이션

소소한 장보기 고객용 애플리케이션. Contribute to howtolivelikehuman/sososhopping_app_customer development by creating an account on GitHub.

github.com

 

반응형