|
|
@@ -1,928 +0,0 @@
|
|
|
-<template>
|
|
|
- <div>
|
|
|
- <breadCrumbs
|
|
|
- :main-menu="breadcrumbData.mainMenu"
|
|
|
- :current-sub-menu="breadcrumbData.currentSubMenu"
|
|
|
- :sub-menu-items="breadcrumbData.subMenuItems"
|
|
|
- />
|
|
|
- <div class="container">
|
|
|
- <div class="showroom--wrap" :class="{ type2: locationType === 2 }">
|
|
|
- <div class="search--wrap" :class="{ 'at-bottom': isScrolledToBottom }">
|
|
|
- <div class="form--wrap">
|
|
|
- <div class="input--wrap mb--12">
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- v-model="searchKeyword"
|
|
|
- :placeholder="searchPlaceholder"
|
|
|
- @keyup.enter="searchLocations"
|
|
|
- />
|
|
|
- <button class="search--btn" @click="searchLocations"></button>
|
|
|
- </div>
|
|
|
- <div class="input--wrap">
|
|
|
- <select
|
|
|
- name="location"
|
|
|
- id="location"
|
|
|
- v-model="selectedRegion"
|
|
|
- required
|
|
|
- @change="searchLocations"
|
|
|
- >
|
|
|
- <option value="" disabled selected hidden>시/도 선택</option>
|
|
|
- <option value="">전체</option>
|
|
|
- <option value="서울">서울</option>
|
|
|
- <option value="경기">경기</option>
|
|
|
- <option value="인천">인천</option>
|
|
|
- <option value="부산">부산</option>
|
|
|
- <option value="대구">대구</option>
|
|
|
- <option value="광주">광주</option>
|
|
|
- <option value="대전">대전</option>
|
|
|
- <option value="울산">울산</option>
|
|
|
- <option value="세종">세종</option>
|
|
|
- <option value="강원">강원</option>
|
|
|
- <option value="충북">충북</option>
|
|
|
- <option value="충남">충남</option>
|
|
|
- <option value="전북">전북</option>
|
|
|
- <option value="전남">전남</option>
|
|
|
- <option value="경북">경북</option>
|
|
|
- <option value="경남">경남</option>
|
|
|
- <option value="제주">제주</option>
|
|
|
- </select>
|
|
|
- <!-- <button class="location--btn" @click="findNearbyLocations">
|
|
|
- 내 주변<i class="ico"></i>
|
|
|
- </button> -->
|
|
|
- </div>
|
|
|
- <!-- <div class="btn--wrap">
|
|
|
- <button class="btn--sky" @click="searchLocations">
|
|
|
- {{ nearbyButtonText }}
|
|
|
- </button>
|
|
|
- </div> -->
|
|
|
- </div>
|
|
|
- <div class="list--wrap" ref="listWrapRef" @scroll="handleScroll">
|
|
|
- <div v-if="isLoading" class="loading">데이터를 불러오는 중...</div>
|
|
|
- <div
|
|
|
- v-else-if="!filteredLocations || filteredLocations.length === 0"
|
|
|
- class="empty"
|
|
|
- >
|
|
|
- {{ emptyMessage }}
|
|
|
- </div>
|
|
|
- <div
|
|
|
- v-else
|
|
|
- v-for="location in filteredLocations"
|
|
|
- :key="location.id"
|
|
|
- class="list"
|
|
|
- :class="{ active: selectedLocation?.id === location.id }"
|
|
|
- @click="selectLocation(location)"
|
|
|
- >
|
|
|
- <div class="list--title">
|
|
|
- <h3>{{ location.name }}</h3>
|
|
|
- <p>{{ location.branch_name || "" }}</p>
|
|
|
- <p v-if="location.distance" class="distance">
|
|
|
- {{ location.distance.toFixed(1) }}km
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <div class="list--info">
|
|
|
- <div class="address">
|
|
|
- <i class="ico"></i>
|
|
|
- <p>{{ location.address }}</p>
|
|
|
- </div>
|
|
|
- <div class="call">
|
|
|
- <i class="ico"></i>
|
|
|
- <p>{{ location.main_phone }}</p>
|
|
|
- </div>
|
|
|
- <div class="time">
|
|
|
- <i class="ico"></i>
|
|
|
- <p>{{ location.business_hours }}</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="list--btn" v-if="locationType === 1">
|
|
|
- <NuxtLink :to="location.quote_link" target="_blank" class="btn--gray"
|
|
|
- >견적 요청</NuxtLink
|
|
|
- >
|
|
|
- <NuxtLink
|
|
|
- :to="location.test_drive_link"
|
|
|
- target="_blank"
|
|
|
- class="btn--black"
|
|
|
- >시승 요청</NuxtLink
|
|
|
- >
|
|
|
- </div>
|
|
|
- <div class="list--btn" v-else>
|
|
|
- <NuxtLink
|
|
|
- :to="location.service_reservation_link"
|
|
|
- target="_blank"
|
|
|
- class="btn--black"
|
|
|
- >서비스 예약</NuxtLink
|
|
|
- >
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="map--wrap">
|
|
|
- <div class="title--wrap">
|
|
|
- <h2>{{ pageTitle }}</h2>
|
|
|
- <p>{{ pageDescription }}</p>
|
|
|
- </div>
|
|
|
- <!-- 지도 영역 -->
|
|
|
- <div id="map" class="map"></div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
- import { ref, computed, onMounted, nextTick, watch } from "vue";
|
|
|
-
|
|
|
- const route = useRoute();
|
|
|
- const { get } = useApi();
|
|
|
-
|
|
|
- // type 파라미터: 1=전시장, 2=서비스센터 (반응형으로 route.query 직접 참조)
|
|
|
- const locationType = computed(() => {
|
|
|
- const type = parseInt(route.query.type) || 1;
|
|
|
- return type === 2 ? 2 : 1;
|
|
|
- });
|
|
|
-
|
|
|
- // breadcrumb도 반응형으로
|
|
|
- const breadcrumbData = computed(() => ({
|
|
|
- mainMenu: "NETWORK",
|
|
|
- currentSubMenu: locationType.value === 2 ? "서비스센터 찾기" : "전시장 찾기",
|
|
|
- subMenuItems: [
|
|
|
- {
|
|
|
- label: "전시장 찾기",
|
|
|
- to: "/ford/network",
|
|
|
- active: locationType.value === 2 ? false : true,
|
|
|
- },
|
|
|
- {
|
|
|
- label: "서비스센터 찾기",
|
|
|
- to: "/ford/network",
|
|
|
- active: locationType.value === 2 ? true : false,
|
|
|
- },
|
|
|
- {
|
|
|
- label: "포드고객센터",
|
|
|
- to: "/ford/owner/contact",
|
|
|
- active: false,
|
|
|
- },
|
|
|
- ],
|
|
|
- }));
|
|
|
-
|
|
|
- // 타입별 텍스트 설정
|
|
|
- const pageTitle = computed(() =>
|
|
|
- locationType.value === 1 ? "전시장 찾기" : "서비스센터 찾기"
|
|
|
- );
|
|
|
- const pageDescription = computed(() =>
|
|
|
- locationType.value === 1
|
|
|
- ? "전국의 포드 전시장을 한 눈에 찾아보실 수 있습니다."
|
|
|
- : "전국의 포드 서비스센터를 한 눈에 찾아보실 수 있습니다."
|
|
|
- );
|
|
|
- const searchPlaceholder = computed(() =>
|
|
|
- locationType.value === 1
|
|
|
- ? "전시장명 또는 지역명을 입력해주세요"
|
|
|
- : "서비스센터명 또는 지역명을 입력해주세요"
|
|
|
- );
|
|
|
- const emptyMessage = computed(() =>
|
|
|
- locationType.value === 1
|
|
|
- ? "등록된 전시장이 없습니다."
|
|
|
- : "등록된 서비스센터가 없습니다."
|
|
|
- );
|
|
|
- const nearbyButtonText = computed(() =>
|
|
|
- locationType.value === 1 ? "가까운 전시장 추천 지정" : "가까운 서비스센터 추천 지정"
|
|
|
- );
|
|
|
- const locationLabel = computed(() =>
|
|
|
- locationType.value === 1 ? "전시장" : "서비스센터"
|
|
|
- );
|
|
|
-
|
|
|
- // API endpoint (public API - 인증 불필요)
|
|
|
- const apiEndpoint = computed(() =>
|
|
|
- locationType.value === 1 ? "/showroom/public" : "/service-center/public"
|
|
|
- );
|
|
|
-
|
|
|
- const isLoading = ref(false);
|
|
|
- const locations = ref([]);
|
|
|
- const searchKeyword = ref("");
|
|
|
- const selectedRegion = ref("");
|
|
|
- const selectedLocation = ref(null);
|
|
|
- const map = ref(null);
|
|
|
- const markers = ref([]);
|
|
|
- const overlays = ref([]); // 카카오맵 CustomOverlay 저장
|
|
|
- const clusterer = ref([]); // 커스텀 클러스터 오버레이 배열
|
|
|
- const userLocation = ref(null);
|
|
|
- const userLocationMarker = ref(null); // 현재 위치 마커
|
|
|
- const userLocationCircle = ref(null); // 반경 원
|
|
|
- const nearbyRadius = ref(5); // 주변 검색 반경 (km) - 유동적으로 변경 가능
|
|
|
- const listWrapRef = ref(null);
|
|
|
- const isScrolledToBottom = ref(false);
|
|
|
-
|
|
|
- // 카카오맵 API Key
|
|
|
- const KAKAO_API_KEY = "5e42d358f25c02d08a2596876459f6ca";
|
|
|
- //const KAKAO_API_KEY = "47dfc47d66f488da19bdebeabbde1da7";
|
|
|
- // 두 지점 간의 거리 계산 (Haversine formula)
|
|
|
- const calculateDistance = (lat1, lon1, lat2, lon2) => {
|
|
|
- const R = 6371; // 지구 반경 (km)
|
|
|
- const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
|
|
- const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
|
|
- const a =
|
|
|
- Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
|
- Math.cos((lat1 * Math.PI) / 180) *
|
|
|
- Math.cos((lat2 * Math.PI) / 180) *
|
|
|
- Math.sin(dLon / 2) *
|
|
|
- Math.sin(dLon / 2);
|
|
|
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
- return R * c;
|
|
|
- };
|
|
|
-
|
|
|
- // 카카오맵 스크립트 로드
|
|
|
- const loadKakaoMapsScript = () => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- if (window.kakao && window.kakao.maps) {
|
|
|
- console.log("[KakaoMap] Already loaded");
|
|
|
- resolve();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const script = document.createElement("script");
|
|
|
- script.src = `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${KAKAO_API_KEY}&autoload=false`;
|
|
|
- script.async = true;
|
|
|
- script.onload = () => {
|
|
|
- console.log("[KakaoMap] Script loaded, initializing...");
|
|
|
- window.kakao.maps.load(() => {
|
|
|
- console.log("[KakaoMap] Maps API ready");
|
|
|
- resolve();
|
|
|
- });
|
|
|
- };
|
|
|
- script.onerror = (e) => {
|
|
|
- console.error("[KakaoMap] Script load error:", e);
|
|
|
- reject(new Error("카카오맵 API 로드 실패"));
|
|
|
- };
|
|
|
- document.head.appendChild(script);
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- // 지도 초기화
|
|
|
- const initMap = () => {
|
|
|
- console.log("[KakaoMap] initMap called");
|
|
|
- if (typeof window === "undefined" || !window.kakao || !window.kakao.maps) {
|
|
|
- console.error("[KakaoMap] Kakao maps not available");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const container = document.getElementById("map");
|
|
|
- if (!container) {
|
|
|
- console.error("[KakaoMap] Map container not found");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- console.log("[KakaoMap] Creating map...", container);
|
|
|
-
|
|
|
- // 서울 중심으로 지도 생성
|
|
|
- const options = {
|
|
|
- center: new window.kakao.maps.LatLng(37.5665, 126.978),
|
|
|
- level: 8, // 카카오맵 줌 레벨 (숫자가 작을수록 확대)
|
|
|
- };
|
|
|
-
|
|
|
- map.value = new window.kakao.maps.Map(container, options);
|
|
|
- console.log("[KakaoMap] Map created successfully");
|
|
|
- };
|
|
|
-
|
|
|
- // 모든 마커 및 오버레이 제거
|
|
|
- const clearAllMarkers = () => {
|
|
|
- // 오버레이 먼저 제거
|
|
|
- overlays.value.forEach((overlay) => overlay.setMap(null));
|
|
|
- overlays.value = [];
|
|
|
-
|
|
|
- // 클러스터 오버레이 제거
|
|
|
- if (clusterer.value && clusterer.value.length) {
|
|
|
- clusterer.value.forEach((c) => c.setMap(null));
|
|
|
- }
|
|
|
- clusterer.value = [];
|
|
|
-
|
|
|
- // 마커 제거
|
|
|
- markers.value.forEach((marker) => marker.setMap(null));
|
|
|
- markers.value = [];
|
|
|
-
|
|
|
- // 현재 위치 마커 제거
|
|
|
- if (userLocationMarker.value) {
|
|
|
- userLocationMarker.value.setMap(null);
|
|
|
- userLocationMarker.value = null;
|
|
|
- }
|
|
|
-
|
|
|
- // 반경 원 제거
|
|
|
- if (userLocationCircle.value) {
|
|
|
- userLocationCircle.value.setMap(null);
|
|
|
- userLocationCircle.value = null;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 툴팁 콘텐츠 생성
|
|
|
- const createInfoWindowContent = (location) => {
|
|
|
- // 버튼 영역 생성
|
|
|
- let buttonsHtml = "";
|
|
|
- if (locationType.value === 1) {
|
|
|
- // 전시장: 견적요청, 시승요청
|
|
|
- buttonsHtml = `
|
|
|
- <div style="display: flex; gap: 8px; margin-top: 12px;">
|
|
|
- ${
|
|
|
- location.quote_link
|
|
|
- ? `<a href="${location.quote_link}" target="_blank" style="flex: 1; padding: 8px 12px; background: #6b7280; color: white; text-decoration: none; text-align: center; border-radius: 4px; font-size: 12px;">견적 요청</a>`
|
|
|
- : ""
|
|
|
- }
|
|
|
- ${
|
|
|
- location.test_drive_link
|
|
|
- ? `<a href="${location.test_drive_link}" target="_blank" style="flex: 1; padding: 8px 12px; background: #1a1a1a; color: white; text-decoration: none; text-align: center; border-radius: 4px; font-size: 12px;">시승 요청</a>`
|
|
|
- : ""
|
|
|
- }
|
|
|
- </div>
|
|
|
- `;
|
|
|
- } else {
|
|
|
- // 서비스센터: 서비스예약
|
|
|
- buttonsHtml = location.service_reservation_link
|
|
|
- ? `
|
|
|
- <div style="margin-top: 12px;">
|
|
|
- <a href="${location.service_reservation_link}" target="_blank" style="display: block; padding: 8px 12px; background: #1a1a1a; color: white; text-decoration: none; text-align: center; border-radius: 4px; font-size: 12px;">서비스 예약</a>
|
|
|
- </div>
|
|
|
- `
|
|
|
- : "";
|
|
|
- }
|
|
|
-
|
|
|
- // 전시장일 때만 로고 표시 (닫기 버튼 포함)
|
|
|
- const logoHtml =
|
|
|
- locationType.value === 1
|
|
|
- ? `
|
|
|
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
|
|
|
- <img src="/img/logo--gate1.svg" alt="Ford" style="height: 24px; width: auto;" />
|
|
|
- <button class="close-btn" style="background: none; border: none; cursor: pointer; font-size: 18px; color: #999; padding: 0; line-height: 1;">×</button>
|
|
|
- </div>
|
|
|
- `
|
|
|
- : `
|
|
|
- <div style="display: flex; justify-content: flex-end; margin-bottom: 8px;">
|
|
|
- <button class="close-btn" style="background: none; border: none; cursor: pointer; font-size: 18px; color: #999; padding: 0; line-height: 1;">×</button>
|
|
|
- </div>
|
|
|
- `;
|
|
|
-
|
|
|
- return `
|
|
|
- <div class="kakao-infowindow" style="padding: 12px; min-width: 220px; max-width: 280px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
|
|
- ${logoHtml}
|
|
|
- <div>
|
|
|
- <h3 style="margin: 0 0 4px 0; font-size: 15px; font-weight: bold; color: #1a1a1a;">${
|
|
|
- location.name
|
|
|
- }</h3>
|
|
|
- <p style="margin: 0 0 10px 0; font-size: 12px; color: #666;">${
|
|
|
- location.branch_name || ""
|
|
|
- }</p>
|
|
|
- </div>
|
|
|
- <div style="font-size: 13px; color: #333; line-height: 1.6;">
|
|
|
- <div style="display: flex; align-items: flex-start; margin-bottom: 6px;">
|
|
|
- <span style="margin-right: 8px;">📍</span>
|
|
|
- <span>${location.address}</span>
|
|
|
- </div>
|
|
|
- <div style="display: flex; align-items: center; margin-bottom: 6px;">
|
|
|
- <span style="margin-right: 8px;">📞</span>
|
|
|
- <a href="tel:${
|
|
|
- location.main_phone
|
|
|
- }" style="color: #333; text-decoration: none;">${location.main_phone}</a>
|
|
|
- </div>
|
|
|
- ${
|
|
|
- location.business_hours
|
|
|
- ? `
|
|
|
- <div style="display: flex; align-items: flex-start;">
|
|
|
- <span style="margin-right: 8px;">🕐</span>
|
|
|
- <span style="white-space: pre-line;">${location.business_hours}</span>
|
|
|
- </div>
|
|
|
- `
|
|
|
- : ""
|
|
|
- }
|
|
|
- </div>
|
|
|
- ${buttonsHtml}
|
|
|
- </div>
|
|
|
- <div style="position: absolute; bottom: -10px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 10px solid white;"></div>
|
|
|
- `;
|
|
|
- };
|
|
|
-
|
|
|
- // 픽셀 거리 계산 (두 지점 간)
|
|
|
- const getPixelDistance = (pos1, pos2) => {
|
|
|
- const proj = map.value.getProjection();
|
|
|
- const point1 = proj.pointFromCoords(pos1);
|
|
|
- const point2 = proj.pointFromCoords(pos2);
|
|
|
- return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
|
|
|
- };
|
|
|
-
|
|
|
- // 클러스터 오버레이 생성
|
|
|
- const createClusterOverlay = (positions, count) => {
|
|
|
- // 클러스터 중심점 계산
|
|
|
- let sumLat = 0,
|
|
|
- sumLng = 0;
|
|
|
- positions.forEach((pos) => {
|
|
|
- sumLat += pos.getLat();
|
|
|
- sumLng += pos.getLng();
|
|
|
- });
|
|
|
- const centerLat = sumLat / positions.length;
|
|
|
- const centerLng = sumLng / positions.length;
|
|
|
- const centerPosition = new window.kakao.maps.LatLng(centerLat, centerLng);
|
|
|
-
|
|
|
- // 클러스터 표시 콘텐츠
|
|
|
- const content = document.createElement("div");
|
|
|
- content.innerHTML = `
|
|
|
- <div style="
|
|
|
- width: 40px;
|
|
|
- height: 40px;
|
|
|
- background: #00095B;
|
|
|
- border: 3px solid white;
|
|
|
- border-radius: 50%;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- color: white;
|
|
|
- font-weight: bold;
|
|
|
- font-size: 14px;
|
|
|
- box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
|
- cursor: pointer;
|
|
|
- ">${count}</div>
|
|
|
- `;
|
|
|
-
|
|
|
- // 클러스터 클릭 시 확대
|
|
|
- content.addEventListener("click", () => {
|
|
|
- map.value.setCenter(centerPosition);
|
|
|
- const currentLevel = map.value.getLevel();
|
|
|
- map.value.setLevel(currentLevel - 2);
|
|
|
- });
|
|
|
-
|
|
|
- const overlay = new window.kakao.maps.CustomOverlay({
|
|
|
- content: content,
|
|
|
- position: centerPosition,
|
|
|
- xAnchor: 0.5,
|
|
|
- yAnchor: 0.5,
|
|
|
- zIndex: 2,
|
|
|
- });
|
|
|
-
|
|
|
- return overlay;
|
|
|
- };
|
|
|
-
|
|
|
- // 마커 클러스터링 업데이트
|
|
|
- const updateClustering = () => {
|
|
|
- if (!map.value || !window.kakao) return;
|
|
|
-
|
|
|
- const level = map.value.getLevel();
|
|
|
- const clusterThreshold = 60; // 클러스터링 픽셀 거리
|
|
|
-
|
|
|
- // 기존 클러스터 제거
|
|
|
- if (clusterer.value) {
|
|
|
- clusterer.value.forEach((c) => c.setMap(null));
|
|
|
- }
|
|
|
- clusterer.value = [];
|
|
|
-
|
|
|
- // 줌 레벨이 낮으면 (확대된 상태) 클러스터링 안함
|
|
|
- if (level <= 6) {
|
|
|
- markers.value.forEach((marker) => marker.setMap(map.value));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 마커 그룹핑
|
|
|
- const assigned = new Set();
|
|
|
- const clusters = [];
|
|
|
-
|
|
|
- markers.value.forEach((marker, i) => {
|
|
|
- if (assigned.has(i)) return;
|
|
|
-
|
|
|
- const group = [marker];
|
|
|
- const positions = [marker.getPosition()];
|
|
|
- assigned.add(i);
|
|
|
-
|
|
|
- markers.value.forEach((otherMarker, j) => {
|
|
|
- if (i === j || assigned.has(j)) return;
|
|
|
-
|
|
|
- const distance = getPixelDistance(
|
|
|
- marker.getPosition(),
|
|
|
- otherMarker.getPosition()
|
|
|
- );
|
|
|
- if (distance < clusterThreshold) {
|
|
|
- group.push(otherMarker);
|
|
|
- positions.push(otherMarker.getPosition());
|
|
|
- assigned.add(j);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- clusters.push({ markers: group, positions: positions });
|
|
|
- });
|
|
|
-
|
|
|
- // 클러스터 또는 개별 마커 표시
|
|
|
- clusters.forEach((cluster) => {
|
|
|
- if (cluster.markers.length > 1) {
|
|
|
- // 클러스터로 표시
|
|
|
- cluster.markers.forEach((m) => m.setMap(null));
|
|
|
- const clusterOverlay = createClusterOverlay(
|
|
|
- cluster.positions,
|
|
|
- cluster.markers.length
|
|
|
- );
|
|
|
- clusterOverlay.setMap(map.value);
|
|
|
- clusterer.value.push(clusterOverlay);
|
|
|
- } else {
|
|
|
- // 개별 마커 표시
|
|
|
- cluster.markers[0].setMap(map.value);
|
|
|
- }
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- // 마커 생성
|
|
|
- const createMarkers = () => {
|
|
|
- if (!map.value || !window.kakao) return;
|
|
|
-
|
|
|
- // 기존 오버레이 제거
|
|
|
- overlays.value.forEach((overlay) => overlay.setMap(null));
|
|
|
- overlays.value = [];
|
|
|
-
|
|
|
- // 기존 클러스터 제거
|
|
|
- if (clusterer.value) {
|
|
|
- clusterer.value.forEach((c) => c.setMap(null));
|
|
|
- }
|
|
|
- clusterer.value = [];
|
|
|
-
|
|
|
- // 기존 마커 제거
|
|
|
- markers.value.forEach((marker) => marker.setMap(null));
|
|
|
- markers.value = [];
|
|
|
-
|
|
|
- // 마커 생성 (CustomOverlay로 로고 마커 적용)
|
|
|
- filteredLocations.value.forEach((location) => {
|
|
|
- if (location.latitude && location.longitude) {
|
|
|
- const position = new window.kakao.maps.LatLng(
|
|
|
- parseFloat(location.latitude),
|
|
|
- parseFloat(location.longitude)
|
|
|
- );
|
|
|
-
|
|
|
- // 커스텀 마커 HTML (타원형 안에 로고)
|
|
|
- const markerWrapper = document.createElement("div");
|
|
|
- markerWrapper.innerHTML = `
|
|
|
- <div class="ford-marker" style="
|
|
|
- width: 90px;
|
|
|
- height: 40px;
|
|
|
- // background: #00095B;
|
|
|
- // border: 2px solid white;
|
|
|
- border-radius: 20px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- cursor: pointer;
|
|
|
- box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
|
- ">
|
|
|
- <img src="/img/logo--gate1.svg" alt="Ford" style="width: 70px; height: auto; display: block;" />
|
|
|
- </div>
|
|
|
- `;
|
|
|
-
|
|
|
- // 정보창 오버레이 생성
|
|
|
- const overlayContent = document.createElement("div");
|
|
|
- overlayContent.innerHTML = createInfoWindowContent(location);
|
|
|
- overlayContent.style.position = "relative";
|
|
|
- overlayContent.style.bottom = "45px";
|
|
|
-
|
|
|
- const overlay = new window.kakao.maps.CustomOverlay({
|
|
|
- content: overlayContent,
|
|
|
- position: position,
|
|
|
- xAnchor: 0.5,
|
|
|
- yAnchor: 1,
|
|
|
- zIndex: 3,
|
|
|
- });
|
|
|
-
|
|
|
- // 닫기 버튼 이벤트
|
|
|
- const closeBtn = overlayContent.querySelector(".close-btn");
|
|
|
- if (closeBtn) {
|
|
|
- closeBtn.addEventListener("click", (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- overlay.setMap(null);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 마커 클릭 이벤트 핸들러
|
|
|
- markerWrapper.onclick = () => {
|
|
|
- // 다른 오버레이 닫기
|
|
|
- overlays.value.forEach((ov) => ov.setMap(null));
|
|
|
- // 현재 오버레이 열기
|
|
|
- overlay.setMap(map.value);
|
|
|
- selectLocation(location);
|
|
|
- };
|
|
|
-
|
|
|
- // 마커 생성 (CustomOverlay)
|
|
|
- const marker = new window.kakao.maps.CustomOverlay({
|
|
|
- position: position,
|
|
|
- content: markerWrapper,
|
|
|
- yAnchor: 0.5,
|
|
|
- xAnchor: 0.5,
|
|
|
- });
|
|
|
-
|
|
|
- marker.overlay = overlay;
|
|
|
- markers.value.push(marker);
|
|
|
- overlays.value.push(overlay);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 클러스터링 적용
|
|
|
- updateClustering();
|
|
|
-
|
|
|
- // 줌 변경 시 클러스터링 업데이트 (기존 리스너 제거 후 추가)
|
|
|
- window.kakao.maps.event.removeListener(map.value, "zoom_changed", updateClustering);
|
|
|
- window.kakao.maps.event.addListener(map.value, "zoom_changed", updateClustering);
|
|
|
- };
|
|
|
-
|
|
|
- // 목록 불러오기
|
|
|
- const loadLocations = async () => {
|
|
|
- isLoading.value = true;
|
|
|
-
|
|
|
- // 기존 마커 모두 제거
|
|
|
- clearAllMarkers();
|
|
|
-
|
|
|
- // 지도 새로 초기화 (서울 중심)
|
|
|
- if (window.kakao && window.kakao.maps) {
|
|
|
- const container = document.getElementById("map");
|
|
|
- if (container) {
|
|
|
- const options = {
|
|
|
- center: new window.kakao.maps.LatLng(37.5665, 126.978),
|
|
|
- level: 8,
|
|
|
- };
|
|
|
- map.value = new window.kakao.maps.Map(container, options);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const { data } = await get(apiEndpoint.value);
|
|
|
-
|
|
|
- if (data?.success && data?.data) {
|
|
|
- // public API는 배열을 직접 반환
|
|
|
- locations.value = Array.isArray(data.data) ? data.data : data.data.items || [];
|
|
|
-
|
|
|
- // 마커 생성
|
|
|
- await nextTick();
|
|
|
- createMarkers();
|
|
|
- }
|
|
|
-
|
|
|
- isLoading.value = false;
|
|
|
- };
|
|
|
-
|
|
|
- // 필터링된 목록
|
|
|
- const filteredLocations = computed(() => {
|
|
|
- let result = locations.value;
|
|
|
-
|
|
|
- // 내 주변 필터 (우선순위 최상위)
|
|
|
- if (userLocation.value) {
|
|
|
- result = result.filter((location) => {
|
|
|
- if (!location.latitude || !location.longitude) return false;
|
|
|
- const distance = calculateDistance(
|
|
|
- userLocation.value.lat,
|
|
|
- userLocation.value.lng,
|
|
|
- parseFloat(location.latitude),
|
|
|
- parseFloat(location.longitude)
|
|
|
- );
|
|
|
- return distance <= nearbyRadius.value;
|
|
|
- });
|
|
|
-
|
|
|
- // 거리순으로 정렬
|
|
|
- result = result
|
|
|
- .map((location) => {
|
|
|
- const distance = calculateDistance(
|
|
|
- userLocation.value.lat,
|
|
|
- userLocation.value.lng,
|
|
|
- parseFloat(location.latitude),
|
|
|
- parseFloat(location.longitude)
|
|
|
- );
|
|
|
- return { ...location, distance };
|
|
|
- })
|
|
|
- .sort((a, b) => a.distance - b.distance);
|
|
|
- }
|
|
|
-
|
|
|
- // 지역 필터
|
|
|
- if (selectedRegion.value) {
|
|
|
- result = result.filter((location) =>
|
|
|
- location.address?.includes(selectedRegion.value)
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 검색어 필터
|
|
|
- if (searchKeyword.value) {
|
|
|
- const keyword = searchKeyword.value.toLowerCase();
|
|
|
- result = result.filter(
|
|
|
- (location) =>
|
|
|
- location.name?.toLowerCase().includes(keyword) ||
|
|
|
- location.address?.toLowerCase().includes(keyword) ||
|
|
|
- location.branch_name?.toLowerCase().includes(keyword)
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- return result;
|
|
|
- });
|
|
|
-
|
|
|
- // 선택
|
|
|
- const selectLocation = (location) => {
|
|
|
- selectedLocation.value = location;
|
|
|
-
|
|
|
- if (map.value && location.latitude && location.longitude && window.kakao) {
|
|
|
- // 지도 중심 이동 및 줌인
|
|
|
- const position = new window.kakao.maps.LatLng(
|
|
|
- parseFloat(location.latitude),
|
|
|
- parseFloat(location.longitude)
|
|
|
- );
|
|
|
- map.value.setCenter(position);
|
|
|
- map.value.setLevel(3); // 확대
|
|
|
-
|
|
|
- // 해당 마커의 오버레이 열기
|
|
|
- const targetLat = parseFloat(location.latitude);
|
|
|
- const targetLng = parseFloat(location.longitude);
|
|
|
- const marker = markers.value.find((m) => {
|
|
|
- const pos = m.getPosition();
|
|
|
- // 부동소수점 비교를 위해 근사값 비교
|
|
|
- return (
|
|
|
- Math.abs(pos.getLat() - targetLat) < 0.0001 &&
|
|
|
- Math.abs(pos.getLng() - targetLng) < 0.0001
|
|
|
- );
|
|
|
- });
|
|
|
- if (marker && marker.overlay) {
|
|
|
- // 다른 오버레이 닫기
|
|
|
- overlays.value.forEach((ov) => ov.setMap(null));
|
|
|
- marker.overlay.setMap(map.value);
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 검색
|
|
|
- const searchLocations = () => {
|
|
|
- // 내 주변 필터 해제
|
|
|
- userLocation.value = null;
|
|
|
-
|
|
|
- // computed에서 자동으로 필터링되므로 별도 처리 불필요
|
|
|
- // 검색 후 마커 업데이트
|
|
|
- nextTick(() => {
|
|
|
- createMarkers();
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- // 내 주변 찾기
|
|
|
- const findNearbyLocations = () => {
|
|
|
- if (!navigator.geolocation) {
|
|
|
- alert("이 브라우저는 위치 정보를 지원하지 않습니다.");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- isLoading.value = true;
|
|
|
-
|
|
|
- navigator.geolocation.getCurrentPosition(
|
|
|
- (position) => {
|
|
|
- userLocation.value = {
|
|
|
- lat: position.coords.latitude,
|
|
|
- lng: position.coords.longitude,
|
|
|
- };
|
|
|
-
|
|
|
- // 지도 중심을 현재 위치로 이동
|
|
|
- if (map.value && window.kakao) {
|
|
|
- const currentPosition = new window.kakao.maps.LatLng(
|
|
|
- userLocation.value.lat,
|
|
|
- userLocation.value.lng
|
|
|
- );
|
|
|
- map.value.setCenter(currentPosition);
|
|
|
- map.value.setLevel(5);
|
|
|
-
|
|
|
- // 기존 현재 위치 마커/원 제거
|
|
|
- if (userLocationMarker.value) {
|
|
|
- userLocationMarker.value.setMap(null);
|
|
|
- }
|
|
|
- if (userLocationCircle.value) {
|
|
|
- userLocationCircle.value.setMap(null);
|
|
|
- }
|
|
|
-
|
|
|
- // 현재 위치 마커 추가 (파란색 원)
|
|
|
- const markerContent = `
|
|
|
- <div style="width: 20px; height: 20px; background: #4285F4; border: 3px solid white; border-radius: 50%; box-shadow: 0 2px 6px rgba(0,0,0,0.3);"></div>
|
|
|
- `;
|
|
|
- userLocationMarker.value = new window.kakao.maps.CustomOverlay({
|
|
|
- content: markerContent,
|
|
|
- position: currentPosition,
|
|
|
- xAnchor: 0.5,
|
|
|
- yAnchor: 0.5,
|
|
|
- });
|
|
|
- userLocationMarker.value.setMap(map.value);
|
|
|
-
|
|
|
- // 반경 원 표시
|
|
|
- userLocationCircle.value = new window.kakao.maps.Circle({
|
|
|
- center: currentPosition,
|
|
|
- radius: nearbyRadius.value * 1000, // km를 m로 변환
|
|
|
- strokeWeight: 2,
|
|
|
- strokeColor: "#4285F4",
|
|
|
- strokeOpacity: 0.8,
|
|
|
- strokeStyle: "solid",
|
|
|
- fillColor: "#4285F4",
|
|
|
- fillOpacity: 0.15,
|
|
|
- });
|
|
|
- userLocationCircle.value.setMap(map.value);
|
|
|
- }
|
|
|
-
|
|
|
- // 마커 업데이트
|
|
|
- nextTick(() => {
|
|
|
- createMarkers();
|
|
|
- });
|
|
|
-
|
|
|
- isLoading.value = false;
|
|
|
-
|
|
|
- const nearbyCount = filteredLocations.value.length;
|
|
|
- if (nearbyCount === 0) {
|
|
|
- alert(`주변 ${nearbyRadius.value}km 이내에 ${locationLabel.value}이 없습니다.`);
|
|
|
- } else {
|
|
|
- alert(
|
|
|
- `주변 ${nearbyRadius.value}km 이내에 ${nearbyCount}개의 ${locationLabel.value}을 찾았습니다.`
|
|
|
- );
|
|
|
- }
|
|
|
- },
|
|
|
- (error) => {
|
|
|
- isLoading.value = false;
|
|
|
-
|
|
|
- let errorMessage = "위치 정보를 가져올 수 없습니다.";
|
|
|
- if (error.code === 1) {
|
|
|
- errorMessage =
|
|
|
- "위치 정보 접근이 거부되었습니다. 브라우저 설정에서 위치 정보 권한을 허용해주세요.";
|
|
|
- } else if (error.code === 2) {
|
|
|
- errorMessage = "위치 정보를 사용할 수 없습니다.";
|
|
|
- } else if (error.code === 3) {
|
|
|
- errorMessage = "위치 정보 요청 시간이 초과되었습니다.";
|
|
|
- }
|
|
|
-
|
|
|
- alert(errorMessage);
|
|
|
- },
|
|
|
- {
|
|
|
- enableHighAccuracy: true,
|
|
|
- timeout: 10000,
|
|
|
- maximumAge: 0,
|
|
|
- }
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- // 스크롤 이벤트 핸들러
|
|
|
- const handleScroll = () => {
|
|
|
- const el = listWrapRef.value;
|
|
|
- if (el) {
|
|
|
- const threshold = 10;
|
|
|
- isScrolledToBottom.value =
|
|
|
- el.scrollTop + el.clientHeight >= el.scrollHeight - threshold;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 검색어나 지역 변경시 내 주변 필터 해제
|
|
|
- watch([searchKeyword, selectedRegion], () => {
|
|
|
- if (userLocation.value) {
|
|
|
- userLocation.value = null;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // type이 변경되면 데이터 다시 로드 및 지도 리셋
|
|
|
- watch(
|
|
|
- () => route.query.type,
|
|
|
- (newType, oldType) => {
|
|
|
- // 실제로 타입이 변경된 경우에만 실행
|
|
|
- if (newType === oldType) return;
|
|
|
-
|
|
|
- // 데이터 초기화
|
|
|
- locations.value = [];
|
|
|
- selectedLocation.value = null;
|
|
|
- searchKeyword.value = "";
|
|
|
- selectedRegion.value = "";
|
|
|
- userLocation.value = null;
|
|
|
-
|
|
|
- // 모든 마커 제거
|
|
|
- clearAllMarkers();
|
|
|
-
|
|
|
- // 지도 리셋 (서울 중심으로)
|
|
|
- if (map.value && window.kakao) {
|
|
|
- const center = new window.kakao.maps.LatLng(37.5665, 126.978);
|
|
|
- map.value.setCenter(center);
|
|
|
- map.value.setLevel(8);
|
|
|
- }
|
|
|
-
|
|
|
- // 데이터 다시 로드
|
|
|
- loadLocations();
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 페이지 로드 시 위치 권한 요청 (권한만 요청, 필터는 적용하지 않음)
|
|
|
- const requestLocationPermission = () => {
|
|
|
- if (!navigator.geolocation) {
|
|
|
- return;
|
|
|
- }
|
|
|
- // 위치 권한만 요청하고 userLocation은 설정하지 않음
|
|
|
- navigator.geolocation.getCurrentPosition(
|
|
|
- () => {
|
|
|
- // 권한 허용됨 - 나중에 "내 주변" 클릭 시 사용
|
|
|
- },
|
|
|
- () => {
|
|
|
- // 권한 거부되어도 페이지는 정상 동작
|
|
|
- },
|
|
|
- { enableHighAccuracy: false, timeout: 10000, maximumAge: 300000 }
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- onMounted(async () => {
|
|
|
- console.log("[KakaoMap] onMounted");
|
|
|
-
|
|
|
- // 페이지 로드 시 위치 권한 요청
|
|
|
- requestLocationPermission();
|
|
|
-
|
|
|
- try {
|
|
|
- await loadKakaoMapsScript();
|
|
|
- await nextTick(); // DOM이 렌더링될 때까지 대기
|
|
|
- initMap();
|
|
|
- } catch (error) {
|
|
|
- console.error("[KakaoMap] Load error:", error);
|
|
|
- // 지도 로드 실패해도 목록은 표시
|
|
|
- } finally {
|
|
|
- await loadLocations();
|
|
|
- }
|
|
|
- });
|
|
|
-</script>
|