예약 포털 (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>