2025. 9. 4. 16:35ㆍVue
1. 개요
- 네이버 지도를 활용한 맵 팝업을 구성
- 네이버 정책상 네이버 지도 url에 위도, 경도 값을 넣어도 출발,도착 경로 설정이 불가
- 개선방향으로 데스크톱 웹에서는 url에 장소명을 넣어 장소를 표출하고 출발, 도착 지점을 사용자가 지정할 수 있도록 구성
모바일에서는 네이버 지도 어플이 있는 경우 어플로 이동하고, 없는 경우 웹페이지 자체에서 네이버 지도 영역으로 이동해
데스크톱 웹처럼 이용할 수 있도록 구성
2. 요구되어지는 사항
- 모바일에서 안드로이드 또는 IOS 기기의 네이버 지도 어플이 없는 경우 열린 웹페이지내에서 페이지 이동이 가능하도록
하는 fallback 기능이 필요
** 여기서 fallback_ 이란 개발에서 자주 쓰는 개념으로 쉽게 말하면 **주요 동작이 실패했을 때 대신 실행되는 차선책(백업 경로)”**을 뜻한다. 주요 기능 실패 시 대신 실행되는 예비 동작(대체 경로) 으로 이해하면 된다.
** 전체 완료 코드 _ 설명은 아래에 (필요한 코드 요소만 가져오고 삭제처리함_ 템플릿내 data 요소는 신경쓰지 말 것)
<template>
<div class="map-content">
<div class="content-space non-flex">
<div v-if="resultFlag" class="result-space">
<h1>총 <span>{{ searchResults.length > 0 ? searchResults.length : '' }}</span>건</h1>
<p class="mapResult-notice">* 지점에 따른 운영정보 *</p>
<div class="tableWrappers">
<table>
<tbody>
<tr v-for="(v, index) in searchResults" :key="`result-${index}`">
<!-- <td :class="{ 'is-selected': selectedName === index }" @click="getMarkerText(v, true)">{{ v.name }}</td> -->
<td :class="{ 'is-selected': selectedName === index }" @click="getMarkerText(v, true, index)">{{ v.name }}</td>
<td class="map-icon addressIcon">{{ v.jibunAddress }}
<a href="#" @click.prevent="openRoadLink(v.name)" class="roadLink">길찾기</a>
</td>
<td class="map-icon callIcon">{{ v.tell }}</td>
<td class="map-icon bookingIcon">
<a :href="getReservationLink(v.name)" target="blank">예약하기</a>
<a :href="getHomepage(v.name)" target="blank" class="homepageIcon">홈페이지</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-else class="result-space no-result">
<p>검색결과가 없습니다</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
roadLinks : [],
}
},
mounted(){
this.getMapStoreList();
},
methods: {
openRoadLink(name) {
const found = this.roadLinks.find(l => l.name === name);
if (!found) return;
const nmapUrl = found.mobileLink ? found.mobileLink : "#" // 모바일인 경우 ex) nmap://route/pubtrans?dlat=...&dlng=...&appname=mywebapp
const webUrl = found.webLink ? found.webLink : "#" // 웹인 경우
const isMobile = window.innerWidth < 1023;
const isAndroid = /Android/i.test(navigator.userAgent);
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
if (!isMobile) {
// PC는 웹으로
window.open(webUrl, '_blank');
return;
}
//안드로이드 경우
if (isAndroid) {
// Android는 intent://가 가장 안정적 (앱 없으면 자동으로 webUrl로 폴백)
// mobileLink에 들어간 쿼리(dlat, dlng, dname, appname)를 그대로 재사용.
const u = new URL(nmapUrl.replace('nmap://', 'https://')); // 파싱용 편법
const path = u.pathname.replace(/^\/+/, ''); // route/pubtrans 등
const qs = u.search.replace(/^\?/, ''); // dlat=..&dlng=..&...
const intentUrl =
`intent://${path}?${qs}` +
`#Intent;scheme=nmap;package=com.nhn.android.nmap;` +
`S.browser_fallback_url=${encodeURIComponent(webUrl)};end`;
// 새 창 대신 현재 창 이동이 더 안정적(권장)
window.location.href = intentUrl;
return;
}
//IOS 경우
if (isIOS) {
let navigated = false; // 앱 전환 감지 플래그
let t;
const cancelFallback = () => {
if (navigated) return;
navigated = true;
clearTimeout(t);
document.removeEventListener('visibilitychange', onVis);
window.removeEventListener('pagehide', onHide);
window.removeEventListener('blur', onBlur);
// 숨김 iframe 제거
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
};
//document.hidden >> 현재 문서가 사용자에게 보이는지 안보이는지 확인 => false = 현재 페이지 보고있음(활성 탭, 전환 안됨) true = 페이지 숨겨짐(다른 앱, 탭으로 전환)
const onVis = () => { if (document.hidden) cancelFallback(); };
const onHide = () => cancelFallback();
const onBlur = () => cancelFallback();
//visibilitychange >> 브라우저가 페이지의 가시성 상태(보이는지/숨겨졌는지)가 바뀔 때 발생하는 이벤트
document.addEventListener('visibilitychange', onVis);
window.addEventListener('pagehide', onHide);
window.addEventListener('blur', onBlur);
//IOS safari 보완 1) 숨김 iframe으로 앱 실행 "시도"(앱 없으면 조용히 실패)
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = nmapUrl; // nmap://route/...
document.body.appendChild(iframe);
// 2) 폴백 타이머 (앱 없으면 웹으로 이동)
t = setTimeout(() => {
if (!navigated) {
cancelFallback();
window.location.href = webUrl; // 현재 창에서 웹 폴백
}
}, 1200); // 800~1500ms 권장
// 3) 아주 짧은 재확인(일부 iOS에서 visibilitychange 지연 보완)
setTimeout(() => {
if (document.hidden) cancelFallback();
}, 200);
return;
}
// 그 외 모바일 환경은 웹으로
window.location.href = webUrl;
},
getHomepage(name){
const found = this.roadLinks.find(linkObj => linkObj.name === name);
return found ? found.homepage : '#'; // 없으면 기본값
},
getReservationLink(name){
const found = this.roadLinks.find(linkObj => linkObj.name === name);
return found ? `https://www.로그인할페이지.co.kr/login?${found.loginStoreIndex}` : '#'; // 없으면 기본값
},
/* 지점 불러오기 */
getMapStoreList(){
const data = {};
const callback = {
success: (response) => {
this.roadLinks = response.data.map((item) => ({
name : item.name,
webLink: `https://map.naver.com/v5/search/${item.name}`,
homepage : item.homepageUrl ? item.homepageUrl : '#',
loginStoreIndex : item.storeIdx? item.storeIdx : '',
mobileLink : `nmap://route/pubtrans?dlat=${item.latitude}&dlng=${item.longitude}&appname=mywebapp`
}
))
},
error: (error) => {
console.log('지점목록 불러오기 실패:', error);
}
}
this.$api.chat.getMapStoreList(data, callback);
}
}
}
</script>
3. 이슈사항
IOS safari 의 경우, 어플이 없으면 없다는 알림창이 뜨는데 이 알림창 이후로 자동 폴백이 진행되지 않았다.
=> 이유가 궁금해서 gpt에 물어보니 아래와 같은 답변이 나왔다.

