15. 동네 (예약) 포털 (Vue 3 + Firebase) - 네이버 지도 상점 위치 보기 개선
상점 위치 보기 개선
상점의 주소로 네이버 지도 위에 표시를 하였을 때 실제 위치와 차이가 납니다.
검색 API는 네이버 검색 결과를 뉴스, 백과사전, 블로그, 쇼핑, 웹 문서, 전문정보, 지식iN, 책, 카페글 등 분야별로 볼 수 있는 API입니다. 그 외에 지역 검색 결과와 성인 검색어 판별 기능, 오타 변환 기능을 제공합니다.
지역 검색은 검색 API를 사용해 네이버 지역 서비스에 등록된 업체 및 기관을 검색한 결과를 반환하는 RESTful API입니다. 지역 검색 결과를 XML 형식 또는 JSON 형식으로 반환합니다. API를 호출할 때는 검색어와 검색 조건을 쿼리 스트링(Query String) 형식의 데이터로 전달합니다.
https://developers.naver.com/docs/serviceapi/search/local/local.md
상호로 위치를 검색하는 기능을 추가하였습니다.
상호로 위치 검색은 사용자가 입력한 키워드에 대한 검색으로
여러 개의 검색결과를 돌려 줍니다.
네이버 상호 검색은 내부적으로 POI 정보를 반환합니다.
📌 POI (Point of Interest): 위치 기반 데이터에서 관심 지점(상점, 식당, 병원, 공원 등)을 의미합니다.
https://github.com/inetsos/downtown
GitHub - inetsos/downtown: 동네 포털 - Vue 3 + Firebase
동네 포털 - Vue 3 + Firebase. Contribute to inetsos/downtown development by creating an account on GitHub.
github.com
주소로 검색
상호로 검색한 결과
검색 결과에서 선택 후
네이버 검색 API를 사용하는 방법은?
ChatGPT의 말:
네이버 검색 API는 네이버 개발자 센터에서 제공하는 RESTful API입니다. 로컬, 이미지, 뉴스, 블로그 등 다양한 검색 서비스를 제공합니다. 여기선 **로컬 검색 API(Local Search API)**를 기준으로 설명드릴게요.
✅ 1. 네이버 검색 API 사용을 위한 준비
① 네이버 개발자 센터 접속
👉 https://developers.naver.com/
② 애플리케이션 등록
- "내 애플리케이션" → 애플리케이션 등록
- 사용 API 선택: 검색 > 로컬 검색
- 환경 설정: 웹 서버 등
- Client ID와 Client Secret 발급받기
✅ 2. API 요청 예시 (로컬 검색)
📌 엔드포인트
GET https://openapi.naver.com/v1/search/local.json
📌 필수 요청 헤더
X-Naver-Client-Id: 발급받은 Client ID
X-Naver-Client-Secret: 발급받은 Client Secret
📌 쿼리 파라미터 예시
파라미터 | 설명 |
---|---|
query | 검색어 (UTF-8 인코딩 필요) |
display | 한 번에 가져올 검색 결과 수 (기본 10, 최대 30) |
start | 검색 시작 위치 (기본 1, 최대 1000) |
sort | 정렬 방식 (random |
✅ 3. JavaScript (fetch) 사용 예시
const query = '강남 스타벅스'
const encodedQuery = encodeURIComponent(query)
fetch(`https://openapi.naver.com/v1/search/local.json?query=${encodedQuery}&display=5`, {
headers: {
'X-Naver-Client-Id': 'YOUR_CLIENT_ID',
'X-Naver-Client-Secret': 'YOUR_CLIENT_SECRET'
}
})
.then(res => res.json())
.then(data => {
console.log(data.items) // 검색 결과 배열
})
.catch(err => console.error(err))
✅ 4. 응답 구조 예시
{
"lastBuildDate": "Mon, 05 Jun 2025 10:00:00 +0900",
"total": 3,
"start": 1,
"display": 3,
"items": [
{
"title": "<b>스타벅스</b> 강남점",
"link": "http://map.naver.com/local/siteview.nhn?code=123456",
"category": "카페",
"description": "",
"telephone": "02-123-4567",
"address": "서울 강남구 역삼동 123-45",
"roadAddress": "서울 강남구 테헤란로 123",
"mapx": "1271234567",
"mapy": "371234567"
}
]
}
src/views/MapView.vue - 개선된 네이버 지도에서의 상점 위치 보기
<!-- src/views/MapView.vue -->
<template>
<v-container>
<h3>{{ name }} 위치</h3>
<v-btn color="primary" class="mt-2 mb-2" @click="search">
상호로 위치 검색
</v-btn> 주소로 상점 위치를 찾는 경우 실제 위치와 차이가 나는 경우가 있습니다.
<div ref="mapElement" style="width: 100%; height: 400px;"></div>
<v-icon
@click="goBack"
style="cursor: pointer;"
aria-label="뒤로가기"
>
mdi-arrow-left
</v-icon>
<ul v-if="results.length" class="mt-4">
<li
v-for="(item, index) in results"
:key="index"
class="border-b py-2 cursor-pointer"
@click="moveToLocation(item)"
>
<strong v-html="item.title"></strong><br />
주소: {{ item.roadAddress || item.address }}<br />
전화번호: {{ item.telephone || '없음' }}
</li>
</ul>
</v-container>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const mapElement = ref(null)
const name = decodeURIComponent(route.query.name || '')
const address = decodeURIComponent(route.query.address || '')
const dong = ref('')
const results = ref([])
let map = null
let marker = null
let infoWindow = null
const search = async () => {
const encoded = encodeURIComponent(dong.value + ' ' + name)
const res = await fetch(`/naver-api/search/local.json?query=${encoded}&display=15`, {
headers: {
'X-Naver-Client-Id': import.meta.env.VITE_NAVER_CLIENT_ID,
'X-Naver-Client-Secret': import.meta.env.VITE_NAVER_CLIENT_SECRET,
}
})
const data = await res.json()
results.value = data.items || []
}
// 주소 → 좌표 변환 함수
const getCoordinates = (address) => {
return new Promise((resolve, reject) => {
naver.maps.Service.geocode({ query: address }, (status, response) => {
if (status !== naver.maps.Service.Status.OK || !response.v2?.addresses?.length) {
return reject(new Error('주소를 찾을 수 없거나 오류가 발생했습니다.'))
}
const item = response.v2.addresses[0]
resolve({
latitude: parseFloat(item.y),
longitude: parseFloat(item.x),
roadAddress: item.roadAddress,
jibunAddress: item.jibunAddress,
dongName:item.addressElements[2].longName
})
})
})
}
// 지도 초기화
const initMap = async () => {
await nextTick()
if (!mapElement.value || !window.naver) {
console.error('naver 객체 또는 mapElement가 존재하지 않음')
return
}
try {
const { latitude, longitude, roadAddress, jibunAddress, dongName } = await getCoordinates(address)
dong.value = dongName;
map = new naver.maps.Map(mapElement.value, {
center: new naver.maps.LatLng(latitude, longitude),
zoom: 15
})
marker = new naver.maps.Marker({
position: new naver.maps.LatLng(latitude, longitude),
map,
title: name
})
infoWindow = new naver.maps.InfoWindow({
content: `
<div id="infoWindowContent" style="padding:10px;min-width:200px;line-height:150%;cursor:pointer;">
<strong>${name}</strong><br/>
[도로명 주소] ${roadAddress || '없음'}<br/>
[지번 주소] ${jibunAddress || '없음'}<br/>
<small>(클릭하면 닫힘)</small>
</div>
`
})
infoWindow.open(map, marker)
// infoWindow 내용 클릭 시 닫기
setTimeout(() => {
const el = document.getElementById('infoWindowContent')
if (el) {
el.addEventListener('click', () => {
infoWindow.close()
})
}
}, 0)
} catch (error) {
alert(error.message)
}
}
// 검색 결과 클릭 시 지도 이동 및 마커 표시
const moveToLocation = (item) => {
if (!map) return
const lat = parseFloat(item.mapy/10000000)
const lng = parseFloat(item.mapx/10000000)
const position = new naver.maps.LatLng(lat, lng)
map.setCenter(position)
map.setZoom(15)
if (marker) {
marker.setPosition(position)
marker.setTitle(item.title)
} else {
marker = new naver.maps.Marker({
position,
map,
title: item.title
})
}
if (infoWindow) {
infoWindow.setContent(`
<div id="infoWindowContent" style="padding:10px;min-width:200px;line-height:150%;cursor:pointer;">
<strong>${item.title}</strong><br/>
주소: ${item.roadAddress || item.address}<br/>
전화번호: ${item.telephone || '없음'}<br/>
<small>(클릭하면 닫힘)</small>
</div>
`)
infoWindow.open(map, marker)
// infoWindow 내용 클릭 시 닫기
setTimeout(() => {
const el = document.getElementById('infoWindowContent')
if (el) {
el.addEventListener('click', () => {
infoWindow.close()
})
}
}, 0)
}
}
function goBack() {
router.back()
}
onMounted(() => {
if (!window.naver || !window.naver.maps) {
const script = document.createElement('script')
script.src = 'https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=s781heidm9&submodules=geocoder'
script.async = true
script.onload = initMap
document.head.appendChild(script)
} else {
initMap()
}
})
</script>