feat(supporters): 영어 화면의 병원 고유 사실을 데이터에서 읽도록 바꾸고 템플릿 재생성
templates/supporters-astro 는 supporters/ 에서 만들어지는 파생물인데, 정본이 된 supporters/ 코드에 뷰 고유 사실이 문장으로 박혀 있었다. 그대로 내보내면 다른 병원 사이트에 틀린 사실이 나간다. 빈 데이터로 빌드해 보니 실제로 "Founded 확인 대기 · Sinnonhyeon Station Exit 3" 가 나왔다. 데이터로 바꾼 곳 (전부 factSheetEn.json 에 이미 있던 값이다) - 히어로 이미지 alt: site.json 의 heroImageAltEn 을 읽고, 없으면 한국어 alt 를 쓴다. - 히어로 칩: 설립연도와 위치를 데이터에 있는 것만 잇는다. 둘 다 없으면 칩을 내지 않는다. - 방문 카드 요약: factSheetEn 의 airportShort 가 있을 때만 경로를 적는다. - 병원 카드 요약: 설립연도와 factSheetEn 의 building 첫 문장을 데이터에서 잇는다. - 공항 카드: factSheetEn 의 airport 가 있을 때만 카드를 낸다. - 영문 플래너 병원명: factSheetEn 의 shortName 을 읽고, 영문 표기가 없는 병원은 한국어 상호를 쓴다. - 404 안내: 상호를 factSheet 에서 읽는다. 템플릿 빈 데이터셋에 factSheetEn.json 이 없어서 뷰의 영문 사실이 통째로 남고 있었다. 빈 factSheetEn.json 을 만들고 site.json 에 heroImageAltEn 자리를 넣었다. export_template.mjs 의 누락 검사가 상호만 찾아 지명으로 적힌 사실을 놓쳤다. 지명·건물 표현을 패턴에 넣고, 문장을 그리는 코드(src/pages·layouts·components·lib)만 내보내기를 멈추게 하고 데이터·빌드 도구는 알림만 하도록 나눴다. 빌드 도구의 기본 인자 때문에 검사가 늘 실패해 신호가 되지 못하던 문제도 함께 풀린다. 검증 - 빈 데이터 템플릿 빌드 14페이지, 영어 홈에 병원 고유 사실 0건, 사실을 말하던 조각은 생략되고 요약문만 일반 문장으로 남는다. - 뷰 사이트 빌드 32페이지, 영어 홈의 칩·공항 카드·요약문이 모두 그대로 나온다. - 게이트 41/41 통과, check.mjs 오류 0건(경고 23건은 기존 건). - 루트 tsc --noEmit 0 에러, 내보내기 종료코드 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f01bc6cafc
commit
e60ef04503
@ -45,9 +45,17 @@ pkg.name = 'supporters-template';
|
||||
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
|
||||
// 남은 병원 고유 문자열 검사
|
||||
const CLINIC_RE = /뷰성형외과|viewclinic|ViewclinicKR|안성민|o2oteam/;
|
||||
// 상호뿐 아니라 지명·건물 같은 병원 고유 사실도 잡는다. 상호만 찾으면
|
||||
// "Sinnonhyeon Station Exit 3" 처럼 지명으로 적힌 사실이 그대로 템플릿에 남는다.
|
||||
const CLINIC_RE = /뷰성형외과|viewclinic|ViewclinicKR|안성민|o2oteam|View Plastic|Sinnonhyeon|Bongeunsa|Gimpo|신논현|봉은사|19-storey/;
|
||||
const leaks = [];
|
||||
const walk = (d) => { for (const f of readdirSync(d, { withFileTypes: true })) { const p = join(d, f.name); if (f.isDirectory()) { if (f.name !== 'node_modules') walk(p); } else if (/\.(astro|ts|mjs|json|css)$/.test(f.name) && !p.endsWith('export_template.mjs') && !p.includes('/scripts/gate/fixtures/') && CLINIC_RE.test(readFileSync(p, 'utf8'))) leaks.push(relative(DEST, p)); } };
|
||||
walk(DEST);
|
||||
if (leaks.length) { console.error('병원 고유 문자열이 남은 파일:\n ' + leaks.join('\n ')); process.exit(1); }
|
||||
// 문장을 그리는 코드에 병원 고유 사실이 박히면 다른 병원 화면에 틀린 말이 나가므로 내보내기를 멈춘다.
|
||||
// 데이터(src/data)는 워커가 병원마다 갈아끼우고 빌드 도구(scripts)는 고객이 보지 않으므로 알림만 한다.
|
||||
const RENDER_RE = /^src\/(pages|layouts|components|lib)\//;
|
||||
const blocking = leaks.filter((f) => RENDER_RE.test(f));
|
||||
const notes = leaks.filter((f) => !RENDER_RE.test(f));
|
||||
if (notes.length) console.log('참고 · 병원 이름이 남은 파일(고객 화면 아님):\n ' + notes.join('\n '));
|
||||
if (blocking.length) { console.error('고객 화면 코드에 병원 고유 사실이 남았습니다. 데이터에서 읽도록 고치세요:\n ' + blocking.join('\n ')); process.exit(1); }
|
||||
console.log(`템플릿 내보내기 완료: ${DEST}${existsSync(join(DEST, 'node_modules')) ? '' : ' (npm ci 필요)'}`);
|
||||
|
||||
25
supporters/scripts/template/data/factSheetEn.json
Normal file
25
supporters/scripts/template/data/factSheetEn.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"_comment": "영어 페이지용 병원 사실. 값의 원본은 factSheet.json 이고 여기에는 영어 표기만 둔다. 비어 있는 값은 영어 화면에서 해당 조각을 렌더하지 않는다. 지어내지 않는다.",
|
||||
"name": "",
|
||||
"shortName": "",
|
||||
"kind": "",
|
||||
"areaLabel": "",
|
||||
"address": {
|
||||
"full": "",
|
||||
"mapQuery": ""
|
||||
},
|
||||
"transit": "",
|
||||
"airport": "",
|
||||
"airportShort": "",
|
||||
"parking": "",
|
||||
"hoursDays": {
|
||||
"월·화·수·목": "",
|
||||
"금": "",
|
||||
"토": "",
|
||||
"일·공휴일": ""
|
||||
},
|
||||
"hoursClosed": "",
|
||||
"building": "",
|
||||
"arrival": "",
|
||||
"specialties": []
|
||||
}
|
||||
@ -23,5 +23,6 @@
|
||||
"newsOutletsExample": "",
|
||||
"sponsorNotice": "",
|
||||
"ga4MeasurementId": "",
|
||||
"_comment_ga4": "GA4 측정 ID(G-XXXX). 비우면 태그를 넣지 않는다. 키 이벤트 이름은 data/ai_channels.json conversion_events 와 같다."
|
||||
"_comment_ga4": "GA4 측정 ID(G-XXXX). 비우면 태그를 넣지 않는다. 키 이벤트 이름은 data/ai_channels.json conversion_events 와 같다.",
|
||||
"heroImageAltEn": ""
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
},
|
||||
"transit": "Line 9, Sinnonhyeon Station, Exit 3. Within 50 m of the exit. The whole building is the clinic, so you can see it from the exit.",
|
||||
"airport": "From Incheon Airport: take the AREX airport railroad to Gimpo Airport Station, then Line 9 express to Sinnonhyeon Station with no transfer. About 60 to 70 minutes. A taxi takes 60 to 90 minutes depending on traffic.",
|
||||
"airportShort": "Line 9 from Gimpo Airport with no transfer",
|
||||
"parking": "The street in front of the main entrance is one-way. Enter from the pharmacy alley and turn right at the crossroads. The car park is on the left.",
|
||||
"hoursDays": {
|
||||
"월·화·수·목": "Mon to Thu",
|
||||
|
||||
@ -6,44 +6,149 @@
|
||||
"tagline": "뷰성형외과를 알아보는 사람들의 질문에 먼저 답하는 서포터즈 매체 (샘플)",
|
||||
"heroTitle": "궁금한 것부터 답합니다.",
|
||||
"editorId": "ansm",
|
||||
"logo": { "src": "/img/view-logo.png", "alt": "뷰성형외과 View Plastic Surgery", "width": 184, "height": 66 },
|
||||
"logoFoot": { "src": "/img/view-logo-foot.png", "alt": "뷰성형외과", "width": 138, "height": 49 },
|
||||
"heroImage": { "src": "/img/clinic/hero-collage.jpg", "alt": "뷰성형외과 빌딩 외관과 내부 공간 콜라주. 상담센터, 수술실, 수술센터 복도", "width": 1000, "height": 850 },
|
||||
"buildingImage": { "src": "/img/clinic/building-clean.jpg", "alt": "뷰성형외과 빌딩 외관. 유리 외벽에 VIEW 로고가 있는 고층 건물", "caption": "뷰성형외과 빌딩 외관. 출처: 공식 홈페이지 병원둘러보기" },
|
||||
"logo": {
|
||||
"src": "/img/view-logo.png",
|
||||
"alt": "뷰성형외과 View Plastic Surgery",
|
||||
"width": 184,
|
||||
"height": 66
|
||||
},
|
||||
"logoFoot": {
|
||||
"src": "/img/view-logo-foot.png",
|
||||
"alt": "뷰성형외과",
|
||||
"width": 138,
|
||||
"height": 49
|
||||
},
|
||||
"heroImage": {
|
||||
"src": "/img/clinic/hero-collage.jpg",
|
||||
"alt": "뷰성형외과 빌딩 외관과 내부 공간 콜라주. 상담센터, 수술실, 수술센터 복도",
|
||||
"width": 1000,
|
||||
"height": 850
|
||||
},
|
||||
"buildingImage": {
|
||||
"src": "/img/clinic/building-clean.jpg",
|
||||
"alt": "뷰성형외과 빌딩 외관. 유리 외벽에 VIEW 로고가 있는 고층 건물",
|
||||
"caption": "뷰성형외과 빌딩 외관. 출처: 공식 홈페이지 병원둘러보기"
|
||||
},
|
||||
"imageCredit": "사진 출처: 뷰성형외과 공식 홈페이지 병원둘러보기",
|
||||
"homeVideos": [
|
||||
{ "id": "Nbemu6mL_uY", "title": "가장 VIEW다운 가치 : 안전과 신뢰의 약속", "published": "2026-05-19" },
|
||||
{ "id": "_OZRSJIDWgs", "title": "스포르자가 또 방문한 이유, 모티바 프리저베 교육 현장", "published": "2026-08-20" },
|
||||
{ "id": "xohC5C6aY70", "title": "모티바 프리저베는 수술 직후 만세가 가능하다?", "published": "2026-08-31" }
|
||||
{
|
||||
"id": "Nbemu6mL_uY",
|
||||
"title": "가장 VIEW다운 가치 : 안전과 신뢰의 약속",
|
||||
"published": "2026-05-19"
|
||||
},
|
||||
{
|
||||
"id": "_OZRSJIDWgs",
|
||||
"title": "스포르자가 또 방문한 이유, 모티바 프리저베 교육 현장",
|
||||
"published": "2026-08-20"
|
||||
},
|
||||
{
|
||||
"id": "xohC5C6aY70",
|
||||
"title": "모티바 프리저베는 수술 직후 만세가 가능하다?",
|
||||
"published": "2026-08-31"
|
||||
}
|
||||
],
|
||||
"insideSummary": "19층 자체 사옥. 상담 4~5층, 검진 지하 1층, 수술 11~14층, 입원 8·10층.",
|
||||
"insideGallery": [
|
||||
{ "src": "/img/clinic/consult-1.jpg", "alt": "뷰성형외과 4~5층 상담센터 대기 공간. 검은 소파와 밝은 조명", "caption": "4~5F 상담센터" },
|
||||
{ "src": "/img/clinic/exam-1.jpg", "alt": "뷰성형외과 지하 1층 검진센터 입구. Examination Center 간판", "caption": "B1F 검진센터" },
|
||||
{ "src": "/img/clinic/surgery-2.jpg", "alt": "뷰성형외과 수술실 내부. 수술대와 무영등", "caption": "11~14F 수술센터" },
|
||||
{ "src": "/img/clinic/ward-1.jpg", "alt": "뷰성형외과 입원실. 창가의 1인 침대", "caption": "8·10F 입원센터" },
|
||||
{ "src": "/img/clinic/vip-lounge.jpg", "alt": "뷰성형외과 VIP 라운지. 어두운 톤의 라운지 공간", "caption": "VIP 라운지" },
|
||||
{ "src": "/img/clinic/dental-15f.jpg", "alt": "뷰성형외과 15층 치과 진료실. 파란 치과 의자가 줄지어 있음", "caption": "15F 치과 (양악·윤곽 협진)" }
|
||||
{
|
||||
"src": "/img/clinic/consult-1.jpg",
|
||||
"alt": "뷰성형외과 4~5층 상담센터 대기 공간. 검은 소파와 밝은 조명",
|
||||
"caption": "4~5F 상담센터"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/exam-1.jpg",
|
||||
"alt": "뷰성형외과 지하 1층 검진센터 입구. Examination Center 간판",
|
||||
"caption": "B1F 검진센터"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/surgery-2.jpg",
|
||||
"alt": "뷰성형외과 수술실 내부. 수술대와 무영등",
|
||||
"caption": "11~14F 수술센터"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/ward-1.jpg",
|
||||
"alt": "뷰성형외과 입원실. 창가의 1인 침대",
|
||||
"caption": "8·10F 입원센터"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/vip-lounge.jpg",
|
||||
"alt": "뷰성형외과 VIP 라운지. 어두운 톤의 라운지 공간",
|
||||
"caption": "VIP 라운지"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/dental-15f.jpg",
|
||||
"alt": "뷰성형외과 15층 치과 진료실. 파란 치과 의자가 줄지어 있음",
|
||||
"caption": "15F 치과 (양악·윤곽 협진)"
|
||||
}
|
||||
],
|
||||
"clinicGallery": [
|
||||
{ "src": "/img/clinic/consult-2.jpg", "alt": "상담센터 상담실 내부", "caption": "4~5F 상담센터" },
|
||||
{ "src": "/img/clinic/exam-2.jpg", "alt": "검진센터 접수 데스크", "caption": "B1F 검진센터" },
|
||||
{ "src": "/img/clinic/surgery-3.jpg", "alt": "수술실 앞 복도의 수술 실명제 안내 화면", "caption": "11~14F 수술센터 복도" },
|
||||
{ "src": "/img/clinic/ward-2.jpg", "alt": "입원실 침대", "caption": "8·10F 입원센터" },
|
||||
{ "src": "/img/clinic/vip-1.jpg", "alt": "VIP 병동 복도", "caption": "7F VIP 입원실" },
|
||||
{ "src": "/img/clinic/consult-3.jpg", "alt": "상담 대기 라운지", "caption": "상담 대기 라운지" }
|
||||
{
|
||||
"src": "/img/clinic/consult-2.jpg",
|
||||
"alt": "상담센터 상담실 내부",
|
||||
"caption": "4~5F 상담센터"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/exam-2.jpg",
|
||||
"alt": "검진센터 접수 데스크",
|
||||
"caption": "B1F 검진센터"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/surgery-3.jpg",
|
||||
"alt": "수술실 앞 복도의 수술 실명제 안내 화면",
|
||||
"caption": "11~14F 수술센터 복도"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/ward-2.jpg",
|
||||
"alt": "입원실 침대",
|
||||
"caption": "8·10F 입원센터"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/vip-1.jpg",
|
||||
"alt": "VIP 병동 복도",
|
||||
"caption": "7F VIP 입원실"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/consult-3.jpg",
|
||||
"alt": "상담 대기 라운지",
|
||||
"caption": "상담 대기 라운지"
|
||||
}
|
||||
],
|
||||
"clinicDoctors": [
|
||||
"dr-choi",
|
||||
"dr-chung",
|
||||
"dr-son",
|
||||
"dr-oh"
|
||||
],
|
||||
"clinicDoctors": ["dr-choi", "dr-chung", "dr-son", "dr-oh"],
|
||||
"visitMaps": [
|
||||
{ "src": "/img/clinic/map-1.jpg", "alt": "양재·강남역 방향에서 뷰성형외과로 오는 약도. 9호선 신논현역과 병원 위치 표시", "caption": "양재·강남역 방향 약도. 출처: 홈페이지 오시는길" },
|
||||
{ "src": "/img/clinic/parking-map.jpg", "alt": "뷰성형외과 주차장 진입 경로 약도. 약국 골목으로 진입해 사거리에서 우회전", "caption": "주차장 진입 경로. 출처: 홈페이지 오시는길" }
|
||||
{
|
||||
"src": "/img/clinic/map-1.jpg",
|
||||
"alt": "양재·강남역 방향에서 뷰성형외과로 오는 약도. 9호선 신논현역과 병원 위치 표시",
|
||||
"caption": "양재·강남역 방향 약도. 출처: 홈페이지 오시는길"
|
||||
},
|
||||
{
|
||||
"src": "/img/clinic/parking-map.jpg",
|
||||
"alt": "뷰성형외과 주차장 진입 경로 약도. 약국 골목으로 진입해 사거리에서 우회전",
|
||||
"caption": "주차장 진입 경로. 출처: 홈페이지 오시는길"
|
||||
}
|
||||
],
|
||||
"arrivalGuide": "먼저 1층 안내데스크로 가면 안내해 줍니다. 상담센터는 4~5층, VIP 상담실은 3층, 수술 전 검진은 지하 1층 검진센터입니다.",
|
||||
"visitReads": [
|
||||
{ "href": "/posts/visit-guide", "title": "방문 가이드: 위치·진료시간·주차·예약", "note": "대중교통·공항에서 오는 법까지 자세히" },
|
||||
{ "href": "/posts/safety-system", "title": "뷰성형외과 안전 시스템은 무엇으로 이루어져 있나요?", "note": "상담에서 확인할 네 가지" },
|
||||
{ "href": "/posts/revision-checklist", "title": "가슴 재수술을 고민 중이라면 무엇을 먼저 확인해야 하나요?", "note": "재수술 상담 준비 체크리스트" }
|
||||
{
|
||||
"href": "/posts/visit-guide",
|
||||
"title": "방문 가이드: 위치·진료시간·주차·예약",
|
||||
"note": "대중교통·공항에서 오는 법까지 자세히"
|
||||
},
|
||||
{
|
||||
"href": "/posts/safety-system",
|
||||
"title": "뷰성형외과 안전 시스템은 무엇으로 이루어져 있나요?",
|
||||
"note": "상담에서 확인할 네 가지"
|
||||
},
|
||||
{
|
||||
"href": "/posts/revision-checklist",
|
||||
"title": "가슴 재수술을 고민 중이라면 무엇을 먼저 확인해야 하나요?",
|
||||
"note": "재수술 상담 준비 체크리스트"
|
||||
}
|
||||
],
|
||||
"newsNote": "뷰성형외과 홈페이지 언론보도 게시판과 네이버 뉴스에서 모은 기사입니다(2026-09-04 기준). 병원이 알리거나 원장이 설명한 기사만 모았고, 그 밖의 보도는 포함하지 않았습니다. 기사 본문은 원문에서 읽을 수 있으며, 원문이 사라진 기사는 병원 게시판 사본으로 연결됩니다.",
|
||||
"newsOutletsExample": "메디컬투데이, 전민일보, 아주경제, 머니S, 뉴시스 등"
|
||||
"newsOutletsExample": "메디컬투데이, 전민일보, 아주경제, 머니S, 뉴시스 등",
|
||||
"heroImageAltEn": "View Plastic Surgery building exterior and interior collage: consultation center, operating room, surgery center corridor"
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
// 없는 주소. Vercel 이 dist/404.html 을 자동으로 쓴다. 옛 주소(/recovery, /stay)는 vercel.json 리다이렉트가 /plan 으로 먼저 보낸다.
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { SITE_NAME } from '../lib';
|
||||
import { SITE_NAME, fact, v } from '../lib';
|
||||
const f = fact as Record<string, any>;
|
||||
const site = Astro.site!.toString().replace(/\/$/, '');
|
||||
const ld = { '@type': 'WebPage', '@id': `${site}/404#page`, name: '페이지를 찾을 수 없음', url: `${site}/404`, inLanguage: 'ko', isPartOf: { '@id': `${site}/#website` }, publisher: { '@type': 'Organization', name: SITE_NAME, url: site } };
|
||||
---
|
||||
@ -9,7 +10,7 @@ const ld = { '@type': 'WebPage', '@id': `${site}/404#page`, name: '페이지를
|
||||
<section class="section dark" style="padding:5rem 0 4.5rem"><div class="wrap-wide">
|
||||
<div class="sec-head"><div class="eyebrow" style="color:var(--purple-300)">404</div><h1 class="serif" style="font-size:2rem;display:inline-block;background:linear-gradient(to right,var(--purple-300),var(--blue-300));-webkit-background-clip:text;background-clip:text;color:transparent">Page Not Found</h1><p class="sub">주소가 바뀌었거나 없는 페이지입니다. 아래에서 이어서 보실 수 있습니다.</p></div>
|
||||
<div class="grid">
|
||||
<a class="card" href="/" style="text-decoration:none"><div class="cat">Home</div><h3>홈</h3><p>뷰성형외과를 알아보는 분들의 질문과 답</p></a>
|
||||
<a class="card" href="/" style="text-decoration:none"><div class="cat">Home</div><h3>홈</h3><p>{v(f.shortName, '병원')} 정보를 알아보는 분들의 질문과 답</p></a>
|
||||
<a class="card" href="/plan" style="text-decoration:none"><div class="cat">Recovery Planner</div><h3>회복 일정</h3><p>수술일 기준 입국·출국 계산과 회복기 외출 계획. 영어 화면은 /en/plan</p></a>
|
||||
<a class="card" href="/visit" style="text-decoration:none"><div class="cat">Visit</div><h3>방문 안내</h3><p>예약, 가는 길, 주차, 도착 후 동선</p></a>
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,24 @@ import VisitMap from '../../components/VisitMap.astro';
|
||||
const f = fact as Record<string, any>;
|
||||
const site = Astro.site!.toString().replace(/\/$/, '');
|
||||
const yt = surface('youtube'); const gu = surface('gangnamunni'); const gg = surface('google');
|
||||
// 카드 요약문은 factSheetEn.json 에서만 만든다. 병원마다 다른 사실(공항 경로·건물)을 문장에 적어 두면
|
||||
// 다른 병원 사이트에 그대로 나가 틀린 말이 된다. 값이 없으면 그 조각을 빼고 일반 문장만 남긴다.
|
||||
const airportShort = String((EN as Record<string, any>).airportShort || '');
|
||||
const buildingShort = String((EN as Record<string, any>).building || '').split('. ')[0];
|
||||
const visitCard = airportShort
|
||||
? `${airportShort}, opening hours by day, and where to go when you arrive.`
|
||||
: 'How to reach the clinic, opening hours by day, and where to go when you arrive.';
|
||||
const clinicCard = [
|
||||
has(f.founded) && `Founded ${f.founded}`,
|
||||
buildingShort || null,
|
||||
'specialties, and where reviews can be read',
|
||||
].filter(Boolean).join(', ') + '.';
|
||||
const heroAlt = String((S as Record<string, any>).heroImageAltEn || S.heroImage?.alt || '');
|
||||
// 히어로 칩. 설립연도와 위치는 병원마다 다르므로 데이터에 있는 것만 잇는다. 둘 다 없으면 칩을 내지 않는다.
|
||||
const chipFacts = [
|
||||
has(f.founded) && `Founded ${f.founded}`,
|
||||
has((EN as Record<string, any>).areaLabel) && String((EN as Record<string, any>).areaLabel),
|
||||
].filter(Boolean).join(' · ');
|
||||
const hoursDays = EN.hoursDays as Record<string, string>;
|
||||
const ld = [
|
||||
{ '@type': 'WebPage', '@id': `${site}/en#page`, name: `${EN.shortName} Supporters (English)`, url: `${site}/en`, inLanguage: 'en', isPartOf: { '@id': `${site}/#website` }, about: { '@id': `${fact.url}/#clinic` } },
|
||||
@ -23,7 +41,7 @@ const ld = [
|
||||
<h1>{EN.shortName},<br /><span class="accent">plan your stay before you fly.</span></h1>
|
||||
<p class="lede">{EN.shortName} is a {EN.kind} at {EN.areaLabel}{has(f.founded) && <>, founded in {f.founded}</>}. This site is run by supporters who answer the questions people ask before visiting. In English you can plan your recovery stay, find the way here and check clinic facts. Articles are in Korean.</p>
|
||||
<div class="chips">
|
||||
<span class="chip"><span class="dot"></span>Founded {v(f.founded)} · Sinnonhyeon Station Exit 3</span>
|
||||
{chipFacts && <span class="chip"><span class="dot"></span>{chipFacts}</span>}
|
||||
<span class="chip"><span class="dot"></span>Based on the clinic's published guidance</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.8rem;flex-wrap:wrap;margin-top:1.6rem">
|
||||
@ -31,7 +49,7 @@ const ld = [
|
||||
<a class="btn secondary" href="#visit">Getting here</a>
|
||||
</div>
|
||||
</div>
|
||||
{has(S.heroImage?.src) && <div class="hero-img collage"><img src={S.heroImage.src} alt="View Plastic Surgery building exterior and interior collage: consultation center, operating room, surgery center corridor" width={S.heroImage.width} height={S.heroImage.height} /></div>}
|
||||
{has(S.heroImage?.src) && <div class="hero-img collage"><img src={S.heroImage.src} alt={heroAlt} width={S.heroImage.width} height={S.heroImage.height} /></div>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -39,8 +57,8 @@ const ld = [
|
||||
<div class="sec-head"><div class="eyebrow">Start Here</div><h2>Where Are You Now?</h2><p class="sub">Pick the step that matches where you are.</p></div>
|
||||
<div class="grid">
|
||||
<a class="card entry" href="/en/plan"><div class="cat">Surgery date set</div><h3>Plan arrival, departure and recovery days</h3><p>Choose your procedure and date. See when to arrive, the earliest you can fly home, which days you can go out, and hotels near the clinic.</p><span class="more">Open the Recovery Planner</span></a>
|
||||
<a class="card entry" href="#visit"><div class="cat">Preparing to visit</div><h3>Airport to clinic, hours and parking</h3><p>Line 9 from Gimpo Airport with no transfer, opening hours by day, and where to go when you arrive.</p><span class="more">Getting here</span></a>
|
||||
<a class="card entry" href="#clinic"><div class="cat">Checking the clinic</div><h3>Facts, building and reviews</h3><p>Founded {v(f.founded)}, 19-storey clinic building, specialties, and where reviews can be read.</p><span class="more">Clinic facts</span></a>
|
||||
<a class="card entry" href="#visit"><div class="cat">Preparing to visit</div><h3>Airport to clinic, hours and parking</h3><p>{visitCard}</p><span class="more">Getting here</span></a>
|
||||
<a class="card entry" href="#clinic"><div class="cat">Checking the clinic</div><h3>Facts, building and reviews</h3><p>{clinicCard}</p><span class="more">Clinic facts</span></a>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
@ -58,7 +76,7 @@ const ld = [
|
||||
<div class="sec-head"><div class="eyebrow">Visit</div><h2>Getting Here</h2><p class="sub">Address, transit, airport route, opening hours and what to do when you arrive.</p></div>
|
||||
<div class="grid">
|
||||
<div class="card"><div class="cat">Address</div><h3>{EN.address.full}</h3><p>{EN.transit}</p><p><a href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(EN.address.mapQuery)}`} rel="noopener" target="_blank">Open in Google Maps</a></p></div>
|
||||
<div class="card"><div class="cat">From the airport</div><h3>Incheon to Sinnonhyeon</h3><p>{EN.airport}</p></div>
|
||||
{has(EN.airport) && <div class="card"><div class="cat">From the airport</div><h3>Airport to the clinic</h3><p>{EN.airport}</p></div>}
|
||||
<div class="card"><div class="cat">On arrival</div><h3>Where to go in the building</h3><p>{EN.arrival}</p><p>{EN.parking}</p></div>
|
||||
</div>
|
||||
<VisitMap lang="en" hideHead nameEn={EN.shortName} addressEn={EN.address.full} />
|
||||
|
||||
@ -3,8 +3,11 @@
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import Planner from '../../components/Planner.astro';
|
||||
import { fact, SITE_NAME, clinicSchema, v } from '../../lib';
|
||||
import EN from '../../data/factSheetEn.json';
|
||||
const f = fact as Record<string, any>;
|
||||
const clinicEn = 'View Plastic Surgery';
|
||||
// 영문 표기는 factSheetEn 에서 읽는다. 영문 표기를 확인하지 못한 병원은 한국어 상호를 그대로 쓴다.
|
||||
const E = EN as Record<string, any>;
|
||||
const clinicEn = String(E.shortName || E.name || f.shortNameEn || f.nameEn || f.shortName || SITE_NAME);
|
||||
const site = Astro.site!.toString().replace(/\/$/, '');
|
||||
const ld = [
|
||||
{ '@type': 'WebPage', '@id': `${site}/en/plan#page`, name: `${clinicEn} Recovery Planner`, url: `${site}/en/plan`, inLanguage: 'en', isPartOf: { '@id': `${site}/#website` }, about: { '@id': `${fact.url}/#clinic` }, publisher: { '@type': 'Organization', name: SITE_NAME, url: site } },
|
||||
|
||||
@ -1 +1 @@
|
||||
/Users/haewonkam/orca/workspaces/INFINITH/Productize/supporters/AGENTS.md
|
||||
/Users/haewonkam/orca/workspaces/INFINITH/Productize-2/supporters/AGENTS.md
|
||||
@ -45,9 +45,17 @@ pkg.name = 'supporters-template';
|
||||
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
|
||||
// 남은 병원 고유 문자열 검사
|
||||
const CLINIC_RE = /뷰성형외과|viewclinic|ViewclinicKR|안성민|o2oteam/;
|
||||
// 상호뿐 아니라 지명·건물 같은 병원 고유 사실도 잡는다. 상호만 찾으면
|
||||
// "Sinnonhyeon Station Exit 3" 처럼 지명으로 적힌 사실이 그대로 템플릿에 남는다.
|
||||
const CLINIC_RE = /뷰성형외과|viewclinic|ViewclinicKR|안성민|o2oteam|View Plastic|Sinnonhyeon|Bongeunsa|Gimpo|신논현|봉은사|19-storey/;
|
||||
const leaks = [];
|
||||
const walk = (d) => { for (const f of readdirSync(d, { withFileTypes: true })) { const p = join(d, f.name); if (f.isDirectory()) { if (f.name !== 'node_modules') walk(p); } else if (/\.(astro|ts|mjs|json|css)$/.test(f.name) && !p.endsWith('export_template.mjs') && !p.includes('/scripts/gate/fixtures/') && CLINIC_RE.test(readFileSync(p, 'utf8'))) leaks.push(relative(DEST, p)); } };
|
||||
walk(DEST);
|
||||
if (leaks.length) { console.error('병원 고유 문자열이 남은 파일:\n ' + leaks.join('\n ')); process.exit(1); }
|
||||
// 문장을 그리는 코드에 병원 고유 사실이 박히면 다른 병원 화면에 틀린 말이 나가므로 내보내기를 멈춘다.
|
||||
// 데이터(src/data)는 워커가 병원마다 갈아끼우고 빌드 도구(scripts)는 고객이 보지 않으므로 알림만 한다.
|
||||
const RENDER_RE = /^src\/(pages|layouts|components|lib)\//;
|
||||
const blocking = leaks.filter((f) => RENDER_RE.test(f));
|
||||
const notes = leaks.filter((f) => !RENDER_RE.test(f));
|
||||
if (notes.length) console.log('참고 · 병원 이름이 남은 파일(고객 화면 아님):\n ' + notes.join('\n '));
|
||||
if (blocking.length) { console.error('고객 화면 코드에 병원 고유 사실이 남았습니다. 데이터에서 읽도록 고치세요:\n ' + blocking.join('\n ')); process.exit(1); }
|
||||
console.log(`템플릿 내보내기 완료: ${DEST}${existsSync(join(DEST, 'node_modules')) ? '' : ' (npm ci 필요)'}`);
|
||||
|
||||
25
templates/supporters-astro/src/data/factSheetEn.json
Normal file
25
templates/supporters-astro/src/data/factSheetEn.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"_comment": "영어 페이지용 병원 사실. 값의 원본은 factSheet.json 이고 여기에는 영어 표기만 둔다. 비어 있는 값은 영어 화면에서 해당 조각을 렌더하지 않는다. 지어내지 않는다.",
|
||||
"name": "",
|
||||
"shortName": "",
|
||||
"kind": "",
|
||||
"areaLabel": "",
|
||||
"address": {
|
||||
"full": "",
|
||||
"mapQuery": ""
|
||||
},
|
||||
"transit": "",
|
||||
"airport": "",
|
||||
"airportShort": "",
|
||||
"parking": "",
|
||||
"hoursDays": {
|
||||
"월·화·수·목": "",
|
||||
"금": "",
|
||||
"토": "",
|
||||
"일·공휴일": ""
|
||||
},
|
||||
"hoursClosed": "",
|
||||
"building": "",
|
||||
"arrival": "",
|
||||
"specialties": []
|
||||
}
|
||||
@ -23,5 +23,6 @@
|
||||
"newsOutletsExample": "",
|
||||
"sponsorNotice": "",
|
||||
"ga4MeasurementId": "",
|
||||
"_comment_ga4": "GA4 측정 ID(G-XXXX). 비우면 태그를 넣지 않는다. 키 이벤트 이름은 data/ai_channels.json conversion_events 와 같다."
|
||||
"_comment_ga4": "GA4 측정 ID(G-XXXX). 비우면 태그를 넣지 않는다. 키 이벤트 이름은 data/ai_channels.json conversion_events 와 같다.",
|
||||
"heroImageAltEn": ""
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import { SITE_NAME, SITE_TAGLINE, INDEXABLE, fact, site as S, surface, reservationUrl, editor, has, v } from '../lib';
|
||||
import { UI, switchHref } from '../lib/i18n';
|
||||
import EN from '../data/factSheetEn.json';
|
||||
const ed = editor();
|
||||
const f = fact as Record<string, any>;
|
||||
|
||||
@ -11,41 +13,28 @@ interface Props {
|
||||
ogImage?: string;
|
||||
type?: 'website' | 'article';
|
||||
noindex?: boolean;
|
||||
/** 페이지 언어. 외국인 환자용 영문 페이지(/en/*)에서 'en' 을 넘긴다. 기본은 한국어다. */
|
||||
lang?: 'ko' | 'en';
|
||||
alternates?: { lang: string; href: string }[];
|
||||
}
|
||||
const { title, description, jsonLd, ogImage = S.buildingImage?.src || '/favicon.svg', type = 'website', noindex = false, lang = 'ko' } = Astro.props;
|
||||
const isEn = lang === 'en';
|
||||
// 머리말·꼬리말 문구. 법적 고지(지원 관계·부작용)는 번역하지 않는다. 아래 주석 참조.
|
||||
const T = isEn
|
||||
? { nav: [['/posts', 'Questions'], ['/videos', 'Doctors'], ['/newsroom', 'Newsroom'], ['/en/plan', 'Recovery Planner'], ['/about', 'About']], cta: 'Book a Consultation', supporters: 'SUPPORTERS', back: 'Back', top: 'Back to top', sample: 'This is a sample site, not yet public.' }
|
||||
: { nav: [['/posts', '상담 전 질문'], ['/videos', '의료진 영상'], ['/newsroom', '뉴스룸'], ['/visit', '방문 안내'], ['/plan', '회복 일정'], ['/about', '매체 소개']], cta: '상담 예약', supporters: '서포터즈', back: '이전 페이지', top: '맨 위로', sample: '샘플 사이트입니다. 정식 공개 전입니다.' };
|
||||
|
||||
// 언어 전환. 같은 내용의 짝이 있는 페이지만 서로 잇고, 없으면 그 언어의 입구로 보낸다.
|
||||
// 영문 페이지가 늘어나면 이 표에 줄만 추가한다.
|
||||
const PAIRS: Record<string, string> = { '/plan': '/en/plan', '/visit': '/en/plan' };
|
||||
const KO_ENTRY = '/';
|
||||
const EN_ENTRY = '/en/plan';
|
||||
const path = (Astro.url.pathname.replace(/\.html$/, '').replace(/\/index$/, '/').replace(/\/$/, '') || '/');
|
||||
const koPair = Object.entries(PAIRS).find(([, en]) => en === path)?.[0];
|
||||
const otherHref = isEn ? (koPair ?? KO_ENTRY) : (PAIRS[path] ?? EN_ENTRY);
|
||||
// 짝이 없으면 같은 글의 번역본으로 가는 것이 아니므로 그렇게 말한다.
|
||||
const otherExact = isEn ? Boolean(koPair) : Boolean(PAIRS[path]);
|
||||
// 영문 페이지의 예약 버튼은 병원의 외국인용 영문 사이트로 보낸다(factSheet.urlEn).
|
||||
// 한국어 예약 페이지로 보내면 영어로 온 사람이 한국어 화면을 만난다.
|
||||
const ctaHref = isEn
|
||||
? ((f as Record<string, any>).reservationUrlEn || (f as Record<string, any>).urlEn || reservationUrl || '/en/plan')
|
||||
: (reservationUrl || '/visit');
|
||||
// 의료광고 고지는 규정 대상이라 임의로 번역하지 않는다(의료법 56조·추천보증 심사지침).
|
||||
// 병원이 영문 문구를 확정해 site.json 의 sponsorNoticeEn / factSheet 의 sideEffectNoticeEn 에 넣기 전까지는
|
||||
// 한국어 원문을 그대로 싣고 확인 대기임을 밝힌다.
|
||||
const sponsorEn: string = (S as Record<string, any>).sponsorNoticeEn || '';
|
||||
const sideEffectEn: string = (f as Record<string, any>).sideEffectNoticeEn || '';
|
||||
const { title, description, jsonLd, ogImage = S.buildingImage?.src || '/favicon.svg', type = 'website', noindex = false, lang = 'ko', alternates = [] } = Astro.props;
|
||||
const site = Astro.site?.toString().replace(/\/$/, '') ?? '';
|
||||
const cleanPath = Astro.url.pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '');
|
||||
const canonical = (new URL(cleanPath, Astro.site).toString().replace(/\/$/, '') || site);
|
||||
const u = UI[lang];
|
||||
const other = lang === 'ko' ? 'en' : 'ko';
|
||||
const switchTo = switchHref(Astro.url.pathname, other);
|
||||
const homeHref = lang === 'en' ? '/en' : '/';
|
||||
const isHome = ['/', '', '/index.html', '/index', '/en', '/en/index.html', '/en/index'].includes(Astro.url.pathname.replace(/\/$/, '') || '/');
|
||||
// GA4: site.json 의 ga4MeasurementId(G-XXXX) 가 있을 때만 태그를 넣는다. 키 이벤트는 data/ai_channels.json conversion_events 와 같은 이름(click_tel · click_reservation · click_clinic_site · click_map).
|
||||
const GA4: string = /^G-[A-Z0-9]{6,}$/.test(String((S as Record<string, any>).ga4MeasurementId ?? '')) ? String((S as Record<string, any>).ga4MeasurementId) : '';
|
||||
// 의료광고 고지는 규정 대상이라 임의로 번역하지 않는다(의료법 56조·추천보증 심사지침).
|
||||
// 병원이 영문 문구를 확정해 site.json 의 sponsorNoticeEn / factSheet 의 sideEffectNoticeEn 에 넣기 전까지는
|
||||
// 한국어 원문을 그대로 싣고 확인 대기임을 밝힌다. i18n.ts 의 영문 foot_sponsor·foot_side 는 이 자리에 쓰지 않는다.
|
||||
const sponsorEn: string = (S as Record<string, any>).sponsorNoticeEn || '';
|
||||
const sideEffectEn: string = (f as Record<string, any>).sideEffectNoticeEn || '';
|
||||
const noticePending = lang === 'en' && !sponsorEn;
|
||||
const sponsorText = (lang === 'en' && sponsorEn) ? sponsorEn : UI.ko.foot_sponsor.replace(/\{clinic\}/g, v(f.shortName, '병원')).replace('{editor}', ed.name);
|
||||
const sideEffectText = (lang === 'en' && sideEffectEn) ? sideEffectEn : fact.sideEffectNotice;
|
||||
const ld = jsonLd ? JSON.stringify({ '@context': 'https://schema.org', ...(Array.isArray(jsonLd) ? { '@graph': jsonLd } : jsonLd) }) : null;
|
||||
---
|
||||
<!doctype html>
|
||||
@ -63,7 +52,8 @@ const ld = jsonLd ? JSON.stringify({ '@context': 'https://schema.org', ...(Array
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={new URL(ogImage, Astro.site).toString()} />
|
||||
<meta property="og:locale" content={isEn ? "en_US" : "ko_KR"} />
|
||||
<meta property="og:locale" content={lang === 'en' ? 'en_US' : 'ko_KR'} />
|
||||
{alternates.map((a) => <link rel="alternate" hreflang={a.lang} href={`${site}${a.href}`} />)}
|
||||
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
@ -74,32 +64,44 @@ const ld = jsonLd ? JSON.stringify({ '@context': 'https://schema.org', ...(Array
|
||||
document.addEventListener('click',function(e){var a=e.target.closest&&e.target.closest('a');if(!a)return;var h=a.getAttribute('href')||'';var ev=h.indexOf('tel:')===0?'click_tel':a.dataset.ga==='reservation'||a.classList.contains('cta')?'click_reservation':a.dataset.ga==='clinic-site'?'click_clinic_site':a.dataset.ga==='map'?'click_map':null;if(ev)gtag('event',ev,{link_url:h,page_path:location.pathname});},true);`} />}
|
||||
</head>
|
||||
<body>
|
||||
{!INDEXABLE && <div class="sample-banner">{T.sample}</div>}
|
||||
{!INDEXABLE && <div class="sample-banner">{u.sample}</div>}
|
||||
<header class="site-header">
|
||||
<div class="wrap-wide bar">
|
||||
<a class="brand" href="/">{has(S.logo?.src) ? <img class="brand-logo" src={S.logo.src} alt={S.logo.alt} width={S.logo.width} height={S.logo.height} /> : <span class="brand-logo brand-name">{v(f.shortName, '병원')}</span>}<span class="brand-sep" aria-hidden="true"></span><span class="brand-text">{T.supporters} <small>{S.siteNameEn || 'SUPPORTERS'}</small></span></a>
|
||||
<a class="brand" href={homeHref}>{has(S.logo?.src) ? <img class="brand-logo" src={S.logo.src} alt={S.logo.alt} width={S.logo.width} height={S.logo.height} /> : <span class="brand-logo brand-name">{v(f.shortName, '병원')}</span>}<span class="brand-sep" aria-hidden="true"></span><span class="brand-text">{u.brand} <small>{S.siteNameEn || 'SUPPORTERS'}</small></span></a>
|
||||
<div class="right">
|
||||
<nav class="nav">
|
||||
{T.nav.map(([href, label]) => <a href={href}>{label}</a>)}
|
||||
{lang === 'ko' ? (
|
||||
<>
|
||||
<a href="/posts">{u.nav_posts}</a>
|
||||
<a href="/videos">{u.nav_videos}</a>
|
||||
<a href="/newsroom">{u.nav_news}</a>
|
||||
<a href="/visit">{u.nav_visit}</a>
|
||||
<a href="/plan">{u.nav_plan}</a>
|
||||
<a href="/about">{u.nav_about}</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<a href="/en">{u.nav_home}</a>
|
||||
<a href="/en/plan">{u.nav_plan}</a>
|
||||
<a href="/en#visit">{u.nav_visit}</a>
|
||||
<a href="/en#clinic">{u.nav_clinic}</a>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
<div class="langswitch" role="group" aria-label={isEn ? 'Language' : '언어'}>
|
||||
<a href={isEn ? otherHref : path} hreflang="ko" aria-current={!isEn ? 'true' : undefined} class={!isEn ? 'on' : ''}>KO</a>
|
||||
<a href={isEn ? path : otherHref} hreflang="en" aria-current={isEn ? 'true' : undefined} class={isEn ? 'on' : ''}
|
||||
title={isEn || otherExact ? undefined : 'English pages are limited. This goes to the English section.'}>EN</a>
|
||||
</div>
|
||||
<a class="cta" href={ctaHref} rel="noopener">{T.cta}</a>
|
||||
<a class="lang-switch" href={switchTo} aria-label={u.switchAria} hreflang={other} lang={other}>{u.switchLabel}</a>
|
||||
<a class="cta" href={reservationUrl || '/visit'} rel="noopener">{u.cta}</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{!['/', '', '/index.html', '/index'].includes(Astro.url.pathname.replace(/\/$/, '') || '/') && (
|
||||
{!isHome && (
|
||||
<div class="backbar-wrap"><div class="wrap-wide backbar">
|
||||
<a href="/" class="backbtn" id="backbtn" aria-label={T.back}><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M15.5 4.5 8 12l7.5 7.5 1.8-1.8L11.6 12l5.7-5.7z"/></svg>{T.back}</a>
|
||||
<a href={homeHref} class="backbtn" id="backbtn" aria-label={u.backAria}><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M15.5 4.5 8 12l7.5 7.5 1.8-1.8L11.6 12l5.7-5.7z"/></svg>{u.back}</a>
|
||||
</div></div>
|
||||
)}
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<button type="button" class="totop" id="totop" aria-label={T.top}><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 5 4.5 12.5l1.8 1.8L11 9.6V20h2V9.6l4.7 4.7 1.8-1.8z"/></svg></button>
|
||||
<button type="button" class="totop" id="totop" aria-label={u.totop}><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 5 4.5 12.5l1.8 1.8L11 9.6V20h2V9.6l4.7 4.7 1.8-1.8z"/></svg></button>
|
||||
<script>
|
||||
const back = document.getElementById('backbtn');
|
||||
if (back) back.addEventListener('click', (e) => { if (history.length > 1 && document.referrer && new URL(document.referrer).origin === location.origin) { e.preventDefault(); history.back(); } });
|
||||
@ -112,16 +114,20 @@ document.addEventListener('click',function(e){var a=e.target.closest&&e.target.c
|
||||
<div class="wrap-wide cols">
|
||||
<div>
|
||||
<p><strong>{SITE_NAME}</strong> · {SITE_TAGLINE}</p>
|
||||
{isEn && !sponsorEn && <p class="notice-pending">Korean original below. The English wording of these notices is pending clinic approval.</p>}
|
||||
<p>{isEn && sponsorEn ? sponsorEn : <>이 매체는 {v(f.shortName, '병원')}의 지원을 받아 서포터즈가 운영합니다. 글의 의학적 내용은 {v(f.shortName, '병원')} 담당 원장의 검토를 거쳐 표시하며, 검토 전 글은 "의학 검토 대기"로 표시합니다. 편집 책임 {ed.name}.</>}</p>
|
||||
<p>{isEn && sideEffectEn ? sideEffectEn : fact.sideEffectNotice}</p>
|
||||
<p><a href="/clinic">병원 정보</a> · <a href="/visit">방문 안내</a> · <a href="/about">이 사이트에 대해</a> · <a href="/corrections">정정 기록</a> · <a href={ed.email ? `mailto:${ed.email}` : '/corrections'}>정정 요청</a> · <a href="/editorial">편집 기준 (운영자용)</a></p>
|
||||
{noticePending && <p class="notice-pending">The English wording of these notices is pending clinic approval. The Korean original is shown below.</p>}
|
||||
<p>{sponsorText}</p>
|
||||
<p>{sideEffectText}</p>
|
||||
{lang === 'ko' ? (
|
||||
<p><a href="/clinic">{u.foot_clinic}</a> · <a href="/visit">{u.foot_visit}</a> · <a href="/plan">{u.foot_plan}</a> · <a href="/about">{u.foot_about}</a> · <a href="/corrections">{u.foot_corrections}</a> · <a href={ed.email ? `mailto:${ed.email}` : '/corrections'}>{u.foot_request}</a> · <a href="/editorial">{u.foot_editorial}</a></p>
|
||||
) : (
|
||||
<p><a href="/en#clinic">{u.foot_clinic}</a> · <a href="/en#visit">{u.foot_visit}</a> · <a href="/en/plan">{u.foot_plan}</a> · <a href="/about">{u.foot_about}</a> · <a href="/corrections">{u.foot_corrections}</a> · <a href={ed.email ? `mailto:${ed.email}` : '/corrections'}>{u.foot_request}</a></p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{has(S.logoFoot?.src) && <p><img src={S.logoFoot.src} alt={S.logoFoot.alt} width={S.logoFoot.width} height={S.logoFoot.height} style="width:138px;height:auto;margin-bottom:0.5rem" /></p>}
|
||||
<p><strong>{v(f.name)}</strong></p>
|
||||
<p>{v(f.address?.full)}<br />전화 {v(f.phone)} · 팩스 {v(f.fax)}<br />대표 {v(f.representative)} · 사업자등록번호 {v(f.businessNo)}</p>
|
||||
<p>{has(f.url) && <a href={f.url} rel="noopener">공식 홈페이지</a>}{has(surface('youtube').url) && <> · <a href={surface('youtube').url} rel="noopener">유튜브</a></>}{has(surface('gangnamunni').url) && <> · <a href={surface('gangnamunni').url} rel="noopener">강남언니</a></>}</p>
|
||||
<p><strong>{lang === 'en' ? EN.name : v(f.name)}</strong></p>
|
||||
<p>{lang === 'en' ? EN.address.full : v(f.address?.full)}<br />{u.foot_phone} {v(f.phone)} · {u.foot_fax} {v(f.fax)}<br />{u.foot_rep} {v(f.representative)} · {u.foot_biz} {v(f.businessNo)}</p>
|
||||
<p>{lang === 'en' && has(f.urlEn) ? <a href={f.urlEn} rel="noopener">{u.foot_site}</a> : has(f.url) && <a href={f.url} rel="noopener">{u.foot_site}</a>}{has(surface('youtube').url) && <> · <a href={surface('youtube').url} rel="noopener">{u.foot_youtube}</a></>}{has(surface('gangnamunni').url) && <> · <a href={surface('gangnamunni').url} rel="noopener">{u.foot_gu}</a></>}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap-wide powered">AI Discovery · Built with INFINITH</div>
|
||||
|
||||
48
templates/supporters-astro/src/lib/i18n.ts
Normal file
48
templates/supporters-astro/src/lib/i18n.ts
Normal file
@ -0,0 +1,48 @@
|
||||
// 사이트 공통(헤더·푸터·배너) 문자열과 언어 전환 규칙. 페이지 본문은 각 페이지가 맡고, 이 모듈은 Base.astro 가 쓴다.
|
||||
// 언어 전환은 전역 하나(헤더)로만 한다. 페이지마다 따로 토글을 두지 않는다 (haewon 결정, 2026-09-10).
|
||||
export type Lang = 'ko' | 'en';
|
||||
|
||||
/** 한국어 경로 ↔ 영어 경로. 영어판이 없는 한국어 페이지는 영어 홈으로, 반대는 한국어 홈으로 보낸다. */
|
||||
const PAIRS: [string, string][] = [
|
||||
['/', '/en'],
|
||||
['/plan', '/en/plan'],
|
||||
];
|
||||
export function switchHref(pathname: string, to: Lang): string {
|
||||
const p = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '').replace(/\/$/, '') || '/';
|
||||
for (const [ko, en] of PAIRS) {
|
||||
if (to === 'en' && p === ko) return en;
|
||||
if (to === 'ko' && p === en) return ko;
|
||||
}
|
||||
return to === 'en' ? '/en' : '/';
|
||||
}
|
||||
export const isEnPath = (pathname: string): boolean => /^\/en(\/|\.html$|$)/.test(pathname);
|
||||
|
||||
export const UI: Record<Lang, Record<string, string>> = {
|
||||
ko: {
|
||||
sample: '샘플 사이트입니다. 정식 공개 전입니다.',
|
||||
brand: '서포터즈',
|
||||
nav_posts: '상담 전 질문', nav_videos: '의료진 영상', nav_news: '뉴스룸', nav_visit: '방문 안내', nav_plan: '회복 일정', nav_about: '매체 소개',
|
||||
cta: '상담 예약',
|
||||
back: '이전 페이지', backAria: '이전 페이지로 돌아가기', totop: '맨 위로',
|
||||
switchLabel: 'EN', switchAria: 'Switch to English',
|
||||
foot_sponsor: '이 매체는 {clinic}의 지원을 받아 서포터즈가 운영합니다. 글의 의학적 내용은 {clinic} 담당 원장의 검토를 거쳐 표시하며, 검토 전 글은 "의학 검토 대기"로 표시합니다. 편집 책임 {editor}.',
|
||||
foot_clinic: '병원 정보', foot_visit: '방문 안내', foot_plan: '회복 일정', foot_about: '이 사이트에 대해', foot_corrections: '정정 기록', foot_request: '정정 요청', foot_editorial: '편집 기준 (운영자용)',
|
||||
foot_phone: '전화', foot_fax: '팩스', foot_rep: '대표', foot_biz: '사업자등록번호',
|
||||
foot_site: '공식 홈페이지', foot_youtube: '유튜브', foot_gu: '강남언니',
|
||||
},
|
||||
en: {
|
||||
sample: 'Sample site. Not yet public.',
|
||||
brand: 'Supporters',
|
||||
nav_home: 'Home', nav_plan: 'Recovery Planner', nav_visit: 'Getting Here', nav_clinic: 'Clinic Facts', nav_korean: '한국어 전체 보기',
|
||||
cta: 'Book a consultation',
|
||||
back: 'Back', backAria: 'Go back to the previous page', totop: 'Back to top',
|
||||
switchLabel: 'KO', switchAria: '한국어로 보기',
|
||||
// 아래 영문 foot_sponsor · foot_side 는 병원이 확정한 문구가 아니라 푸터 고지에 쓰지 않는다.
|
||||
// 고지는 Base.astro 가 site.json 의 sponsorNoticeEn · factSheet 의 sideEffectNoticeEn 에서 읽고, 없으면 한국어 원문에 확인 대기 표기를 붙인다.
|
||||
foot_sponsor: 'This site is run by supporters with support from {clinic}. Medical content is reviewed by the clinic\'s attending surgeon before it is marked as reviewed. Editor in charge: {editor}.',
|
||||
foot_side: 'After surgery, side effects such as inflammation, bleeding or nerve damage may occur depending on the individual.',
|
||||
foot_clinic: 'Clinic facts', foot_visit: 'Getting here', foot_plan: 'Recovery planner', foot_about: 'About this site (Korean)', foot_corrections: 'Corrections (Korean)', foot_request: 'Request a correction', foot_editorial: '',
|
||||
foot_phone: 'Tel', foot_fax: 'Fax', foot_rep: 'Representative', foot_biz: 'Business no.',
|
||||
foot_site: 'Official website (English)', foot_youtube: 'YouTube', foot_gu: 'Gangnam Unni',
|
||||
},
|
||||
};
|
||||
@ -1,7 +1,8 @@
|
||||
---
|
||||
// 없는 주소. Vercel 이 dist/404.html 을 자동으로 쓴다. 옛 주소(/recovery, /stay)는 vercel.json 리다이렉트가 /plan 으로 먼저 보낸다.
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { SITE_NAME } from '../lib';
|
||||
import { SITE_NAME, fact, v } from '../lib';
|
||||
const f = fact as Record<string, any>;
|
||||
const site = Astro.site!.toString().replace(/\/$/, '');
|
||||
const ld = { '@type': 'WebPage', '@id': `${site}/404#page`, name: '페이지를 찾을 수 없음', url: `${site}/404`, inLanguage: 'ko', isPartOf: { '@id': `${site}/#website` }, publisher: { '@type': 'Organization', name: SITE_NAME, url: site } };
|
||||
---
|
||||
@ -9,7 +10,7 @@ const ld = { '@type': 'WebPage', '@id': `${site}/404#page`, name: '페이지를
|
||||
<section class="section dark" style="padding:5rem 0 4.5rem"><div class="wrap-wide">
|
||||
<div class="sec-head"><div class="eyebrow" style="color:var(--purple-300)">404</div><h1 class="serif" style="font-size:2rem;display:inline-block;background:linear-gradient(to right,var(--purple-300),var(--blue-300));-webkit-background-clip:text;background-clip:text;color:transparent">Page Not Found</h1><p class="sub">주소가 바뀌었거나 없는 페이지입니다. 아래에서 이어서 보실 수 있습니다.</p></div>
|
||||
<div class="grid">
|
||||
<a class="card" href="/" style="text-decoration:none"><div class="cat">Home</div><h3>홈</h3><p>병원을 알아보는 분들의 질문과 답</p></a>
|
||||
<a class="card" href="/" style="text-decoration:none"><div class="cat">Home</div><h3>홈</h3><p>{v(f.shortName, '병원')} 정보를 알아보는 분들의 질문과 답</p></a>
|
||||
<a class="card" href="/plan" style="text-decoration:none"><div class="cat">Recovery Planner</div><h3>회복 일정</h3><p>수술일 기준 입국·출국 계산과 회복기 외출 계획. 영어 화면은 /en/plan</p></a>
|
||||
<a class="card" href="/visit" style="text-decoration:none"><div class="cat">Visit</div><h3>방문 안내</h3><p>예약, 가는 길, 주차, 도착 후 동선</p></a>
|
||||
</div>
|
||||
|
||||
112
templates/supporters-astro/src/pages/en/index.astro
Normal file
112
templates/supporters-astro/src/pages/en/index.astro
Normal file
@ -0,0 +1,112 @@
|
||||
---
|
||||
// 영어 홈. 헤더의 언어 전환(KO/EN)이 도착하는 곳. 구성은 한국어 홈과 같은 섹션 리듬(라이트 히어로 → 틴트 카드 → 다크 통계 → 라이트 → 다크)이고,
|
||||
// 내용은 영어로 읽을 수 있는 것만 싣는다: 회복 일정 플래너, 오는 길, 진료시간, 병원 사실, 원칙. 한국어 글은 번역 없이 링크만 안내한다.
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { fact, site as S, clinicSchema, surface, has, v } from '../../lib';
|
||||
import EN from '../../data/factSheetEn.json';
|
||||
import VisitMap from '../../components/VisitMap.astro';
|
||||
const f = fact as Record<string, any>;
|
||||
const site = Astro.site!.toString().replace(/\/$/, '');
|
||||
const yt = surface('youtube'); const gu = surface('gangnamunni'); const gg = surface('google');
|
||||
// 카드 요약문은 factSheetEn.json 에서만 만든다. 병원마다 다른 사실(공항 경로·건물)을 문장에 적어 두면
|
||||
// 다른 병원 사이트에 그대로 나가 틀린 말이 된다. 값이 없으면 그 조각을 빼고 일반 문장만 남긴다.
|
||||
const airportShort = String((EN as Record<string, any>).airportShort || '');
|
||||
const buildingShort = String((EN as Record<string, any>).building || '').split('. ')[0];
|
||||
const visitCard = airportShort
|
||||
? `${airportShort}, opening hours by day, and where to go when you arrive.`
|
||||
: 'How to reach the clinic, opening hours by day, and where to go when you arrive.';
|
||||
const clinicCard = [
|
||||
has(f.founded) && `Founded ${f.founded}`,
|
||||
buildingShort || null,
|
||||
'specialties, and where reviews can be read',
|
||||
].filter(Boolean).join(', ') + '.';
|
||||
const heroAlt = String((S as Record<string, any>).heroImageAltEn || S.heroImage?.alt || '');
|
||||
// 히어로 칩. 설립연도와 위치는 병원마다 다르므로 데이터에 있는 것만 잇는다. 둘 다 없으면 칩을 내지 않는다.
|
||||
const chipFacts = [
|
||||
has(f.founded) && `Founded ${f.founded}`,
|
||||
has((EN as Record<string, any>).areaLabel) && String((EN as Record<string, any>).areaLabel),
|
||||
].filter(Boolean).join(' · ');
|
||||
const hoursDays = EN.hoursDays as Record<string, string>;
|
||||
const ld = [
|
||||
{ '@type': 'WebPage', '@id': `${site}/en#page`, name: `${EN.shortName} Supporters (English)`, url: `${site}/en`, inLanguage: 'en', isPartOf: { '@id': `${site}/#website` }, about: { '@id': `${fact.url}/#clinic` } },
|
||||
clinicSchema(site),
|
||||
];
|
||||
---
|
||||
<Base lang="en" title={`${EN.shortName}, Seoul · Recovery planner, getting here, clinic facts`} description={`${EN.shortName} is a ${EN.kind} at ${EN.areaLabel}, founded in ${v(f.founded)}. Plan your recovery stay around your surgery date, find the way from the airport, and check opening hours and clinic facts in English.`} jsonLd={ld} alternates={[{ lang: 'ko', href: '/' }, { lang: 'en', href: '/en' }]}>
|
||||
<section class="hero-light">
|
||||
<div class="blob a"></div><div class="blob b"></div>
|
||||
<div class="wrap-wide inner">
|
||||
<div>
|
||||
<div class="eyebrow">{S.siteNameEn || 'VIEW SUPPORTERS'} · English</div>
|
||||
<h1>{EN.shortName},<br /><span class="accent">plan your stay before you fly.</span></h1>
|
||||
<p class="lede">{EN.shortName} is a {EN.kind} at {EN.areaLabel}{has(f.founded) && <>, founded in {f.founded}</>}. This site is run by supporters who answer the questions people ask before visiting. In English you can plan your recovery stay, find the way here and check clinic facts. Articles are in Korean.</p>
|
||||
<div class="chips">
|
||||
{chipFacts && <span class="chip"><span class="dot"></span>{chipFacts}</span>}
|
||||
<span class="chip"><span class="dot"></span>Based on the clinic's published guidance</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.8rem;flex-wrap:wrap;margin-top:1.6rem">
|
||||
<a class="btn primary" href="/en/plan">Recovery Planner</a>
|
||||
<a class="btn secondary" href="#visit">Getting here</a>
|
||||
</div>
|
||||
</div>
|
||||
{has(S.heroImage?.src) && <div class="hero-img collage"><img src={S.heroImage.src} alt={heroAlt} width={S.heroImage.width} height={S.heroImage.height} /></div>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section tint" style="padding:3.2rem 0"><div class="wrap-wide">
|
||||
<div class="sec-head"><div class="eyebrow">Start Here</div><h2>Where Are You Now?</h2><p class="sub">Pick the step that matches where you are.</p></div>
|
||||
<div class="grid">
|
||||
<a class="card entry" href="/en/plan"><div class="cat">Surgery date set</div><h3>Plan arrival, departure and recovery days</h3><p>Choose your procedure and date. See when to arrive, the earliest you can fly home, which days you can go out, and hotels near the clinic.</p><span class="more">Open the Recovery Planner</span></a>
|
||||
<a class="card entry" href="#visit"><div class="cat">Preparing to visit</div><h3>Airport to clinic, hours and parking</h3><p>{visitCard}</p><span class="more">Getting here</span></a>
|
||||
<a class="card entry" href="#clinic"><div class="cat">Checking the clinic</div><h3>Facts, building and reviews</h3><p>{clinicCard}</p><span class="more">Clinic facts</span></a>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="section dark"><div class="wrap-wide">
|
||||
<div class="sec-head"><div class="eyebrow" style="color:var(--purple-300)">At A Glance</div><h2>Clinic In Numbers</h2><p class="sub">Figures from the clinic's public profiles. Dates checked are shown with each number.</p></div>
|
||||
<div class="stats">
|
||||
<div class="stat"><b>{v(f.founded)}</b><span class="label">Founded</span><span class="desc">{EN.areaLabel}. {EN.building}</span></div>
|
||||
<div class="stat"><b>{v(gu.doctors)}</b><span class="label">Doctors listed</span><span class="desc">Gangnam Unni clinic profile, checked {v(gu.checked)}</span></div>
|
||||
<div class="stat"><b>{v(yt.videos)}</b><span class="label">Doctor videos</span><span class="desc">Official YouTube channel (Korean), checked {v(yt.checked)}</span></div>
|
||||
<div class="stat"><b>{v(gu.reviews)}</b><span class="label">Reviews on Gangnam Unni</span><span class="desc">We link to where reviews can be read and do not reproduce patient stories.</span></div>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="section light" id="visit"><div class="wrap-wide">
|
||||
<div class="sec-head"><div class="eyebrow">Visit</div><h2>Getting Here</h2><p class="sub">Address, transit, airport route, opening hours and what to do when you arrive.</p></div>
|
||||
<div class="grid">
|
||||
<div class="card"><div class="cat">Address</div><h3>{EN.address.full}</h3><p>{EN.transit}</p><p><a href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(EN.address.mapQuery)}`} rel="noopener" target="_blank">Open in Google Maps</a></p></div>
|
||||
{has(EN.airport) && <div class="card"><div class="cat">From the airport</div><h3>Airport to the clinic</h3><p>{EN.airport}</p></div>}
|
||||
<div class="card"><div class="cat">On arrival</div><h3>Where to go in the building</h3><p>{EN.arrival}</p><p>{EN.parking}</p></div>
|
||||
</div>
|
||||
<VisitMap lang="en" hideHead nameEn={EN.shortName} addressEn={EN.address.full} />
|
||||
<h3 style="margin-top:2rem">Opening hours</h3>
|
||||
<div class="table-wrap"><table><thead><tr><th>Day</th><th>Hours</th></tr></thead><tbody>
|
||||
{(f.hours?.rows ?? []).map((r: any) => <tr><td>{hoursDays[r.days] ?? r.days}</td><td>{r.open ? `${r.open} to ${r.close}` : EN.hoursClosed}</td></tr>)}
|
||||
</tbody></table></div>
|
||||
<p style="font-size:0.88rem;color:var(--slate-500)">Source: {has(f.hours?.source) ? <a href={f.hours.source} rel="noopener">clinic profile</a> : 'clinic'}. Hours can change, so call or message before you travel.</p>
|
||||
<p style="margin-top:1.2rem"><a class="btn primary" href={f.reservationUrl || f.url} rel="noopener">Book a consultation</a> {has(f.urlEn) && <a class="btn secondary" href={f.urlEn} rel="noopener">Official English website</a>}</p>
|
||||
</div></section>
|
||||
|
||||
<section class="section tint" id="clinic"><div class="wrap-wide">
|
||||
<div class="sec-head"><div class="eyebrow">Fact Sheet</div><h2>Clinic Facts</h2><p class="sub">What the clinic states publicly, with the date we checked.</p></div>
|
||||
<div class="grid">
|
||||
<div class="card"><div class="cat">Specialties</div><h3>What the clinic does</h3><ul style="margin:0;padding-left:1.1rem;font-size:0.93rem;color:var(--slate-600)">{EN.specialties.map((s) => <li>{s}</li>)}</ul></div>
|
||||
<div class="card"><div class="cat">Contact</div><h3>{EN.name}</h3><p>Tel {v(f.phone)}{has(f.kakao) && <> · <a href={f.kakao} rel="noopener">KakaoTalk channel</a></>}</p><p>{has(f.url) && <a href={f.url} rel="noopener">Korean website</a>}{has(f.urlEn) && <> · <a href={f.urlEn} rel="noopener">English website</a></>}</p></div>
|
||||
<div class="card"><div class="cat">Reviews</div><h3>Where to read them</h3><p>{has(gu.url) && <a href={gu.url} rel="noopener">Gangnam Unni</a>}{has(gu.rating) && <> rating {gu.rating}, {v(gu.reviews)} reviews</>}{has(gg.url) && <> · <a href={gg.url} rel="noopener">Google Maps</a>{has(gg.rating) && <> rating {gg.rating}</>}</>}. We do not quote individual patient stories.</p></div>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="section dark"><div class="wrap-wide">
|
||||
<div class="sec-head"><div class="eyebrow" style="color:var(--purple-300)">Principles</div><h2>What We Do, What We Don't</h2><p class="sub">Three rules so you can judge for yourself.</p></div>
|
||||
<div class="grid">
|
||||
<div class="card"><h3>How we answer</h3><p>One question, one article, the answer first. Medical content is reviewed by the clinic's attending surgeon and the review date is shown. Articles are in Korean.</p></div>
|
||||
<div class="card"><h3>What we leave out</h3><p>Before-and-after photos, other patients' stories, comparisons with other clinics, and any promise of results. For reviews we only say where they can be read.</p></div>
|
||||
<div class="card"><h3>What we disclose</h3><p>Who wrote each piece, whether a surgeon reviewed it, what sources were used, and that this site is supported by {EN.shortName}.</p></div>
|
||||
</div>
|
||||
</div></section>
|
||||
</Base>
|
||||
<style>
|
||||
.card.entry { text-decoration: none; color: inherit; }
|
||||
.card .more { font-size: 0.85rem; font-weight: 600; color: var(--accent); }
|
||||
</style>
|
||||
@ -3,8 +3,11 @@
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import Planner from '../../components/Planner.astro';
|
||||
import { fact, SITE_NAME, clinicSchema, v } from '../../lib';
|
||||
import EN from '../../data/factSheetEn.json';
|
||||
const f = fact as Record<string, any>;
|
||||
const clinicEn = (fact as Record<string, any>).nameEn || (fact as Record<string, any>).shortNameEn || (fact as Record<string, any>).shortName || 'the clinic';
|
||||
// 영문 표기는 factSheetEn 에서 읽는다. 영문 표기를 확인하지 못한 병원은 한국어 상호를 그대로 쓴다.
|
||||
const E = EN as Record<string, any>;
|
||||
const clinicEn = String(E.shortName || E.name || f.shortNameEn || f.nameEn || f.shortName || SITE_NAME);
|
||||
const site = Astro.site!.toString().replace(/\/$/, '');
|
||||
const ld = [
|
||||
{ '@type': 'WebPage', '@id': `${site}/en/plan#page`, name: `${clinicEn} Recovery Planner`, url: `${site}/en/plan`, inLanguage: 'en', isPartOf: { '@id': `${site}/#website` }, about: { '@id': `${fact.url}/#clinic` }, publisher: { '@type': 'Organization', name: SITE_NAME, url: site } },
|
||||
|
||||
@ -17,7 +17,7 @@ const ld = [
|
||||
clinicSchema(site),
|
||||
];
|
||||
---
|
||||
<Base title={`${clinic}, ${(S.heroTitle || '궁금한 것부터 답합니다').replace(/\.$/, '')}`} description={`${clinic}${has(f.areaLabel) ? `(${f.areaLabel})` : ''}를 알아보는 분들이 실제로 묻는 질문을 골라, 원장 설명 영상과 병원 공개 자료를 근거로 답합니다. 글마다 작성자와 원장 감수 여부를 밝힙니다.`} jsonLd={ld}>
|
||||
<Base title={`${clinic}, ${(S.heroTitle || '궁금한 것부터 답합니다').replace(/\.$/, '')}`} description={`${clinic}${has(f.areaLabel) ? `(${f.areaLabel})` : ''}를 알아보는 분들이 실제로 묻는 질문을 골라, 원장 설명 영상과 병원 공개 자료를 근거로 답합니다. 글마다 작성자와 원장 감수 여부를 밝힙니다.`} jsonLd={ld} alternates={[{ lang: 'ko', href: '/' }, { lang: 'en', href: '/en' }]}>
|
||||
<section class="hero-light">
|
||||
<div class="blob a"></div><div class="blob b"></div>
|
||||
<div class="wrap-wide inner">
|
||||
|
||||
@ -11,6 +11,7 @@ export const GET: APIRoute = async ({ site }) => {
|
||||
{ loc: `${base}/posts`, lastmod: today },
|
||||
{ loc: `${base}/clinic`, lastmod: today },
|
||||
{ loc: `${base}/visit`, lastmod: today },
|
||||
{ loc: `${base}/en`, lastmod: today },
|
||||
{ loc: `${base}/plan`, lastmod: today },
|
||||
{ loc: `${base}/en/plan`, lastmod: today },
|
||||
{ loc: `${base}/newsroom`, lastmod: today },
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import Base from '../layouts/Base.astro';
|
||||
import Gallery from '../components/Gallery.astro';
|
||||
import VisitMap from '../components/VisitMap.astro';
|
||||
import '../styles/plan.css';
|
||||
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, '병원');
|
||||
@ -38,14 +39,17 @@ const ld = [
|
||||
<h2>4. 도착하면</h2>
|
||||
<p>{v(S.arrivalGuide, `도착 후 동선은 병원 확인 뒤 싣습니다. ${v(null)}.`)} 층별 구성 전체는 <a href="/clinic">병원 정보</a>에 있습니다.</p>
|
||||
|
||||
<div class="plan-entry">
|
||||
<h2>외국에서 오신다면: 회복 일정 플래너</h2>
|
||||
<p>시술과 수술일을 고르면 입국 권장일, 출국 가능 최소일, 회복기 동안 외출할 수 있는 날과 갈 만한 곳, 병원 근처 숙소를 한 화면에서 정리해 드립니다. 영어 화면도 있습니다.</p>
|
||||
<a class="btn primary" href="/plan">일정표 만들기</a>
|
||||
</div>
|
||||
|
||||
<h2>5. 상담 전에 읽어볼 글</h2>
|
||||
<ul>
|
||||
{(S.visitReads ?? []).map((r) => <li><a href={r.href}>{r.title}</a> {r.note}</li>)}
|
||||
{!(S.visitReads ?? []).length && <li>검토를 마친 글부터 차례로 연결합니다.</li>}
|
||||
</ul>
|
||||
<h2>6. 해외에서 오시는 경우</h2>
|
||||
<p>수술 전후에 머무는 동안의 숙박·식사·회복·관광 정보를 영문으로 정리했습니다. <a href="/en/plan" hreflang="en">Recovery Planner</a>.</p>
|
||||
|
||||
<p style="margin-top:2rem">{has(reservationUrl) && <a class="btn primary" href={reservationUrl} rel="noopener">{clinic} 상담 예약</a>}</p>
|
||||
<p class="disclosure">{fact.sideEffectNotice}</p>
|
||||
</article>
|
||||
|
||||
@ -144,6 +144,7 @@ td { color: var(--slate-700); }
|
||||
.site-footer .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; }
|
||||
.site-footer p { margin-bottom: 0.5rem; color: rgba(255,255,255,0.75); }
|
||||
.site-footer strong { color: #fff; }
|
||||
.site-footer .notice-pending { color: #fff; border-left: 2px solid var(--purple-300); padding-left: 0.7rem; margin-bottom: 0.6rem; }
|
||||
.site-footer a { color: var(--purple-300); }
|
||||
.site-footer .powered { margin-top: 2rem; padding-top: 1.2rem; border-top: 1px solid rgba(255,255,255,0.1); font-family: "Playfair Display", Georgia, serif; letter-spacing: 0.05em; color: var(--purple-300); font-size: 0.8rem; }
|
||||
@media (max-width: 720px) {
|
||||
@ -199,6 +200,8 @@ td { color: var(--slate-700); }
|
||||
|
||||
/* 헤더: INFINITH 헤더 규격 (흰 배경, 우측 그라디언트 pill CTA) */
|
||||
.site-header .right { display: flex; align-items: center; gap: 1rem; }
|
||||
.site-header .lang-switch { display: inline-grid; place-items: center; min-width: 2.4rem; height: 2rem; padding: 0 0.6rem; border-radius: 999px; border: 1px solid var(--slate-200); color: var(--slate-600); font-family: Inter, Pretendard, sans-serif; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.06em; text-decoration: none; }
|
||||
.site-header .lang-switch:hover { color: var(--primary-900); border-color: var(--primary-900); text-decoration: none; }
|
||||
.site-header .cta { background: linear-gradient(to right, var(--grad-start), var(--grad-end)); color: #fff; border-radius: 999px; padding: 0.5rem 1.05rem; font-size: 0.86rem; font-weight: 600; }
|
||||
.site-header .cta:hover { text-decoration: none; box-shadow: var(--shadow); }
|
||||
.nav a, .site-header .cta, .brand-text { white-space: nowrap; }
|
||||
@ -265,249 +268,15 @@ td { color: var(--slate-700); }
|
||||
.totop:hover { box-shadow: 0 10px 28px rgba(2,19,65,0.40); }
|
||||
@media (max-width: 720px) { .totop { right: 1rem; bottom: 1.1rem; width: 44px; height: 44px; } }
|
||||
|
||||
|
||||
/* 값이 아직 없다는 것을 숨기지 않고 드러내는 문단. 병원 확인 대기 항목에 쓴다. */
|
||||
.pending-note {
|
||||
background: var(--status-warn-bg, #FFF6ED);
|
||||
border: 1px solid var(--status-warn-border, #F5E0C5);
|
||||
border-left-width: 3px;
|
||||
color: var(--status-warn-text, #7C5C3A);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.notice-pending { font-size: 0.82rem; opacity: 0.75; }
|
||||
|
||||
|
||||
/* 영문 화면에서 한국어 페이지로 가는 링크에 붙인다. 눌러보고 알게 하지 않는다. */
|
||||
.lang-tag {
|
||||
display: inline-block;
|
||||
margin-left: 0.28em;
|
||||
padding: 0 0.3em;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 3px;
|
||||
font-size: 0.62em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
vertical-align: 0.18em;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* 언어 전환. 머리말 오른쪽에 하나만 둔다. 항목마다 표시하지 않는다. */
|
||||
.langswitch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--line, #E2E8F0);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
margin-right: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.langswitch a {
|
||||
padding: 0.26rem 0.62rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--slate-500, #64748B);
|
||||
text-decoration: none;
|
||||
line-height: 1.45;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.langswitch a:hover { background: var(--near, #F4F6FB); color: var(--primary-900, #0A1128); }
|
||||
.langswitch a.on {
|
||||
background: var(--primary-900, #0A1128);
|
||||
color: #fff;
|
||||
}
|
||||
.langswitch a.on:hover { background: var(--primary-900, #0A1128); color: #fff; }
|
||||
|
||||
/* ── /en/stay ── */
|
||||
.stay { padding-bottom: 4rem; }
|
||||
.stay .lede { color: var(--slate-600, #475569); margin-top: 0.6rem; }
|
||||
.stay section { margin-top: 3.4rem; }
|
||||
.stay .sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
||||
.stay .sec-head h2 { margin: 0; }
|
||||
.stay .upd { font-size: 0.8rem; color: var(--slate-500, #64748B); }
|
||||
.stay .sub { color: var(--slate-600, #475569); margin: 0.4rem 0 1rem; }
|
||||
.stay .src { font-size: 0.84rem; color: var(--slate-500, #64748B); margin-top: 1rem; }
|
||||
.stay .chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.2rem 0 1.6rem; }
|
||||
.stay .chip {
|
||||
border: 1px solid var(--line, #E2E8F0); background: #fff; border-radius: 999px;
|
||||
padding: 0.36rem 0.85rem; font: inherit; font-size: 0.86rem; color: var(--slate-600, #475569);
|
||||
cursor: pointer; transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.stay .chip b { font-weight: 700; opacity: 0.55; margin-left: 0.25em; }
|
||||
.stay .chip:hover { border-color: var(--primary-900, #0A1128); }
|
||||
.stay .chip.on { background: var(--primary-900, #0A1128); border-color: var(--primary-900, #0A1128); color: #fff; }
|
||||
.stay .chip.on b { opacity: 0.7; }
|
||||
.stay .grp { margin-bottom: 2.2rem; }
|
||||
.stay .grp h3 { margin: 0 0 0.9rem; }
|
||||
.stay .grp h3 small { font-weight: 400; font-size: 0.82rem; color: var(--slate-500, #64748B); margin-left: 0.45em; }
|
||||
.stay .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); gap: 1rem; }
|
||||
.stay .card {
|
||||
background: #fff; border: 1px solid var(--line, #E2E8F0); border-radius: 14px;
|
||||
overflow: hidden; display: flex; flex-direction: column;
|
||||
box-shadow: 3px 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.stay .card .thumb { position: relative; height: 148px; background: #EEF1F6; display: flex; align-items: center; justify-content: center; }
|
||||
.stay .card .thumb img { width: 100%; height: 148px; object-fit: cover; display: block; }
|
||||
.stay .card .nophoto {
|
||||
font-size: 1.02rem; font-weight: 700; color: var(--slate-500, #64748B);
|
||||
padding: 0 1rem; text-align: center; line-height: 1.35; word-break: keep-all;
|
||||
}
|
||||
.stay .card .badge {
|
||||
position: absolute; left: 0.6rem; bottom: 0.6rem;
|
||||
background: rgba(10, 17, 40, 0.82); color: #fff; border-radius: 6px;
|
||||
padding: 0.18rem 0.45rem; font-size: 0.74rem; font-weight: 600;
|
||||
}
|
||||
.stay .card .badge em { font-style: normal; opacity: 0.7; margin-left: 0.25em; }
|
||||
.stay .card .badge.mono { letter-spacing: 0.06em; }
|
||||
.stay .card .body { padding: 0.85rem 0.95rem 1rem; display: flex; flex-direction: column; flex: 1; }
|
||||
.stay .card h4 { margin: 0 0 0.35rem; font-size: 0.98rem; }
|
||||
.stay .card .when { font-size: 0.8rem; color: var(--slate-600, #475569); margin: 0 0 0.4rem; }
|
||||
.stay .card .desc { font-size: 0.86rem; color: var(--slate-600, #475569); margin: 0 0 0.5rem; }
|
||||
.stay .card .addr { font-size: 0.78rem; color: var(--slate-500, #64748B); margin: 0 0 0.6rem; }
|
||||
.stay .card .go { font-size: 0.82rem; margin-top: auto; }
|
||||
.stay .route { border: 1px solid var(--line, #E2E8F0); border-radius: 16px; padding: 1.3rem 1.4rem; margin-bottom: 1.4rem; background: #fff; }
|
||||
.stay .route-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; }
|
||||
.stay .route-head h3 { margin: 0; }
|
||||
.stay .route .who { font-size: 0.84rem; color: var(--slate-500, #64748B); margin: 0.15rem 0 0; }
|
||||
.stay .route .clock { font-size: 0.86rem; font-weight: 600; color: var(--primary-900, #0A1128); white-space: nowrap; }
|
||||
.stay .route .blurb { color: var(--slate-600, #475569); margin: 0.7rem 0 1rem; }
|
||||
.stay .route .map { width: 100%; height: auto; display: block; border-radius: 12px; }
|
||||
.stay .route .maphint { font-size: 0.76rem; color: var(--slate-500, #64748B); margin: 0.4rem 0 1rem; }
|
||||
.stay .timeline { list-style: none; padding: 0; margin: 0; }
|
||||
.stay .timeline .move { font-size: 0.8rem; color: var(--slate-500, #64748B); margin: 0.35rem 0 0.35rem 1.05rem; }
|
||||
.stay .timeline .stop { display: flex; gap: 0.8rem; align-items: flex-start; }
|
||||
.stay .timeline .n {
|
||||
flex: none; width: 26px; height: 26px; border-radius: 50%;
|
||||
background: var(--primary-900, #0A1128); color: #fff;
|
||||
font-size: 0.8rem; font-weight: 700; display: grid; place-items: center; margin-top: 0.1rem;
|
||||
}
|
||||
.stay .timeline .stop-t { font-size: 0.8rem; color: var(--slate-500, #64748B); }
|
||||
.stay .timeline h4 { margin: 0.1rem 0 0.2rem; font-size: 0.98rem; }
|
||||
.stay .timeline p { margin: 0; font-size: 0.86rem; color: var(--slate-600, #475569); }
|
||||
.stay .botnote { margin-top: 1.6rem; }
|
||||
/* 시술 후 관리 안내(병원 원문 인용). 동선 카드(.route)·정차(.stop) 룩을 그대로 쓰고 접이식 동작만 더한다. 새 색·크기 없음. */
|
||||
.stay details.recovery > summary.btn { list-style: none; cursor: pointer; }
|
||||
.stay details.recovery > summary.btn::-webkit-details-marker { display: none; }
|
||||
.stay details.recovery > summary.btn .hide { display: none; }
|
||||
.stay details.recovery[open] > summary.btn .show { display: none; }
|
||||
.stay details.recovery[open] > summary.btn .hide { display: inline; }
|
||||
.stay details.recovery[open] > summary.btn { margin-bottom: 1.1rem; }
|
||||
.stay details.recovery > .timeline { margin-top: 0.2rem; }
|
||||
.stay .recovery .timeline > li { border-top: 1px solid var(--line, #E2E8F0); padding: 0.7rem 0; }
|
||||
.stay .recovery .timeline > li:first-child { border-top: 0; padding-top: 0; }
|
||||
.stay .recovery summary { cursor: pointer; list-style: none; }
|
||||
.stay .recovery summary::-webkit-details-marker { display: none; }
|
||||
.stay .recovery summary .stop { display: flex; }
|
||||
.stay .recovery summary h4 { margin: 0.1rem 0 0; }
|
||||
.stay .recovery details[open] summary h4 { text-decoration: underline; text-underline-offset: 0.2em; text-decoration-color: var(--slate-200, #E2E8F0); }
|
||||
.stay .recovery details > ul { margin: 0.7rem 0 0.5rem 2.2rem; padding-left: 1.1rem; }
|
||||
.stay .recovery details > ul li { font-size: 0.9rem; color: var(--slate-700, #334155); margin-bottom: 0.35rem; }
|
||||
.stay .recovery details > p { margin: 0 0 0 2.2rem; font-size: 0.84rem; color: var(--slate-500, #64748B); }
|
||||
@media (max-width: 640px) { .cards { grid-template-columns: 1fr; } }
|
||||
|
||||
/* 섹션 점프. 머리말 아래에 붙어 스크롤을 따라온다. 헤더(68px) 아래에 걸리게 top 을 맞춘다. */
|
||||
.stay .jump {
|
||||
position: sticky; top: 68px; z-index: 9;
|
||||
display: flex; flex-wrap: wrap; gap: 0.45rem;
|
||||
margin: 1.4rem 0 2.4rem;
|
||||
padding: 0.62rem 0.68rem;
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
backdrop-filter: blur(16px) saturate(1.5);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 8px 28px rgba(10, 17, 40, 0.09), inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.stay .jump a {
|
||||
display: inline-flex; align-items: center; gap: 0.42rem;
|
||||
padding: 0.46rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.86rem; font-weight: 600; letter-spacing: -0.005em;
|
||||
color: var(--primary-900, #0A1128);
|
||||
text-decoration: none; white-space: nowrap;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line, #E2E8F0);
|
||||
box-shadow: 0 1px 2px rgba(10, 17, 40, 0.06);
|
||||
transition: transform 0.14s ease, box-shadow 0.14s ease, background 0.14s, border-color 0.14s, color 0.14s;
|
||||
}
|
||||
.stay .jump a:hover {
|
||||
text-decoration: none;
|
||||
transform: translateY(-1px);
|
||||
border-color: #C5CBF5;
|
||||
box-shadow: 0 4px 12px rgba(10, 17, 40, 0.12);
|
||||
}
|
||||
.stay .jump a:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(10, 17, 40, 0.08); }
|
||||
.stay .jump a b {
|
||||
font-weight: 700; font-size: 0.72rem; line-height: 1;
|
||||
padding: 0.2rem 0.4rem; border-radius: 999px;
|
||||
background: #EFF0FF; color: #3A3F7C;
|
||||
}
|
||||
/* 지금 보고 있는 섹션 */
|
||||
.stay .jump a.on {
|
||||
background: linear-gradient(to right, #4F1DA1, #021341);
|
||||
border-color: transparent; color: #fff;
|
||||
box-shadow: 0 4px 14px rgba(79, 29, 161, 0.32);
|
||||
}
|
||||
.stay .jump a.on b { background: rgba(255, 255, 255, 0.22); color: #fff; }
|
||||
/* 점프로 이동했을 때 제목이 스티키 바에 가리지 않게 */
|
||||
.stay section, .stay .grp { scroll-margin-top: 152px; }
|
||||
@media (max-width: 640px) {
|
||||
.stay .jump { top: 60px; gap: 0.35rem; padding: 0.5rem; border-radius: 14px; }
|
||||
.stay .jump a { padding: 0.4rem 0.72rem; font-size: 0.8rem; }
|
||||
}
|
||||
|
||||
/* 필터 칩도 눌리는 것처럼 보이게 */
|
||||
.stay .chip {
|
||||
box-shadow: 0 1px 2px rgba(10, 17, 40, 0.05);
|
||||
transition: transform 0.14s ease, box-shadow 0.14s ease, background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.stay .chip:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(10, 17, 40, 0.1); }
|
||||
.stay .chip:active { transform: translateY(0); }
|
||||
.stay .chip.on { box-shadow: 0 4px 14px rgba(10, 17, 40, 0.22); }
|
||||
|
||||
/* 오늘 날씨. 값을 못 받으면 hidden 이라 아예 그려지지 않는다. */
|
||||
.stay .wx {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 1.2rem; flex-wrap: wrap;
|
||||
margin-top: 1.6rem; padding: 1rem 1.2rem;
|
||||
background: linear-gradient(to right, #fff3eb, #e4cfff, #f5f9ff);
|
||||
border-radius: 16px;
|
||||
}
|
||||
.stay .wx-now { display: flex; align-items: center; gap: 0.9rem; }
|
||||
.stay .wx-temp { font-family: 'Playfair Display', serif; font-size: 2.5rem; font-weight: 700; line-height: 1; color: var(--primary-900, #0A1128); }
|
||||
.stay .wx-unit { font-size: 1.1rem; vertical-align: 0.9rem; margin-left: 0.06em; opacity: 0.6; }
|
||||
.stay .wx-cond { font-weight: 700; color: var(--primary-900, #0A1128); }
|
||||
.stay .wx-meta { font-size: 0.8rem; color: var(--slate-600, #475569); margin-top: 0.15rem; }
|
||||
.stay .wx-note { margin: 0; font-size: 0.88rem; color: var(--slate-700, #334155); max-width: 30rem; }
|
||||
|
||||
/* 동선 지도. OSM 타일을 좌표로 깔고 그 위에 핀을 얹는다. 출처 표기는 OSM 정책상 필수다. */
|
||||
.stay .route .map {
|
||||
position: relative; width: 100%; max-width: 680px; height: 340px;
|
||||
border-radius: 12px; overflow: hidden; background: #E8EDF3;
|
||||
border: 1px solid var(--line, #E2E8F0);
|
||||
}
|
||||
.stay .route .map-tiles { position: absolute; inset: 0; }
|
||||
.stay .route .map-tiles .tile { position: absolute; width: 256px; height: 256px; background-size: 256px 256px; background-repeat: no-repeat; }
|
||||
.stay .route .map-pins { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.stay .route .map-credit {
|
||||
position: absolute; right: 0; bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.82); color: var(--slate-600, #475569);
|
||||
font-size: 0.66rem; padding: 0.1rem 0.35rem; border-top-left-radius: 6px;
|
||||
}
|
||||
|
||||
/* 뷰 사이트 헤더 규칙 이식 (2026-09-11) */
|
||||
.site-header .lang-switch { display: inline-grid; place-items: center; min-width: 2.4rem; height: 2rem; padding: 0 0.6rem; border-radius: 999px; border: 1px solid var(--slate-200); color: var(--slate-600); font-family: Inter, Pretendard, sans-serif; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.06em; text-decoration: none; }
|
||||
.site-header .lang-switch:hover { color: var(--primary-900); border-color: var(--primary-900); text-decoration: none; }
|
||||
/* 태블릿·모바일 헤더 (≤900px): 첫 줄 브랜드 + 상담 예약, 둘째 줄 메뉴 가로 스크롤. 메뉴 6개가 두 줄로 겹치던 문제를 DOM 변경 없이 CSS만으로 해결한다 (2026-09-10) */
|
||||
@media (max-width: 900px) {
|
||||
.site-header .bar { height: auto; flex-wrap: wrap; padding-top: 0.6rem; padding-bottom: 0; row-gap: 0.1rem; }
|
||||
.site-header .brand { flex: 1; }
|
||||
.site-header .right { display: contents; }
|
||||
.site-header .lang-switch, .site-header .langswitch { order: 2; margin-right: 0.5rem; }
|
||||
.site-header .lang-switch { order: 2; margin-right: 0.5rem; }
|
||||
.site-header .cta { order: 2; }
|
||||
.site-header .nav { order: 3; display: flex; gap: 1rem; overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; scrollbar-width: none; padding: 0.35rem 0 0.7rem; width: 100%; }
|
||||
.site-header .nav::-webkit-scrollbar { display: none; }
|
||||
.site-header .nav a { margin-left: 0; flex: none; font-size: 0.9rem; }
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user