서버에 html 파일을 업로드 해야합니다. 서버를 빌드하는 방법이 다양하므로 지금은 생략하겠습니다. 추후에 작성하겠습니다.
1. 서버에 업로드할 html
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<!-- HELLO DAUM! -->
<!-- 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="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<!-- <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.Android.processDATA(fullRoadAddr); // data.zonecode + ", " +
},
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>
서버에 올라갈 html 코드 입니다.
2. MainActivity.kt
class MainActivity : AppCompatActivity() {
private var et_address: EditText? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
et_address = findViewById(R.id.et_address)
val btn_search = findViewById<Button>(R.id.button)
btn_search.setOnClickListener {
val intent = Intent(this@MainActivity, WebViewActivity::class.java)
getSearchResult.launch(intent)
}
}
private val getSearchResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult())
{ results ->
if(results.resultCode == RESULT_OK) {
if(results.data != null) {
val data = results.data!!.getStringExtra("data")
et_address?.setText(data)
}
}
}
}
registerForActivityResult에 reusltCode 가 RESULT_OK 일 경우 Text에 받아온 주소 data를 띄워주게 합니다. data는 WebViewActivity에서 넘어오게 됩니다.
3. WebViewActivity.kt
class WebViewActivity : AppCompatActivity() {
private lateinit var browser: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
browser = findViewById(R.id.webView)
browser.settings.javaScriptEnabled = true
browser.addJavascriptInterface(MyJavaScriptInterface(), "Android")
browser.setWebViewClient(object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
//page loading을 끝냈을 때 호출되는 콜백 메서드
//안드로이드에서 자바스크립트 메서드 호출
browser.loadUrl("javascript:sample2_execDaumPostcode();")
}
})
//최초로 웹뷰 로딩
browser.loadUrl("")
}
inner class MyJavaScriptInterface {
@JavascriptInterface
fun processDATA(data: String?) {
//자바 스크립트로 부터 다음 카카오 주소 검색 API 결과를 전달 받는다.
val extra = Bundle()
val intent = Intent()
extra.putString("data", data)
intent.putExtras(extra)
setResult(RESULT_OK, intent)
finish()
}
}
}
WebViewActivity.kt가 켜지면서 브라우저를 load 하게 됩니다. ( browser?.loadUrl("서버에 html 파일이 올라간 주소"))
browser?.loadUrl("javascript:sample2_execDaumPostcode();")
가 위의 html의 자바스크립트 코드가 됩니다. 서울과 같은 지명을 입력하면 이제 관련 지역을 리스트로 나오게 하는 함수가 되겠습니다.
그리고 리스트에 출력된 목록을 클릭 하게 되면, 데이터가 파싱되면서 fun processDATA(data: String?)를 콜백하면서 데이터가 전송되게 됩니다.
btn_search.setOnClickListener {
val intent = Intent(this@MainActivity, WebViewActivity::class.java)
getSearchResult.launch(intent)
}
private val getSearchResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult())
{ results ->
if(results.resultCode == RESULT_OK) {
if(results.data != null) {
val data = results.data!!.getStringExtra("data")
et_address?.setText(data)
}
}
}
getSearchResult가 registerForActivityResult 함수에 따라 WebViewActivity data를 받아서 텍스트에 띄우게 됩니다.
1. MainActivity.kt (init())
private fun init() {
// webViewAddress = // 메인 웹뷰
// webViewLayout = // 웹뷰가 속한 레이아웃
// 공통 설정
binding.webViewAddress.settings.run {
javaScriptEnabled = true// javaScript 허용으로 메인 페이지 띄움
javaScriptCanOpenWindowsAutomatically = true//javaScript window.open 허용
setSupportMultipleWindows(true)
}
binding.webViewAddress.addJavascriptInterface(AndroidBridge(), "TestApp")
binding.webViewAddress.loadUrl("")
binding.webViewAddress.webChromeClient = webChromeClient
}
2. MainActivity.kt (webChromeClient)
private val webChromeClient = object: WebChromeClient() {
/// ---------- 팝업 열기 ----------
/// - 카카오 JavaScript SDK의 로그인 기능은 popup을 이용합니다.
/// - window.open() 호출 시 별도 팝업 webview가 생성되어야 합니다.
///
lateinit var dialog : Dialog
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreateWindow(view: WebView, isDialog: Boolean,
isUserGesture: Boolean, resultMsg: Message): Boolean {
// 웹뷰 만들기
var childWebView = WebView(view.context)
Log.d("TAG", "웹뷰 만들기")
// 부모 웹뷰와 동일하게 웹뷰 설정
childWebView.run {
settings.run {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportMultipleWindows(true)
}
layoutParams = view.layoutParams
webViewClient = view.webViewClient
webChromeClient = view.webChromeClient
}
dialog = Dialog(this@MainActivity).apply {
setContentView(childWebView)
window!!.attributes.width = ViewGroup.LayoutParams.MATCH_PARENT
window!!.attributes.height = ViewGroup.LayoutParams.MATCH_PARENT
show()
}
// TODO: 화면 추가 이외에 onBackPressed() 와 같이
// 사용자의 내비게이션 액션 처리를 위해
// 별도 웹뷰 관리를 권장함
// ex) childWebViewList.add(childWebView)
// 웹뷰 간 연동
val transport = resultMsg.obj as WebView.WebViewTransport
transport.webView = childWebView
resultMsg.sendToTarget()
return true
}
override fun onCloseWindow(window: WebView) {
super.onCloseWindow(window)
Log.d("로그 ", "onCloseWindow")
dialog.dismiss()
// 화면에서 제거하기
// TODO: 화면 제거 이외에 onBackPressed() 와 같이
// 사용자의 내비게이션 액션 처리를 위해
// 별도 웹뷰 array 관리를 권장함
// ex) childWebViewList.remove(childWebView)
}
}
1. onCreateWindow
public boolean onCreateWindow (WebView view,
boolean isDialog,
boolean isUserGesture,
Message resultMsg)
호스트 응용 프로그램에 새 창을 만들도록 요청합니다. 호스트 응용 프로그램이 이 요청을 수락하도록 선택하면 이 메서드에서 true를 반환하고 창을 호스팅할 새 WebView를 만들고 이를 View 시스템에 삽입하고 새 WebView를 인수로 사용하여 제공된 resultMsg 메시지를 대상에 보내야 합니다.
호스트 응용 프로그램이 요청을 수락하지 않기로 선택하면 이 메서드에서 false를 반환해야 합니다. 이 메서드의 기본 구현은 아무 것도 하지 않으므로 false를 반환합니다.
resultMsg
Message : 새로운 WebView가 생성되면 보낼 메시지입니다. resultMsg.obj는 WebView.WebViewTransport 개체입니다. WebView.WebViewTransport.setWebView(WebView)를 호출하여 새 WebView를 전송하는 데 사용해야 합니다.
public Object obj
받는 사람에게 보낼 임의의 개체입니다. Messenger를 사용하여 프로세스 간에 메시지를 보낼 때 이것은 프레임워크 클래스의 Parcelable을 포함하는 경우에만 null이 아닐 수 있습니다(응용 프로그램에 의해 구현된 것이 아님). 다른 데이터 전송의 경우 setData(Bundle)를 사용하십시오.
여기에서 Parcelable 개체는 Build.VERSION_CODES.FROYO 릴리스 이전에는 지원되지 않습니다.
public void sendToTarget ()
이 메시지를 getTarget()에 의해 지정된 핸들러에 보냅니다. 이 필드가 설정되지 않은 경우 null 포인터 예외를 throw합니다.
https://developers.kakao.com/docs/latest/ko/local/dev-guide#address-coord
https://postcode.map.daum.net/guide
https://developers.kakao.com/docs/latest/ko/getting-started/sdk-js
https://developer.android.com/guide/webapps/webview?hl=ko
https://developer.android.com/reference/android/webkit/WebChromeClient
https://developer.android.com/reference/android/os/Message#getTarget()
https://stuff.mit.edu/afs/sipb/project/android/docs/reference/android/webkit/WebChromeClient.html
http://www.dre.vanderbilt.edu/~schmidt/android/android-4.0/out/target/common/docs/doc-comment-check/reference/android/webkit/WebChromeClient.html