feat(supporters): 오시는 길 지도·길찾기(VisitMap) 템플릿 반영, 워커 tourism 단계가 factSheet.geo 좌표 기록
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
aa5b05227f
commit
ed8f65030d
@ -6,7 +6,15 @@
|
||||
"areaLabel": "",
|
||||
"founded": "",
|
||||
"representative": "",
|
||||
"address": { "full": "", "street": "", "locality": "", "region": "", "postalNote": "", "navigation": "", "source": "" },
|
||||
"address": {
|
||||
"full": "",
|
||||
"street": "",
|
||||
"locality": "",
|
||||
"region": "",
|
||||
"postalNote": "",
|
||||
"navigation": "",
|
||||
"source": ""
|
||||
},
|
||||
"phone": "",
|
||||
"fax": "",
|
||||
"kakao": "",
|
||||
@ -18,15 +26,28 @@
|
||||
"transitShort": "",
|
||||
"transitNote": "",
|
||||
"parking": "",
|
||||
"hours": { "source": "", "sourceLabel": "", "rows": [], "schema": [] },
|
||||
"building": { "summary": "", "floors": [], "source": "" },
|
||||
"hours": {
|
||||
"source": "",
|
||||
"sourceLabel": "",
|
||||
"rows": [],
|
||||
"schema": []
|
||||
},
|
||||
"building": {
|
||||
"summary": "",
|
||||
"floors": [],
|
||||
"source": ""
|
||||
},
|
||||
"departments": [],
|
||||
"specialties": [],
|
||||
"safety": { "items": [], "source": "" },
|
||||
"safety": {
|
||||
"items": [],
|
||||
"source": ""
|
||||
},
|
||||
"recognitions": [],
|
||||
"surfaces": {},
|
||||
"sideEffectNotice": "수술 후 개인에 따라 염증, 출혈, 신경손상 등의 부작용이 있을 수 있습니다.",
|
||||
"businessNo": "",
|
||||
"checkedAt": "",
|
||||
"nextCheck": ""
|
||||
"nextCheck": "",
|
||||
"geo": null
|
||||
}
|
||||
|
||||
99
supporters/src/components/VisitMap.astro
Normal file
99
supporters/src/components/VisitMap.astro
Normal file
@ -0,0 +1,99 @@
|
||||
---
|
||||
// 오시는 길: 지도 + 주소 복사 + 길찾기 버튼. 방문 안내(/visit) 3. 찾아가기와 영어 홈 Getting Here 에서 쓴다.
|
||||
// 좌표는 factSheet.geo {lat,lng,source} 를 먼저 보고(source 는 운영 기록이라 화면에 내지 않는다), 없으면 관광공사 수집 메타(medicalTourismKo.json meta.origin)를 쓴다. 둘 다 없으면 지도 없이 주소·버튼만 낸다.
|
||||
// 지도는 OpenStreetMap 타일 + Leaflet(cdnjs). 길찾기는 네이버·카카오 지도의 공개 URL 형식(벤치마크 2026-09-11 확인)이고, 영어 화면에는 구글 지도 길찾기를 더한다.
|
||||
import { fact, has, v } from '../lib';
|
||||
import tour from '../data/medicalTourismKo.json';
|
||||
interface Props { lang?: 'ko' | 'en'; nameEn?: string; addressEn?: string; title?: string; sub?: string; compact?: boolean; hideHead?: boolean }
|
||||
const { lang = 'ko', nameEn, addressEn, title, sub, compact = false, hideHead = false } = Astro.props;
|
||||
const f = fact as Record<string, any>;
|
||||
const origin = (tour as any)?.meta?.origin ?? {};
|
||||
const geo = f.geo && f.geo.lat && f.geo.lng ? f.geo : (origin.lat && origin.lng ? { lat: origin.lat, lng: origin.lng, source: origin.coordSource || '관광공사 수집 기준 좌표' } : null);
|
||||
const nameKo = v(f.shortName, '병원');
|
||||
const name = lang === 'en' ? (nameEn || f.shortNameEn || f.nameEn || nameKo) : nameKo;
|
||||
const address = lang === 'en' ? (addressEn || f.address?.fullEn || f.address?.full || '') : (f.address?.full || '');
|
||||
const phone: string = f.phone || '';
|
||||
const enc = encodeURIComponent;
|
||||
const naverUrl = geo ? `https://map.naver.com/p/directions/-/${geo.lng},${geo.lat},${enc(nameKo)}/-/transit` : (has(f.address?.full) ? `https://map.naver.com/p/search/${enc(f.address.full)}` : '');
|
||||
const kakaoUrl = geo ? `https://map.kakao.com/link/to/${enc(nameKo)},${geo.lat},${geo.lng}` : (has(f.address?.full) ? `https://map.kakao.com/link/search/${enc(f.address.full)}` : '');
|
||||
const googleUrl = geo ? `https://www.google.com/maps/dir/?api=1&destination=${geo.lat},${geo.lng}&destination_place_id=&travelmode=transit` : (address ? `https://www.google.com/maps/search/?api=1&query=${enc(address)}` : '');
|
||||
const T = lang === 'en'
|
||||
? { title: title ?? 'Getting Here', sub: sub ?? 'Address, and directions from where you are.', address: 'Address', copy: 'Copy', copied: 'Copied', phone: 'Phone', naver: 'Naver Map directions', kakao: 'Kakao Map directions', google: 'Google Maps directions', noMap: 'Map is added once the clinic confirms its location.', attribution: 'Map data © OpenStreetMap contributors' }
|
||||
: { title: title ?? '오시는 길', sub: sub ?? '주소와 현재 위치에서 오는 길을 안내합니다.', address: '주소', copy: '복사', copied: '복사했습니다', phone: '문의', naver: '네이버 길찾기', kakao: '카카오 길찾기', google: '구글 지도 길찾기', noMap: '지도는 병원이 위치를 확인한 뒤 싣습니다.', attribution: '지도 데이터 © OpenStreetMap 기여자' };
|
||||
const mapId = `visit-map-${Math.random().toString(36).slice(2, 8)}`;
|
||||
---
|
||||
{geo && <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" crossorigin="" />}
|
||||
<section class={`visit-map ${compact ? 'compact' : ''}`}>
|
||||
{!compact && !hideHead && <div class="vm-head"><div class="eyebrow">Directions</div><h2 class="serif">{T.title}</h2><p class="sub">{T.sub}</p></div>}
|
||||
<div class="vm-grid">
|
||||
<div class="vm-map-wrap">
|
||||
{geo
|
||||
? <div class="vm-map" id={mapId} data-lat={geo.lat} data-lng={geo.lng} data-name={name} role="img" aria-label={`${name} ${T.title}`}></div>
|
||||
: <div class="vm-map vm-empty"><p>{T.noMap}</p></div>}
|
||||
{geo && <p class="vm-attr">{T.attribution}</p>}
|
||||
</div>
|
||||
<div class="vm-card">
|
||||
<div class="vm-label">{T.address}</div>
|
||||
<div class="vm-address"><span>{address || v(null)}</span>{address && <button type="button" class="vm-copy" data-copy={address} data-done={T.copied}><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1Zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm0 16H8V7h11v14Z"/></svg><span>{T.copy}</span></button>}</div>
|
||||
{phone && <p class="vm-phone">{T.phone}: <a href={`tel:${phone.replace(/-/g, '')}`}>{phone}</a></p>}
|
||||
{lang === 'ko' && has(f.transit) && <p class="vm-transit">{f.transit}</p>}
|
||||
<div class="vm-actions">
|
||||
{lang === 'en' && googleUrl && <a class="vm-btn google" href={googleUrl} rel="noopener" target="_blank"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.7 2.3 2.6 10.5c-.9.4-.8 1.7.1 2l7.4 2.4 2.4 7.4c.3.9 1.6 1 2 .1l8.2-19.1c.4-.9-.5-1.8-1-1Z"/></svg>{T.google}</a>}
|
||||
{naverUrl && <a class="vm-btn naver" href={naverUrl} rel="noopener" target="_blank"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.7 2.3 2.6 10.5c-.9.4-.8 1.7.1 2l7.4 2.4 2.4 7.4c.3.9 1.6 1 2 .1l8.2-19.1c.4-.9-.5-1.8-1-1Z"/></svg>{T.naver}</a>}
|
||||
{kakaoUrl && <a class="vm-btn kakao" href={kakaoUrl} rel="noopener" target="_blank"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.7 2.3 2.6 10.5c-.9.4-.8 1.7.1 2l7.4 2.4 2.4 7.4c.3.9 1.6 1 2 .1l8.2-19.1c.4-.9-.5-1.8-1-1Z"/></svg>{T.kakao}</a>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.visit-map { margin: 1.6rem 0 2.2rem; }
|
||||
.vm-head { margin-bottom: 1.2rem; }
|
||||
.vm-head h2 { margin: 0.2rem 0 0.2rem; font-size: 1.6rem; }
|
||||
.vm-head .sub { margin: 0; color: var(--slate-600); }
|
||||
.vm-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.9fr); gap: 1.25rem; align-items: start; }
|
||||
.vm-map { width: 100%; aspect-ratio: 16 / 10; min-height: 260px; border-radius: 16px; overflow: hidden; border: 1px solid var(--slate-100); box-shadow: var(--shadow); background: var(--primary-50); z-index: 0; }
|
||||
.vm-empty { display: grid; place-items: center; color: var(--slate-500); font-size: 0.92rem; padding: 1rem; text-align: center; }
|
||||
.vm-empty p { margin: 0; }
|
||||
.vm-attr { font-size: 0.74rem; color: var(--slate-500); margin: 0.45rem 0 0; }
|
||||
.vm-card { background: #fff; border: 1px solid var(--slate-100); border-radius: 16px; padding: 1.3rem 1.4rem; box-shadow: var(--shadow); display: grid; gap: 0.55rem; align-content: start; }
|
||||
.vm-label { font-size: 0.78rem; color: var(--slate-500); font-weight: 600; letter-spacing: 0.02em; }
|
||||
.vm-address { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.7rem; font-size: 1.08rem; font-weight: 700; color: var(--primary-900); line-height: 1.45; }
|
||||
.vm-copy { display: inline-flex; align-items: center; gap: 0.3rem; border: 1px solid var(--slate-200); background: #fff; color: var(--slate-600); border-radius: 8px; padding: 0.25rem 0.55rem; font: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer; }
|
||||
.vm-copy:hover { color: var(--primary-900); border-color: var(--primary-900); }
|
||||
.vm-phone, .vm-transit { margin: 0; font-size: 0.92rem; color: var(--slate-600); }
|
||||
.vm-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.6rem; margin-top: 0.5rem; }
|
||||
.vm-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; border-radius: 12px; padding: 0.75rem 0.9rem; font-size: 0.92rem; font-weight: 700; text-decoration: none; color: #fff; box-shadow: var(--shadow); }
|
||||
.vm-btn:hover { text-decoration: none; box-shadow: var(--shadow-hover); }
|
||||
.vm-btn.naver { background: #03C75A; }
|
||||
.vm-btn.kakao { background: #FEE500; color: #191919; }
|
||||
.vm-btn.google { background: var(--primary-900); }
|
||||
.visit-map.compact .vm-grid { grid-template-columns: 1fr; }
|
||||
@media (max-width: 760px) { .vm-grid { grid-template-columns: 1fr; } .vm-map { aspect-ratio: 4 / 3; } }
|
||||
@media print { .vm-map { break-inside: avoid; } .vm-actions { display: none; } }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 주소 복사
|
||||
document.querySelectorAll<HTMLButtonElement>('.vm-copy').forEach((b) => b.addEventListener('click', async () => {
|
||||
try { await navigator.clipboard.writeText(b.dataset.copy || ''); const s = b.querySelector('span'); const orig = s?.textContent ?? ''; if (s) s.textContent = b.dataset.done || orig; setTimeout(() => { if (s) s.textContent = orig; }, 1400); } catch { /* 클립보드 권한 없음 */ }
|
||||
}));
|
||||
// 지도: Leaflet 을 필요할 때만 불러온다(지도가 있는 페이지에서만).
|
||||
const maps = Array.from(document.querySelectorAll<HTMLElement>('.vm-map[data-lat]'));
|
||||
if (maps.length) {
|
||||
const s = document.createElement('script');
|
||||
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js';
|
||||
s.crossOrigin = '';
|
||||
s.onload = () => {
|
||||
const L = (window as any).L;
|
||||
for (const el of maps) {
|
||||
const lat = Number(el.dataset.lat), lng = Number(el.dataset.lng);
|
||||
const map = L.map(el, { scrollWheelZoom: false }).setView([lat, lng], 16);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright" rel="noopener">OpenStreetMap</a> contributors' }).addTo(map);
|
||||
const icon = L.divIcon({ className: 'vm-pin', html: '<svg width="34" height="44" viewBox="0 0 34 44" aria-hidden="true"><path d="M17 0C7.6 0 0 7.6 0 17c0 12.5 17 27 17 27s17-14.5 17-27C34 7.6 26.4 0 17 0Z" fill="#4F1DA1"/><circle cx="17" cy="17" r="6.5" fill="#fff"/></svg>', iconSize: [34, 44], iconAnchor: [17, 44], popupAnchor: [0, -40] });
|
||||
L.marker([lat, lng], { icon }).addTo(map).bindPopup(`<b>${el.dataset.name ?? ''}</b>`);
|
||||
}
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
</script>
|
||||
@ -219,5 +219,10 @@
|
||||
"sideEffectNotice": "수술 후 개인에 따라 염증, 출혈, 신경손상 등의 부작용이 있을 수 있습니다.",
|
||||
"businessNo": "220-08-86777",
|
||||
"checkedAt": "2026-09-04",
|
||||
"nextCheck": "2026-10-04"
|
||||
"nextCheck": "2026-10-04",
|
||||
"geo": {
|
||||
"lat": 37.5049823,
|
||||
"lng": 127.0254182,
|
||||
"source": "네이버 지역검색 실측 2026-09-09"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// 방문 안내. 병원 정보(/clinic)가 사실 표라면 이 페이지는 "예약하고, 찾아가고, 도착해서 뭘 하는지"의 행동 순서다. 값은 모두 factSheet.json에서 가져온다.
|
||||
import Base from '../layouts/Base.astro';
|
||||
import Gallery from '../components/Gallery.astro';
|
||||
import VisitMap from '../components/VisitMap.astro';
|
||||
import { fact, site as S, SITE_NAME, clinicSchema, reservationUrl, v, has } from '../lib';
|
||||
const f = fact as Record<string, any>;
|
||||
const clinic = v(f.shortName, '병원');
|
||||
@ -31,6 +32,7 @@ const ld = [
|
||||
<p><strong>주소</strong> {v(f.address?.full)}{has(f.address?.postalNote) && <> (지번 {f.address.postalNote})</>}</p>
|
||||
<p><strong>지하철</strong> {v(f.transit)}.{has(f.transitNote) && <> {f.transitNote}</>}</p>
|
||||
<p><strong>차로 올 때</strong> {v(f.parking, `주차 안내 ${v(null)}`)}.</p>
|
||||
<VisitMap hideHead />
|
||||
{S.visitMaps?.length > 0 && <Gallery cols={2} items={S.visitMaps} />}
|
||||
|
||||
<h2>4. 도착하면</h2>
|
||||
|
||||
99
templates/supporters-astro/src/components/VisitMap.astro
Normal file
99
templates/supporters-astro/src/components/VisitMap.astro
Normal file
@ -0,0 +1,99 @@
|
||||
---
|
||||
// 오시는 길: 지도 + 주소 복사 + 길찾기 버튼. 방문 안내(/visit) 3. 찾아가기와 영어 홈 Getting Here 에서 쓴다.
|
||||
// 좌표는 factSheet.geo {lat,lng,source} 를 먼저 보고(source 는 운영 기록이라 화면에 내지 않는다), 없으면 관광공사 수집 메타(medicalTourismKo.json meta.origin)를 쓴다. 둘 다 없으면 지도 없이 주소·버튼만 낸다.
|
||||
// 지도는 OpenStreetMap 타일 + Leaflet(cdnjs). 길찾기는 네이버·카카오 지도의 공개 URL 형식(벤치마크 2026-09-11 확인)이고, 영어 화면에는 구글 지도 길찾기를 더한다.
|
||||
import { fact, has, v } from '../lib';
|
||||
import tour from '../data/medicalTourismKo.json';
|
||||
interface Props { lang?: 'ko' | 'en'; nameEn?: string; addressEn?: string; title?: string; sub?: string; compact?: boolean; hideHead?: boolean }
|
||||
const { lang = 'ko', nameEn, addressEn, title, sub, compact = false, hideHead = false } = Astro.props;
|
||||
const f = fact as Record<string, any>;
|
||||
const origin = (tour as any)?.meta?.origin ?? {};
|
||||
const geo = f.geo && f.geo.lat && f.geo.lng ? f.geo : (origin.lat && origin.lng ? { lat: origin.lat, lng: origin.lng, source: origin.coordSource || '관광공사 수집 기준 좌표' } : null);
|
||||
const nameKo = v(f.shortName, '병원');
|
||||
const name = lang === 'en' ? (nameEn || f.shortNameEn || f.nameEn || nameKo) : nameKo;
|
||||
const address = lang === 'en' ? (addressEn || f.address?.fullEn || f.address?.full || '') : (f.address?.full || '');
|
||||
const phone: string = f.phone || '';
|
||||
const enc = encodeURIComponent;
|
||||
const naverUrl = geo ? `https://map.naver.com/p/directions/-/${geo.lng},${geo.lat},${enc(nameKo)}/-/transit` : (has(f.address?.full) ? `https://map.naver.com/p/search/${enc(f.address.full)}` : '');
|
||||
const kakaoUrl = geo ? `https://map.kakao.com/link/to/${enc(nameKo)},${geo.lat},${geo.lng}` : (has(f.address?.full) ? `https://map.kakao.com/link/search/${enc(f.address.full)}` : '');
|
||||
const googleUrl = geo ? `https://www.google.com/maps/dir/?api=1&destination=${geo.lat},${geo.lng}&destination_place_id=&travelmode=transit` : (address ? `https://www.google.com/maps/search/?api=1&query=${enc(address)}` : '');
|
||||
const T = lang === 'en'
|
||||
? { title: title ?? 'Getting Here', sub: sub ?? 'Address, and directions from where you are.', address: 'Address', copy: 'Copy', copied: 'Copied', phone: 'Phone', naver: 'Naver Map directions', kakao: 'Kakao Map directions', google: 'Google Maps directions', noMap: 'Map is added once the clinic confirms its location.', attribution: 'Map data © OpenStreetMap contributors' }
|
||||
: { title: title ?? '오시는 길', sub: sub ?? '주소와 현재 위치에서 오는 길을 안내합니다.', address: '주소', copy: '복사', copied: '복사했습니다', phone: '문의', naver: '네이버 길찾기', kakao: '카카오 길찾기', google: '구글 지도 길찾기', noMap: '지도는 병원이 위치를 확인한 뒤 싣습니다.', attribution: '지도 데이터 © OpenStreetMap 기여자' };
|
||||
const mapId = `visit-map-${Math.random().toString(36).slice(2, 8)}`;
|
||||
---
|
||||
{geo && <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" crossorigin="" />}
|
||||
<section class={`visit-map ${compact ? 'compact' : ''}`}>
|
||||
{!compact && !hideHead && <div class="vm-head"><div class="eyebrow">Directions</div><h2 class="serif">{T.title}</h2><p class="sub">{T.sub}</p></div>}
|
||||
<div class="vm-grid">
|
||||
<div class="vm-map-wrap">
|
||||
{geo
|
||||
? <div class="vm-map" id={mapId} data-lat={geo.lat} data-lng={geo.lng} data-name={name} role="img" aria-label={`${name} ${T.title}`}></div>
|
||||
: <div class="vm-map vm-empty"><p>{T.noMap}</p></div>}
|
||||
{geo && <p class="vm-attr">{T.attribution}</p>}
|
||||
</div>
|
||||
<div class="vm-card">
|
||||
<div class="vm-label">{T.address}</div>
|
||||
<div class="vm-address"><span>{address || v(null)}</span>{address && <button type="button" class="vm-copy" data-copy={address} data-done={T.copied}><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1Zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm0 16H8V7h11v14Z"/></svg><span>{T.copy}</span></button>}</div>
|
||||
{phone && <p class="vm-phone">{T.phone}: <a href={`tel:${phone.replace(/-/g, '')}`}>{phone}</a></p>}
|
||||
{lang === 'ko' && has(f.transit) && <p class="vm-transit">{f.transit}</p>}
|
||||
<div class="vm-actions">
|
||||
{lang === 'en' && googleUrl && <a class="vm-btn google" href={googleUrl} rel="noopener" target="_blank"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.7 2.3 2.6 10.5c-.9.4-.8 1.7.1 2l7.4 2.4 2.4 7.4c.3.9 1.6 1 2 .1l8.2-19.1c.4-.9-.5-1.8-1-1Z"/></svg>{T.google}</a>}
|
||||
{naverUrl && <a class="vm-btn naver" href={naverUrl} rel="noopener" target="_blank"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.7 2.3 2.6 10.5c-.9.4-.8 1.7.1 2l7.4 2.4 2.4 7.4c.3.9 1.6 1 2 .1l8.2-19.1c.4-.9-.5-1.8-1-1Z"/></svg>{T.naver}</a>}
|
||||
{kakaoUrl && <a class="vm-btn kakao" href={kakaoUrl} rel="noopener" target="_blank"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.7 2.3 2.6 10.5c-.9.4-.8 1.7.1 2l7.4 2.4 2.4 7.4c.3.9 1.6 1 2 .1l8.2-19.1c.4-.9-.5-1.8-1-1Z"/></svg>{T.kakao}</a>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.visit-map { margin: 1.6rem 0 2.2rem; }
|
||||
.vm-head { margin-bottom: 1.2rem; }
|
||||
.vm-head h2 { margin: 0.2rem 0 0.2rem; font-size: 1.6rem; }
|
||||
.vm-head .sub { margin: 0; color: var(--slate-600); }
|
||||
.vm-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.9fr); gap: 1.25rem; align-items: start; }
|
||||
.vm-map { width: 100%; aspect-ratio: 16 / 10; min-height: 260px; border-radius: 16px; overflow: hidden; border: 1px solid var(--slate-100); box-shadow: var(--shadow); background: var(--primary-50); z-index: 0; }
|
||||
.vm-empty { display: grid; place-items: center; color: var(--slate-500); font-size: 0.92rem; padding: 1rem; text-align: center; }
|
||||
.vm-empty p { margin: 0; }
|
||||
.vm-attr { font-size: 0.74rem; color: var(--slate-500); margin: 0.45rem 0 0; }
|
||||
.vm-card { background: #fff; border: 1px solid var(--slate-100); border-radius: 16px; padding: 1.3rem 1.4rem; box-shadow: var(--shadow); display: grid; gap: 0.55rem; align-content: start; }
|
||||
.vm-label { font-size: 0.78rem; color: var(--slate-500); font-weight: 600; letter-spacing: 0.02em; }
|
||||
.vm-address { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.7rem; font-size: 1.08rem; font-weight: 700; color: var(--primary-900); line-height: 1.45; }
|
||||
.vm-copy { display: inline-flex; align-items: center; gap: 0.3rem; border: 1px solid var(--slate-200); background: #fff; color: var(--slate-600); border-radius: 8px; padding: 0.25rem 0.55rem; font: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer; }
|
||||
.vm-copy:hover { color: var(--primary-900); border-color: var(--primary-900); }
|
||||
.vm-phone, .vm-transit { margin: 0; font-size: 0.92rem; color: var(--slate-600); }
|
||||
.vm-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.6rem; margin-top: 0.5rem; }
|
||||
.vm-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; border-radius: 12px; padding: 0.75rem 0.9rem; font-size: 0.92rem; font-weight: 700; text-decoration: none; color: #fff; box-shadow: var(--shadow); }
|
||||
.vm-btn:hover { text-decoration: none; box-shadow: var(--shadow-hover); }
|
||||
.vm-btn.naver { background: #03C75A; }
|
||||
.vm-btn.kakao { background: #FEE500; color: #191919; }
|
||||
.vm-btn.google { background: var(--primary-900); }
|
||||
.visit-map.compact .vm-grid { grid-template-columns: 1fr; }
|
||||
@media (max-width: 760px) { .vm-grid { grid-template-columns: 1fr; } .vm-map { aspect-ratio: 4 / 3; } }
|
||||
@media print { .vm-map { break-inside: avoid; } .vm-actions { display: none; } }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 주소 복사
|
||||
document.querySelectorAll<HTMLButtonElement>('.vm-copy').forEach((b) => b.addEventListener('click', async () => {
|
||||
try { await navigator.clipboard.writeText(b.dataset.copy || ''); const s = b.querySelector('span'); const orig = s?.textContent ?? ''; if (s) s.textContent = b.dataset.done || orig; setTimeout(() => { if (s) s.textContent = orig; }, 1400); } catch { /* 클립보드 권한 없음 */ }
|
||||
}));
|
||||
// 지도: Leaflet 을 필요할 때만 불러온다(지도가 있는 페이지에서만).
|
||||
const maps = Array.from(document.querySelectorAll<HTMLElement>('.vm-map[data-lat]'));
|
||||
if (maps.length) {
|
||||
const s = document.createElement('script');
|
||||
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js';
|
||||
s.crossOrigin = '';
|
||||
s.onload = () => {
|
||||
const L = (window as any).L;
|
||||
for (const el of maps) {
|
||||
const lat = Number(el.dataset.lat), lng = Number(el.dataset.lng);
|
||||
const map = L.map(el, { scrollWheelZoom: false }).setView([lat, lng], 16);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright" rel="noopener">OpenStreetMap</a> contributors' }).addTo(map);
|
||||
const icon = L.divIcon({ className: 'vm-pin', html: '<svg width="34" height="44" viewBox="0 0 34 44" aria-hidden="true"><path d="M17 0C7.6 0 0 7.6 0 17c0 12.5 17 27 17 27s17-14.5 17-27C34 7.6 26.4 0 17 0Z" fill="#4F1DA1"/><circle cx="17" cy="17" r="6.5" fill="#fff"/></svg>', iconSize: [34, 44], iconAnchor: [17, 44], popupAnchor: [0, -40] });
|
||||
L.marker([lat, lng], { icon }).addTo(map).bindPopup(`<b>${el.dataset.name ?? ''}</b>`);
|
||||
}
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
</script>
|
||||
@ -6,7 +6,15 @@
|
||||
"areaLabel": "",
|
||||
"founded": "",
|
||||
"representative": "",
|
||||
"address": { "full": "", "street": "", "locality": "", "region": "", "postalNote": "", "navigation": "", "source": "" },
|
||||
"address": {
|
||||
"full": "",
|
||||
"street": "",
|
||||
"locality": "",
|
||||
"region": "",
|
||||
"postalNote": "",
|
||||
"navigation": "",
|
||||
"source": ""
|
||||
},
|
||||
"phone": "",
|
||||
"fax": "",
|
||||
"kakao": "",
|
||||
@ -18,15 +26,28 @@
|
||||
"transitShort": "",
|
||||
"transitNote": "",
|
||||
"parking": "",
|
||||
"hours": { "source": "", "sourceLabel": "", "rows": [], "schema": [] },
|
||||
"building": { "summary": "", "floors": [], "source": "" },
|
||||
"hours": {
|
||||
"source": "",
|
||||
"sourceLabel": "",
|
||||
"rows": [],
|
||||
"schema": []
|
||||
},
|
||||
"building": {
|
||||
"summary": "",
|
||||
"floors": [],
|
||||
"source": ""
|
||||
},
|
||||
"departments": [],
|
||||
"specialties": [],
|
||||
"safety": { "items": [], "source": "" },
|
||||
"safety": {
|
||||
"items": [],
|
||||
"source": ""
|
||||
},
|
||||
"recognitions": [],
|
||||
"surfaces": {},
|
||||
"sideEffectNotice": "수술 후 개인에 따라 염증, 출혈, 신경손상 등의 부작용이 있을 수 있습니다.",
|
||||
"businessNo": "",
|
||||
"checkedAt": "",
|
||||
"nextCheck": ""
|
||||
"nextCheck": "",
|
||||
"geo": null
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// 방문 안내. 병원 정보(/clinic)가 사실 표라면 이 페이지는 "예약하고, 찾아가고, 도착해서 뭘 하는지"의 행동 순서다. 값은 모두 factSheet.json에서 가져온다.
|
||||
import Base from '../layouts/Base.astro';
|
||||
import Gallery from '../components/Gallery.astro';
|
||||
import VisitMap from '../components/VisitMap.astro';
|
||||
import { fact, site as S, SITE_NAME, clinicSchema, reservationUrl, v, has } from '../lib';
|
||||
const f = fact as Record<string, any>;
|
||||
const clinic = v(f.shortName, '병원');
|
||||
@ -31,6 +32,7 @@ const ld = [
|
||||
<p><strong>주소</strong> {v(f.address?.full)}{has(f.address?.postalNote) && <> (지번 {f.address.postalNote})</>}</p>
|
||||
<p><strong>지하철</strong> {v(f.transit)}.{has(f.transitNote) && <> {f.transitNote}</>}</p>
|
||||
<p><strong>차로 올 때</strong> {v(f.parking, `주차 안내 ${v(null)}`)}.</p>
|
||||
<VisitMap hideHead />
|
||||
{S.visitMaps?.length > 0 && <Gallery cols={2} items={S.visitMaps} />}
|
||||
|
||||
<h2>4. 도착하면</h2>
|
||||
|
||||
@ -285,6 +285,7 @@ await phase('tourism', () => {
|
||||
return { warn: true, summary: `주변 안내 수집 실패 (페이지는 확인 대기로 발행)\n${tail(r.out, 3)}` };
|
||||
}
|
||||
const en = readJson(outEn), ko = readJson(outKo);
|
||||
if (en.meta?.origin?.lat) { fact.geo = { lat: en.meta.origin.lat, lng: en.meta.origin.lng, source: en.meta.origin.coordSource || '관광공사 수집 기준 좌표' }; writeJson(join(DATA, 'factSheet.json'), fact); } // 오시는 길 지도(VisitMap)가 쓴다
|
||||
status.report = { ...(status.report ?? {}), tourism: { origin: en.meta.origin, en: { places: en.filters.total, festivals: en.festivals.length }, ko: { places: ko.filters.total, festivals: ko.festivals.length }, fetchedAt: en.meta.fetchedAt } };
|
||||
return `기준 ${en.meta.origin.lat}, ${en.meta.origin.lng} (${en.meta.origin.coordSource}) · 영문 장소 ${en.filters.total}·축제 ${en.festivals.length} · 국문 장소 ${ko.filters.total}·축제 ${ko.festivals.length}`;
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user