Files
jongryangje/app/Views/bag/_dashboard_kakao_map.php

195 lines
8.2 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
/**
* 메인 대시보드 카카오 지도 패널 지도() + 지정판매소 목록(, 스크롤).
* 주소→좌표는 카카오 지오코딩(services)으로 클라이언트 변환. 목록 클릭 해당 위치로 이동.
*
* @var string $kakaoJsKey
* @var string $lgLabel
* @var array<int,array{name:string,addr:string}> $mapShops
*/
$kakaoJsKey = (string) ($kakaoJsKey ?? '');
$lgLabel = (string) ($lgLabel ?? '');
$mapShops = is_array($mapShops ?? null) ? $mapShops : [];
$mapId = 'mainKakaoMap';
?>
<div class="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden">
<div class="flex items-center justify-between px-4 py-2.5 border-b border-gray-100">
<h2 class="text-sm font-bold text-gray-900"><i class="fa-solid fa-map-location-dot text-[#243a5e] mr-1"></i>지정판매소 위치<?= $lgLabel !== '' ? ' · ' . esc($lgLabel) : '' ?> <span class="text-[11px] font-normal text-gray-400">(<?= count($mapShops) ?>곳)</span></h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-[1fr_240px]">
<!-- 지도 -->
<div id="<?= $mapId ?>" style="width:100%;height:200px;background:#eef2f7;" role="application" aria-label="<?= esc($lgLabel, 'attr') ?> 지정판매소 지도"></div>
<!-- 판매소 목록 (스크롤) -->
<div class="border-t md:border-t-0 md:border-l border-gray-100 overflow-y-auto" style="height:200px;">
<?php if ($mapShops === []): ?>
<p class="p-3 text-[12px] text-gray-400">표시할 지정판매소가 없습니다.</p>
<?php else: ?>
<ul id="shopList" class="divide-y divide-gray-50">
<?php foreach ($mapShops as $i => $shop): ?>
<li>
<button type="button" data-idx="<?= (int) $i ?>"
class="shop-item w-full text-left px-3 py-2 hover:bg-blue-50/60 transition flex gap-2 items-start">
<span class="shop-dot mt-1 inline-block w-2 h-2 rounded-full bg-gray-300 shrink-0"></span>
<span class="min-w-0">
<span class="block text-[12px] font-semibold text-gray-800 truncate"><?= esc($shop['name'] !== '' ? $shop['name'] : '(이름없음)') ?></span>
<span class="block text-[11px] text-gray-500 truncate"><?= esc($shop['addr']) ?></span>
</span>
</button>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
<?php if ($kakaoJsKey === ''): ?>
<div class="px-4 py-2 text-[11px] text-amber-700 bg-amber-50 border-t border-amber-200">카카오맵 키가 설정되지 않아 지도를 표시할 없습니다.</div>
<?php endif; ?>
</div>
<?php if ($kakaoJsKey !== ''): ?>
<script>
(function () {
var APP_KEY = <?= json_encode($kakaoJsKey, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var SHOPS = <?= json_encode($mapShops, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
var MAP_ID = <?= json_encode($mapId, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var DEFAULT_CENTER = { lat: 35.8714, lng: 128.6014 };
var markers = {}; // idx -> kakao.maps.Marker
var positions = {}; // idx -> kakao.maps.LatLng
var mapRef = null, infowindow = null, geocoder = null, places = null;
function ensureScript(cb) {
if (typeof kakao !== 'undefined' && kakao.maps && kakao.maps.services) { cb(); return; }
var s = document.createElement('script');
s.charset = 'UTF-8'; s.async = true;
s.src = 'https://dapi.kakao.com/v2/maps/sdk.js?appkey=' + encodeURIComponent(APP_KEY) + '&libraries=services&autoload=false';
s.onload = function () {
if (typeof kakao === 'undefined' || !kakao.maps || typeof kakao.maps.load !== 'function') { return; }
kakao.maps.load(cb);
};
document.head.appendChild(s);
}
// "대구광역시 북구 검단동 칠곡중앙대로 21" → "대구광역시 북구 칠곡중앙대로 21" (행정동 토큰 제거)
function roadVariant(addr) {
var p = String(addr || '').trim().split(/\s+/);
if (p.length < 4) return '';
var roadIdx = -1;
for (var i = 2; i < p.length; i++) { if (/(|)$/.test(p[i])) { roadIdx = i; break; } }
if (roadIdx < 3) return '';
var out = [];
p.forEach(function (tok, i) {
if (i >= 2 && i < roadIdx && /(||)$/.test(tok)) return; // 행정동 제거
out.push(tok);
});
var joined = out.join(' ');
return joined !== addr ? joined : '';
}
// "대구광역시 북구 검단동 ..." → "대구광역시 북구 검단동" (시도 구군 동 — 행정구역 폴백)
function regionVariant(addr) {
var p = String(addr || '').trim().split(/\s+/);
if (p.length < 3) return '';
for (var i = 2; i < p.length; i++) {
if (/(||)$/.test(p[i])) { return p[0] + ' ' + p[1] + ' ' + p[i]; }
}
return p[0] + ' ' + p[1]; // 동을 못 찾으면 구·군까지
}
// 여러 후보를 순서대로 시도(정밀주소 → 도로명변형 → 지번 → 키워드 → 행정동 → 행정동 키워드)
function geocodeChain(shop, cb) {
var tries = [];
if (shop.addr) tries.push(['addr', shop.addr]);
var rv = roadVariant(shop.addr);
if (rv) tries.push(['addr', rv]);
if (shop.jibun) tries.push(['addr', shop.jibun]);
if (rv) tries.push(['kw', rv]);
else if (shop.addr) tries.push(['kw', shop.addr]);
var region = regionVariant(shop.addr || shop.jibun);
if (region) { tries.push(['addr', region]); tries.push(['kw', region]); }
(function next(i) {
if (i >= tries.length) { cb(null); return; }
var mode = tries[i][0], q = tries[i][1];
if (mode === 'addr') {
geocoder.addressSearch(q, function (result, status) {
if (status === kakao.maps.services.Status.OK && result && result[0]) {
cb(new kakao.maps.LatLng(result[0].y, result[0].x));
} else { next(i + 1); }
});
} else {
places.keywordSearch(q, function (data, status) {
if (status === kakao.maps.services.Status.OK && data && data[0]) {
cb(new kakao.maps.LatLng(data[0].y, data[0].x));
} else { next(i + 1); }
});
}
})(0);
}
function openInfo(idx) {
var name = (SHOPS[idx] && SHOPS[idx].name) || '판매소';
infowindow.setContent('<div style="padding:5px 8px;font-size:12px;font-weight:600;white-space:nowrap;">' + name + '</div>');
if (markers[idx]) infowindow.open(mapRef, markers[idx]);
}
function focusShop(idx) {
var pos = positions[idx];
if (!pos || !mapRef) return;
mapRef.setCenter(pos);
mapRef.setLevel(3); // 줌인
openInfo(idx);
}
function initMap() {
var el = document.getElementById(MAP_ID);
if (!el || typeof kakao === 'undefined' || !kakao.maps) return;
var map = new kakao.maps.Map(el, { center: new kakao.maps.LatLng(DEFAULT_CENTER.lat, DEFAULT_CENTER.lng), level: 8 });
mapRef = map;
infowindow = new kakao.maps.InfoWindow({ zIndex: 1 });
geocoder = new kakao.maps.services.Geocoder();
places = new kakao.maps.services.Places();
var bounds = new kakao.maps.LatLngBounds();
var placed = 0, pending = SHOPS.length;
function done() {
if (placed > 0) map.setBounds(bounds);
setTimeout(function () { map.relayout(); if (placed > 0) map.setBounds(bounds); }, 150);
}
if (pending === 0) { done(); return; }
SHOPS.forEach(function (shop, idx) {
geocodeChain(shop, function (pos) {
pending--;
if (pos) {
var marker = new kakao.maps.Marker({ position: pos, map: map });
markers[idx] = marker;
positions[idx] = pos;
bounds.extend(pos); placed++;
kakao.maps.event.addListener(marker, 'click', function () { openInfo(idx); });
var dot = document.querySelector('.shop-item[data-idx="' + idx + '"] .shop-dot');
if (dot) { dot.style.background = '#243a5e'; }
}
if (pending === 0) done();
});
});
// 목록 클릭 → 해당 판매소로 줌인
document.querySelectorAll('.shop-item').forEach(function (btn) {
btn.addEventListener('click', function () { focusShop(parseInt(btn.getAttribute('data-idx'), 10)); });
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { ensureScript(initMap); });
} else {
ensureScript(initMap);
}
})();
</script>
<?php endif; ?>