예약 포털 (Vue3 + Firebase) - 서비스 오픈까지
14. 동네 (예약) 포털 (Vue 3 + Firebase) - 네이버 지도에 업체 위치 보기
그랜파 개발자
2025. 6. 4. 12:50
업체 위치 보기
Home의 업체 리스트에서 업체 정보에 주소가 있습니다.
주소 옆에 '지도 보기' 버튼이 있고 이것을 누르면 네이버 지도로 위치를 보여 줍니다.
index.html
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 카카오 주소 검색 위젯 -->
<script src="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<!-- 네이버 지도 API -->
<script src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=s781heidm9&submodules=geocoder"></script>
<title>우리 동네 포털</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src/router/index.js
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Register from '../views/Register.vue'
import Profile from '../views/Profile.vue'
import Login from '../views/Login.vue'
const routes = [
{ path: '/', name: 'home', component: Home },
{ path: '/register', name: 'register', component: Register },
{ path: '/profile', name: 'profile', component: Profile },
{ path: '/login', component: Login },
{
path: '/register-company',
name: 'RegisterCompany',
component: () => import('@/views/RegisterCompany.vue'),
meta: { requiresAuth: true }
},
{
path: '/my-companies',
name: 'MyCompanies',
component: () => import('@/views/MyCompanies.vue'),
meta: { requiresAuth: true }
},
{
path: '/edit-company/:id',
name: 'EditCompany',
component: () => import('@/views/EditCompany.vue'),
meta: { requiresAuth: true }
},
{
path: '/company/:id',
component: () => import('@/views/CompanyDetail.vue')
},
{
path: '/reservation',
name: 'Reservation',
component: () => import('@/views/Reservation.vue')
},
{
path: '/my-reservations',
name: 'MyReservations',
component: () => import('@/views/MyReservations.vue'),
meta: { requiresAuth: true } // 로그인 필요하면
},
{
path: '/company-reservations/:companyId',
name: 'CompanyReservations',
component: () => import('@/views/CompanyReservations.vue')
},
{
path: '/map',
name: 'MapView',
component: () => import('@/views/MapView.vue'),
}
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
src/views/Home.vue - 동네 포털 등록 업체 리스트
<!-- src/views/Home.vue -->
<template>
<v-container>
<v-card-title>우리 동네 '예약 포털' 입니다.</v-card-title>
<div class="my-3">
<div class="ma-1 mb-1">업종 선택</div>
<v-chip-group
v-model="selectedCategory"
column
mandatory
>
<v-chip
v-for="item in ['전체', ...categories]"
:key="item"
:value="item"
class="ma-1"
color="primary"
variant="outlined"
filter
>
{{ item }}
</v-chip>
</v-chip-group>
</div>
<v-row>
<v-col
v-for="company in filteredCompanies"
:key="company.id"
cols="12"
md="6"
>
<v-list-item
class="border rounded pa-3 mb-0"
style="cursor: pointer"
@click="goToDetail(company.id)"
>
<div>
<v-icon color="primary" class="mr-2">mdi-storefront</v-icon>
<b>{{ company.name }}</b> ( {{ company.category }} )
</div>
<div class="mb-2">
{{ company.description || '소개글 없음' }}
</div>
<!-- ✅ 영업시간 및 상태 -->
<div v-if="company.openTime && company.closeTime" class="mb-2 text-grey">
영업시간: {{ company.openTime }} ~ {{ company.closeTime }}
<v-chip
:color="isOpenNow(company) ? 'green' : 'red'"
size="x-small"
class="ml-2"
>
{{ isOpenNow(company) ? '영업 중' : '영업 종료' }}
</v-chip>
</div>
<v-card-subtitle class="pa-0 pt-1">
<strong>주소:</strong> {{ company.address || '--' }}
<v-btn
size="x-small"
class="ml-2"
variant="text"
color="blue"
@click.stop="goToMap(company)"
>
지도 보기
</v-btn>
</v-card-subtitle>
<v-card-subtitle class="pa-0 pt-1">
<strong>상세주소:</strong> {{ company.detailAddress || '--' }}
</v-card-subtitle>
<!-- ✅ 로그인된 사용자에게만 표시 -->
<div v-if="authStore.user">
<v-btn
color="primary"
size="small"
@click.stop="goToReservation(company.id)"
>
예약하기
</v-btn>
</div>
</v-list-item>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useCompanyStore } from '@/stores/companyStore'
import { useAuthStore } from '@/stores/authStore' // ✅ 추가
const companyStore = useCompanyStore()
const authStore = useAuthStore() // ✅ 추가
const router = useRouter()
const selectedCategory = ref('전체')
const categories = ['배달음식', '카페', '소매업', '서비스업', '교육', '병원', '기타']
onMounted(() => {
companyStore.fetchAllCompanies()
})
const goToMap = (company) => {
router.push({
path: '/map',
query: {
address: company.address,
name: company.name,
},
})
}
const goToDetail = (id) => {
router.push(`/company/${id}`)
}
const goToReservation = (companyId) => {
//console.log(companyId, authStore.profile.name);
router.push({
path: '/reservation',
query: {
companyId,
username: authStore.profile.name
}
})
}
// 카테고리 필터링된 목록
const filteredCompanies = computed(() => {
if (selectedCategory.value === '전체' || !selectedCategory.value) {
return companyStore.companies
}
return companyStore.companies.filter(
(c) => c.category === selectedCategory.value
)
})
function isOpenNow(company) {
if (!company.openTime || !company.closeTime) return false;
const now = new Date();
const currentTime = now.getHours() * 60 + now.getMinutes();
const [openHour, openMinute] = company.openTime.split(':').map(Number);
const [closeHour, closeMinute] = company.closeTime.split(':').map(Number);
const openTime = openHour * 60 + openMinute;
const closeTime = closeHour * 60 + closeMinute;
return currentTime >= openTime && currentTime < closeTime;
}
</script>
src/views/MapView.vue - 네이버 지도로 업체 위치 보기
<!-- src/views/MapView.vue -->
<template>
<v-container>
<h3>{{ name }} 위치</h3>
<div ref="mapElement" style="width: 100%; height: 500px;"></div>
</v-container>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const mapElement = ref(null)
const name = decodeURIComponent(route.query.name || '')
const address = decodeURIComponent(route.query.address || '')
// 주소 → 좌표 변환 함수
const getCoordinates = (address) =>
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
})
})
})
// 지도 초기화
const initMap = async () => {
await nextTick()
if (!mapElement.value || !window.naver) {
console.error('naver 객체 또는 mapElement가 존재하지 않음')
return
}
try {
const { latitude, longitude, roadAddress, jibunAddress } = await getCoordinates(address)
const map = new naver.maps.Map(mapElement.value, {
center: new naver.maps.LatLng(latitude, longitude),
zoom: 15
})
const marker = new naver.maps.Marker({
position: new naver.maps.LatLng(latitude, longitude),
map,
title: name
})
const infoWindow = new naver.maps.InfoWindow({
content: `
<div style="padding:10px;min-width:200px;line-height:150%;">
<strong>${name}</strong><br/>
[도로명 주소] ${roadAddress || '없음'}<br/>
[지번 주소] ${jibunAddress || '없음'}
</div>
`
})
infoWindow.open(map, marker)
} catch (error) {
alert(error.message)
}
}
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>