diff --git a/supporters/scripts/template/data/factSheet.json b/supporters/scripts/template/data/factSheet.json index 0eddd10..13e64bd 100644 --- a/supporters/scripts/template/data/factSheet.json +++ b/supporters/scripts/template/data/factSheet.json @@ -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 } diff --git a/supporters/src/components/VisitMap.astro b/supporters/src/components/VisitMap.astro new file mode 100644 index 0000000..91ee282 --- /dev/null +++ b/supporters/src/components/VisitMap.astro @@ -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; +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 && } +
+ {!compact && !hideHead &&
Directions

{T.title}

{T.sub}

} +
+
+ {geo + ? + :

{T.noMap}

} + {geo &&

{T.attribution}

} +
+
+
{T.address}
+
{address || v(null)}{address && }
+ {phone &&

{T.phone}: {phone}

} + {lang === 'ko' && has(f.transit) &&

{f.transit}

} +
+ {lang === 'en' && googleUrl && {T.google}} + {naverUrl && {T.naver}} + {kakaoUrl && {T.kakao}} +
+
+
+
+ + + + diff --git a/supporters/src/data/factSheet.json b/supporters/src/data/factSheet.json index 490910f..5516b5d 100644 --- a/supporters/src/data/factSheet.json +++ b/supporters/src/data/factSheet.json @@ -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" + } } diff --git a/supporters/src/pages/visit.astro b/supporters/src/pages/visit.astro index e61e25a..9e2b138 100644 --- a/supporters/src/pages/visit.astro +++ b/supporters/src/pages/visit.astro @@ -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; const clinic = v(f.shortName, '병원'); @@ -31,6 +32,7 @@ const ld = [

주소 {v(f.address?.full)}{has(f.address?.postalNote) && <> (지번 {f.address.postalNote})}

지하철 {v(f.transit)}.{has(f.transitNote) && <> {f.transitNote}}

차로 올 때 {v(f.parking, `주차 안내 ${v(null)}`)}.

+ {S.visitMaps?.length > 0 && }

4. 도착하면

diff --git a/templates/supporters-astro/src/components/VisitMap.astro b/templates/supporters-astro/src/components/VisitMap.astro new file mode 100644 index 0000000..91ee282 --- /dev/null +++ b/templates/supporters-astro/src/components/VisitMap.astro @@ -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; +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 && } +
+ {!compact && !hideHead &&
Directions

{T.title}

{T.sub}

} +
+
+ {geo + ? + :

{T.noMap}

} + {geo &&

{T.attribution}

} +
+
+
{T.address}
+
{address || v(null)}{address && }
+ {phone &&

{T.phone}: {phone}

} + {lang === 'ko' && has(f.transit) &&

{f.transit}

} +
+ {lang === 'en' && googleUrl && {T.google}} + {naverUrl && {T.naver}} + {kakaoUrl && {T.kakao}} +
+
+
+
+ + + + diff --git a/templates/supporters-astro/src/data/factSheet.json b/templates/supporters-astro/src/data/factSheet.json index 0eddd10..13e64bd 100644 --- a/templates/supporters-astro/src/data/factSheet.json +++ b/templates/supporters-astro/src/data/factSheet.json @@ -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 } diff --git a/templates/supporters-astro/src/pages/visit.astro b/templates/supporters-astro/src/pages/visit.astro index e61e25a..9e2b138 100644 --- a/templates/supporters-astro/src/pages/visit.astro +++ b/templates/supporters-astro/src/pages/visit.astro @@ -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; const clinic = v(f.shortName, '병원'); @@ -31,6 +32,7 @@ const ld = [

주소 {v(f.address?.full)}{has(f.address?.postalNote) && <> (지번 {f.address.postalNote})}

지하철 {v(f.transit)}.{has(f.transitNote) && <> {f.transitNote}}

차로 올 때 {v(f.parking, `주차 안내 ${v(null)}`)}.

+ {S.visitMaps?.length > 0 && }

4. 도착하면

diff --git a/workers/supporters-build/run.mjs b/workers/supporters-build/run.mjs index 4f275d7..b424b04 100644 --- a/workers/supporters-build/run.mjs +++ b/workers/supporters-build/run.mjs @@ -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}`; });