결론 >> 사파리/OS 레벨의 기본동작이라 다른 처리가 필요하다는 것.
해결책 ** 숨김 iframe 방식
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = nmapUrl; // nmap://...
document.body.appendChild(iframe);
- 앱 설치 O → iOS가 네이버 지도 앱 실행 → document.hidden / pagehide 이벤트 발생 → 타이머 취소 → 웹 폴백 안 실행됨
- 앱 설치 X → iframe 로드 실패 → 팝업 없이 조용히 무시됨→ 일정 시간 타이머가 만료 → window.location.href = webUrl 실행 → 웹 네이버 지도 열림
✅ 정리
- 숨김 iframe을 쓰면 앱 미설치 시 오류 팝업이 전혀 뜨지 않는다.
- 앱이 없거나 앱 실행 실패 → 조용히 무시된 뒤, 우리가 넣은 타이머 폴백 로직으로 웹 네이버 지도가 열리게 된다.
- 앱이 있으면 → 정상적으로 앱으로 전환되고, 이벤트 감지로 웹 폴백은 실행되지 않는다.
'Vue' 카테고리의 다른 글
| [Vue] pinia 를 이용한 상태관리 (+compositionApi, optionsApi 차이) (0) | 2025.03.12 |
|---|---|
| [Vue] v-bind:class 객체로 사용하기 (0) | 2025.03.08 |
| [Vue] vue3 + vite 깃허브 페이지 배포하기 (0) | 2025.02.16 |
| [Vue] Tailwind.css(with Nuxt.js) 시작하기 (0) | 2025.02.13 |
| [Vue] Nuxt.js 시작하기 (0) | 2025.02.11 |