o2o-infinith-demo/supporters/src/pages/en/stay.astro
Haewon Kam a807d5dd9c refactor(supporters): 의료관광 연결을 /discovery 에서 서포터즈 사이트 /en/stay 로 옮김
붙일 자리를 잘못 잡았다. /discovery 는 INFINITH 제품의 진단 화면이고 병원마다 쓰는
범용 도구다. 외국인 환자가 보는 페이지는 병원 서포터즈 사이트다. 그쪽으로 옮겼다.

옮긴 것
- src/components/discovery/MedicalTourismPanel.tsx, src/types/medicalTourism.ts,
  src/data/medicalTourism.json 삭제. DiscoveryReportPage 원복.
- supporters/src/pages/en/stay.astro 신설. 독립 URL 과 자체 WebPage JSON-LD 를 가져
  AI 인용 단위가 된다. sitemap 에 등록하고 /visit 에서 hreflang 링크로 잇는다.
- 수집 스크립트 출력을 supporters/src/data/medicalTourism.json 으로 바꿨다.

영문 페이지 지원
- Base.astro 에 lang prop 추가(기본 ko). html lang · og:locale · 머리말 · 꼬리말이
  따라 바뀐다. 레이아웃을 하나로 유지해 한국어 페이지 동작은 그대로다.
- 의료광고 고지(지원 관계·부작용)는 규정 대상이라 임의로 번역하지 않는다. 병원이
  sponsorNoticeEn / sideEffectNoticeEn 을 확정하기 전에는 한국어 원문을 그대로 싣고
  영문 문구가 확인 대기임을 밝힌다.
- factSheet 에 nameEn / shortNameEn / shortNameEnSource 추가. 뷰성형외과 값의 근거는
  병원 자신의 영문 도메인 viewplasticsurgery.com 이며 한국관광공사 등재명도 같다.

페이지가 만들지 않는 것
- 평점·리뷰·영업시간: TourAPI 미제공이라 싣지 않는다.
- 식당의 회복 적합성: 의학 판단이라 병원이 확정한 영문 문구(site.json dietGuideEn)만
  싣는다. 확인 전에는 어떤 식당도 적합으로 표시하지 않는다고 페이지에 적는다.
- 회복 단계 일수: 병원 확정값(recoveryStagesEn). 단계는 이동 거리로만 정의한다.
- 하단에 이 페이지가 답하지 못하는 것 네 가지를 그대로 적었다.

템플릿 동기화
- templates/supporters-astro 에 같은 변경을 넣되 데이터는 빈 스캐폴드로 둔다.
  뷰성형외과 factSheet 와 관광 데이터가 새 병원 빌드에 새어 들어가면 안 된다.
  빈 데이터로 빌드해 6개 범주가 "No English listings" 로 나오는 것까지 확인했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 11:42:46 +09:00

