| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928 |
- <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>
|