118 lines
7.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
// 외국인 환자용 체류 안내. 병원 좌표를 중심으로 숙박·식사·회복·관광·행사를 이동 반경 순으로 놓는다.
// 데이터는 한국관광공사 TourAPI 영문(EngService2)에서 scripts/fetch_medical_tourism.py 가 미리 수집한
// src/data/medicalTourism.json 이다. 값을 만들지 않는다.
// - 평점·리뷰·영업시간은 TourAPI 가 주지 않아 싣지 않는다.
// - 수술 전후 식이 적합성은 의학 판단이라 병원이 확정한 문구만 싣는다(site.json dietGuideEn).
// - 회복 단계가 며칠째인지도 병원 확정값이다(site.json recoveryStagesEn). 단계는 이동 거리로만 정의한다.
import Base from '../../layouts/Base.astro';
import { fact, site as S, SITE_NAME, clinicSchema, reservationUrl, has } from '../../lib';
import data from '../../data/medicalTourism.json';
const f = fact as Record<string, any>;
const Sx = S as Record<string, any>;
const clinic = f.shortNameEn || f.nameEn || f.shortName || 'the clinic';
const site = Astro.site!.toString().replace(/\/$/, '');
const T = data as any;
const GROUPS: Array<{ key: string; label: string; blurb: string }> = [
{ key: 'stay', label: 'Where to stay', blurb: 'Hotels and guesthouses near the clinic, for the days when visits are frequent.' },
{ key: 'restaurant', label: 'Where to eat', blurb: 'Restaurants within walking or short-ride distance.' },
{ key: 'wellness', label: 'Rest and recovery', blurb: 'Spas and jjimjilbang listed by the Korea Tourism Organization.' },
{ key: 'attraction', label: 'Places to see', blurb: 'Sights within the city, once you are able to travel further.' },
{ key: 'culture', label: 'Culture', blurb: 'Museums, theatres and halls.' },
{ key: 'shopping', label: 'Shopping', blurb: 'Department stores, markets and streets.' },
];
const dist = (m: number | null) => (m === null ? '' : m < 1000 ? `${m} m` : `${(m / 1000).toFixed(1)} km`);
const dt = (d: string | null) => (d && d.length === 8 ? `${d.slice(0, 4)}.${d.slice(4, 6)}.${d.slice(6, 8)}` : '');
// 병원이 확정하기 전에는 비운다. 임의로 채우지 않는다.
const dietGuide: string = Sx.dietGuideEn || '';
const recoveryStages: string = Sx.recoveryStagesEn || '';
const ld = [
{ '@type': 'WebPage', '@id': `${site}/en/stay#page`, name: `Your stay near ${clinic}`, url: `${site}/en/stay`, inLanguage: 'en', isPartOf: { '@id': `${site}/#website` }, about: { '@id': `${f.url}/#clinic` }, publisher: { '@type': 'Organization', name: SITE_NAME, url: site } },
clinicSchema(site),
];
---
<Base
lang="en"
title={`Your stay near ${clinic} — hotels, food, recovery, and what to see`}
description={`Practical guide for international patients: where to stay, eat and rest near ${clinic} in Gangnam, Seoul, plus sights and festivals, listed by distance from the clinic. Source: Korea Tourism Organization.`}
jsonLd={ld}
>
<article class="wrap" style="padding-bottom:4rem">
<header class="article-head">
<div class="eyebrow">For international patients</div>
<h1 class="serif">Your Stay in Seoul</h1>
<p style="color:var(--slate-600);margin-top:0.6rem">
<strong style="color:var(--primary-900)">Where to stay, eat and rest around {clinic}.</strong>
Everything below is listed by distance from the clinic at {f.address?.fullEn || f.address?.full}.
Addresses and names come from the Korea Tourism Organization, not from us.
For the clinic's own address, hours and phone, see <a href="/clinic">Clinic information</a>.
</p>
</header>
<h2>Before you use this page</h2>
<div class="table-wrap"><table><tbody>
<tr><th style="width:32%">Reference point</th><td>{clinic} — {T.meta.origin.lat}, {T.meta.origin.lng}</td></tr>
<tr><th>Source</th><td>Korea Tourism Organization, TourAPI EngService2 — collected {T.meta.fetchedAt.slice(0, 10)}</td></tr>
<tr><th>Search radius</th><td>Stay and food 5 km · sights, culture, shopping 10 km · festivals {T.meta.festivalRadiusKm} km</td></tr>
</tbody></table></div>
<h2>Planning your days</h2>
<p>
How many days each stage lasts depends on your procedure, and only your doctor can tell you that.
The stages below are defined by <strong>distance from the clinic</strong>, nothing else.
</p>
{recoveryStages
? <p>{recoveryStages}</p>
: <p class="pending-note">The day-by-day schedule is pending confirmation by {clinic}. Ask at your consultation.</p>}
<div class="table-wrap"><table><thead><tr><th>Stage</th><th>Distance from clinic</th><th>What fits here</th></tr></thead><tbody>
<tr><td>Frequent visits</td><td>within 1 km</td><td>Hotels and food you can reach on foot</td></tr>
<tr><td>Short outings</td><td>1 3 km</td><td>Restaurants, spas</td></tr>
<tr><td>Around the city</td><td>3 10 km</td><td>Sights, culture, shopping</td></tr>
</tbody></table></div>
{GROUPS.map((g) => {
const items = (T.places[g.key] ?? []) as any[];
return (
<section>
<h2>{g.label}</h2>
<p>{g.blurb}</p>
{g.key === 'restaurant' && (
dietGuide
? <p><strong>What {clinic} advises after surgery.</strong> {dietGuide}</p>
: <p class="pending-note">What you may eat after surgery is a medical question. {clinic} has not yet published English guidance, so we do not mark any restaurant as suitable. Please ask at your consultation.</p>
)}
{items.length === 0
? <p class="pending-note">No English listings within this radius.</p>
: <div class="table-wrap"><table><thead><tr><th style="width:44%">Name</th><th style="width:14%">Distance</th><th>Address</th></tr></thead><tbody>
{items.map((p) => <tr><td>{p.title}</td><td>{dist(p.distanceM)}</td><td>{p.address}</td></tr>)}
</tbody></table></div>}
</section>
);
})}
<h2>Festivals and events while you are here</h2>
<p>Currently running or starting soon, within {T.meta.festivalRadiusKm} km of the clinic.</p>
{T.festivals.length === 0
? <p class="pending-note">No listings.</p>
: <div class="table-wrap"><table><thead><tr><th style="width:40%">Event</th><th style="width:22%">Dates</th><th style="width:12%">Distance</th><th>Where</th></tr></thead><tbody>
{T.festivals.map((e: any) => <tr><td>{e.title}</td><td>{dt(e.startDate)} {dt(e.endDate)}</td><td>{e.distanceKm} km</td><td>{e.address}</td></tr>)}
</tbody></table></div>}
<h2>What this page does not tell you</h2>
<ul>
<li>No ratings, review counts or opening hours. The tourism data does not carry them, and we do not guess.</li>
<li>No claim that any restaurant suits your recovery. That is for {clinic} to say.</li>
<li>English tourism listings around Gangnam are thin. Some categories hold only a handful of entries.</li>
<li>We are not a travel agency and take no booking or commission. Links and names are for reference.</li>
</ul>
<p style="margin-top:2rem">{has(reservationUrl) && <a class="btn primary" href={reservationUrl} rel="noopener">Book a consultation with {clinic}</a>}</p>
<p class="disclosure">{f.sideEffectNoticeEn || f.sideEffectNotice}</p>
</article>
</Base>