merge: feature/ai-sentiment-rubric 진행분 (주변 안내 정본 반영, §8 인사이트) 반영
supporters/ 정본에 주변 안내(/stay·/en/stay)가 i18n 방식으로 들어왔다. recoveryNotes 는 뷰 실데이터를 유지한다. 핸드오버 제목 날짜를 2026-09-12(토)로 맞추고, §8-1 과 §8-11 에 이번 통합에서 같은 사고를 되풀이한 기록과 재발 방지(머지 직전 브랜치 끝 재확인)를 남겼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
0a0f64c29e
@ -1,4 +1,4 @@
|
||||
# 다음 세션 핸드오버 · 2026-09-12 · INFINITH AI Discovery Product 빌드
|
||||
# 세션 핸드오버 · 2026-09-12(토) · INFINITH AI Discovery Product 빌드
|
||||
|
||||
작성 2026-09-11 · 갱신 2026-09-12(토) · 작성자 Claude (haewon 세션) · 상태: §1 확인 완료, §4-1 완료
|
||||
하위 문서: 서포터즈 사이트 빌드 절차 `~/orca/workspaces/INFINITH/Productize/docs/NEXT_SESSION_2026-09-12_PRODUCT_BUILD.md`, 자동 빌드 상세 `…/NEXT_SESSION_SUPPORTERS_AUTOBUILD_v3.md`, 자동 채점 `docs/AI_DISCOVERY_BUILD2_HANDOVER.md`, 회고 `docs/INFINITH_AI_Discovery_Retrospective_2026-09-08.md`.
|
||||
@ -9,7 +9,7 @@
|
||||
INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI 답변 준비도 진단(AEO/GEO v2) → 서포터즈 사이트 자동 빌드(글·영상·뉴스룸·회복 일정 플래너·오시는 길) → 병원 확인·재빌드 → 측정(질문뱅크·감성·GA4·크롤러 로그)" 파이프라인이다.
|
||||
코드 위치: 프런트·스크립트·문서는 메인 체크아웃 ~/Claude/Agentic Marketing/INFINITH (브랜치 feature/ai-sentiment-rubric, 프로덕션 main → infinith-demo.vercel.app),
|
||||
워커·템플릿·큐는 워크트리 ~/orca/workspaces/INFINITH/Productize (브랜치 feature/supporters-autobuild), 랜딩 리드 CTA 는 브랜치 feature/discovery-url-cta(미머지).
|
||||
먼저 docs/NEXT_SESSION_2026-09-12_AI_DISCOVERY_PRODUCT.md 를 읽고, §1 표의 "확인" 칸을 git log·Supabase 로 실제 확인한 뒤, §5 결정 대기의 답을 받고 §4 순서대로 진행한다.
|
||||
먼저 docs/NEXT_SESSION_2026-09-12_AI_DISCOVERY_PRODUCT.md 를 읽는다(§8 인사이트 포함). 시작 절차: (1) 두 저장소 git status 와 .vercel 연결을 확인하고 다른 세션이 열려 있지 않은지 haewon 에게 묻는다 (2) §1 표의 "확인" 칸을 git log·Supabase 로 실제 확인한다 (3) §5 결정 대기의 답을 받는다 (4) §4 순서대로 진행하되 1번 브랜치 통합을 먼저 끝낸다. 한 세션은 한 저장소·한 배포 대상만 다룬다.
|
||||
원칙: 근거 없는 사실은 생성하지 않는다("확인 대기"). 사람 게이트(의학 검토·게시 승인·이미지 육안·채널 확인·구조 기준선)는 자동화하지 않는다. 언급률·점수는 "스키마를 넣으면 인용된다"고 약속하지 않는다(정직성 규약).
|
||||
고객 화면에 운영자 어휘 금지. 존댓말, 엠대시 금지(커밋 메시지 포함), 은유 금지, 흰 배경 위 연보라 텍스트 금지(인디고 #1D0024). INFINITH 리포트 PDF 는 PPTX 덱(infinith-deck 스킬)으로 만든 뒤 변환한다.
|
||||
클라이언트 격리: INFINITH 문서·코드에 다른 서비스 이름을 쓰지 않는다. 산출 전 grep.
|
||||
@ -39,7 +39,7 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
|
||||
| 6 측정 | 질문뱅크 언급률(2엔진), 감성, GA4 AI 유입, AI 크롤러 히트, 공식 vs 서포터즈 재채점 | `scripts/run_question_bank_openai.py`·`merge_qb_results.py`·`sentiment_qb.py`·`ga4_ai_traffic.py`, `data/ai_channels.json`, 리포트 Integrity 절 "측정 가능 범위" | 메인(QB·감성) + Productize(GA4) | 스크립트 단위. 리포트 카드 자동 갱신 없음. GA4 는 GCP 계정 대기 |
|
||||
| 7 계약·가격 | 상담 기반 계약 | 가격 문서 `docs/INFINITH_가격정책_사업부보고서_v1.1.docx`(2026-04, 이전 제품 INSIGHT/INTELLIGENCE 티어), AI Discovery 가격 제안은 `docs/INFINITH_vs_FrostAI_비교_및_가격제안_2026-09-08.html` | 메인 | **AI Discovery 전용 티어 미확정** |
|
||||
|
||||
2026-09-10~11 추가: 회복 일정 플래너(설계 v0.2 §13, 관광공사 데이터 합침), 오시는 길 지도·길찾기, 전역 언어 전환·영어 홈(뷰), 모바일 헤더. 전부 템플릿·워커에 통합됨.
|
||||
2026-09-10~11 추가: 회복 일정 플래너 `/plan`(설계 v0.2 §13, 관광공사 데이터 합침), 주변 안내 `/stay`(장소·축제·동선 지도·날씨, 플래너와 별개 페이지로 복원), 오시는 길 지도·길찾기, 전역 언어 전환·영어 홈(뷰), 모바일 헤더, 발행(색인) 단계. 전부 템플릿·워커에 통합됨.
|
||||
|
||||
## 2. 코드·브랜치·배포 지도
|
||||
|
||||
@ -70,6 +70,7 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
|
||||
- 진단 결과가 코드 파일(`discovery_<id>.ts`)이라 병원마다 프런트 배포가 필요하다. 런타임 데이터(Supabase 또는 JSON 스토리지)로 옮겨야 "URL 입력 → 리포트 링크"가 된다.
|
||||
- 측정 지표가 리포트 카드에 자동으로 들어가지 않는다(스크립트 산출물을 사람이 옮긴다).
|
||||
- 워커가 Mac 에 묶여 있다. 규칙표 잠정값·큐레이션 장소·의료진 후보·이미지·채널 12건은 사람 확인 대기.
|
||||
- 색인 작업의 사람 몫(네이버 등록·GSC 소유자·도메인·법무)이 체크리스트 파일로만 나오고, 담당자에게 알림·요청하는 UI 가 아직 없다(§4-9).
|
||||
- AI Discovery 가격·계약 조건 미확정. 랜딩 FAQ 의 해지·환불 항목 삭제 상태.
|
||||
|
||||
## 4. Product 빌드 순서 (권장)
|
||||
@ -84,6 +85,7 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
|
||||
6. **운영 이관**: Mac launchd 폴러 → Trigger.dev(또는 동등)로. 키·비용 로그(`status.json` costUsd)·실패 알림.
|
||||
7. **가격·계약**: AI Discovery 티어(진단만 / 진단+서포터즈 / +측정·관리) 확정, 랜딩 FAQ·계약서·해지 조건. FrostAI 비교 문서의 제안을 출발점으로.
|
||||
8. **다음 병원**: 톡스앤필·차앤박(피부과), 레지스트리 73곳 중 회귀 병원. 한 화면 비교 산출물은 만들지 않는다.
|
||||
9. **발행(색인) 단계**: "서포터즈 사이트 완료"의 정의는 배포가 아니라 색인 작업과 사람 체크리스트까지 끝난 상태다(haewon 결정 2026-09-11). 워커 `publish` 단계(`supporters/scripts/publish_site.mjs`)가 자동으로 하는 것: 라이브 확인(홈·robots·sitemap·noindex 헤더), IndexNow 로 사이트맵 URL 전부 알림(빙·네이버 공용, 색인 허용 빌드일 때만), Google Search Console 사이트맵 제출과 URL 색인 상태 조회(서비스 계정 `GOOGLE_SERVICE_ACCOUNT_JSON` 있을 때), 검색엔진 소유 확인 메타 태그(site.json `googleSiteVerification`·`naverSiteVerification`), IndexNow 키 파일(`public/<key>.txt`) 자동 생성, `--indexable` 로 noindex 헤더 제거·승인 글만 발행, `--domain` 으로 정식 도메인 연결. 사람에게 남기는 것(`~/supporters-builds/<clinic>/publish-checklist.md`, `status.humanTasks`): 네이버 서치어드바이저 등록·소유 확인·사이트맵/RSS 제출(공개 API 없음), Google Search Console 소유자 추가(첫 1회), 정식 도메인 결정, 색인 허용 전환 전 법무 3건·게시 승인, 병원 자산에서 백링크·sameAs, 네이버 블로그 재작성 트랙. 하지 않는 것: Google Indexing API(채용·라이브 방송 전용), 색인 보장 약속. 제품에서는 이 체크리스트를 확인 패널(`ClinicInputsPanel`) 또는 운영자 알림으로 사람에게 요청하게 만드는 것이 다음 일이다.
|
||||
|
||||
## 5. haewon 결정 대기
|
||||
|
||||
@ -122,6 +124,7 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
|
||||
## 6. 함정 (제품 관점)
|
||||
|
||||
- **한 Vercel 프로젝트에 두 폴더**: 2026-09-11 Productize/supporters 와 메인 supporters 가 같은 프로젝트에 배포해 `/plan` 이 404 됐다. 지금은 Productize 쪽 연결을 껐다. 배포는 병원 폴더와 메인 supporters 에서만.
|
||||
- **페이지를 덮어쓰지 않는다**: 플래너 통합 때 `/stay`(주변 안내)를 `/plan` 으로 리다이렉트해 세 사이트에서 주변 안내가 사라졌다(09-11 17시 복원). 새 기능이 기존 페이지와 '같은 주제'로 보여도 haewon 확인 전에는 기존 페이지를 지우거나 리다이렉트하지 않는다.
|
||||
- **세 브랜치 분산**: 같은 파일(`supporters/`)이 두 브랜치에서 다르게 진화했다. 머지 전에 "템플릿이 정본"을 정하고 뷰 샘플을 옮긴다.
|
||||
- **진단 결과가 코드**: `discovery_<id>.ts` 커밋 방식은 병원 수가 늘면 배포 병목이다(§4-3).
|
||||
- **게이트는 고객 화면 전체를 본다**: 운영 어휘("실측"·"API 집계"·"임베드")가 데이터 값을 타고 화면에 나가면 빌드가 막힌다. 운영 기록은 데이터에만.
|
||||
@ -140,3 +143,20 @@ INFINITH AI Discovery Product 빌드 세션. 제품은 "병원 URL 하나 → AI
|
||||
- 측정: `scripts/run_question_bank_openai.py`, `scripts/sentiment_qb.py`, `docs/AI_ANSWER_SENTIMENT_RUBRIC_v0.1.md`, Productize `scripts/ga4_ai_traffic.py`, `data/ai_channels.json`
|
||||
- 가격·경쟁: `docs/INFINITH_vs_FrostAI_비교_및_가격제안_2026-09-08.html`, `docs/INFINITH_가격정책_사업부보고서_v1.1.docx`
|
||||
- PRD: `docs/prd/INFINITH_AI-Discovery_PRD_20260828_v02.md`, `docs/prd/INFINITH_Global_Medical_Hub_PRD_v0.1.html`(Productize 에 v0.2), `docs/prd/INFINITH_Dermatology_Supporters_PRD_v0.1.html`
|
||||
|
||||
## 8. 이번 이틀(09-10~11)의 인사이트 · 다음 개발에 그대로 적용할 것
|
||||
|
||||
1. **한 세션 = 한 저장소 = 한 배포 대상.** 두 세션이 같은 Vercel 프로젝트와 같은 `supporters/` 폴더를 다른 브랜치에서 건드려 `/plan` 404 와 주변 안내 실종이 났다. 세션 시작 때 `git status`·`.vercel/project.json`·열린 세션 여부를 확인하고, 그 세션이 맡지 않은 자산은 손대지 않는다. 브랜치 통합(§4-1)이 이 문제의 근본 해결이므로 첫 일로 했다. **2026-09-12 완료.**
|
||||
- 다만 통합 중에 같은 사고를 한 번 더 냈다. 머지 기준으로 삼은 브랜치 끝이 세션 도중 9개 커밋만큼 앞서 있었고(주변 안내 복원·워커 publish 단계), 그것을 모르고 올린 첫 머지가 둘을 떨어뜨렸다. 푸시 전에 발견해 되돌렸다.
|
||||
- **머지 직전에 브랜치 끝을 다시 읽는다.** 세션 시작 때 읽은 `git rev-parse`/`git log` 값은 머지 시점에 낡아 있을 수 있다. 다른 워크트리가 열려 있으면 특히 그렇다. 머지 뒤에는 `git rev-list --count main..<branch>` 가 0 인지 확인한다.
|
||||
2. **"완료"를 산출물이 아니라 고객 결과로 정의하면 자동·사람 경계가 드러난다.** 서포터즈 사이트 완료 = 색인 작업 + 사람 체크리스트로 바꾸자 IndexNow·GSC 는 자동, 네이버 등록·소유자 추가는 사람으로 갈렸고 워커 단계가 그 경계를 코드로 가졌다. 진단(사람 판정 4항목), 병원 확인(재빌드 트리거)도 같은 방식으로 "완료 정의 → 자동 부분 → 사람 요청"을 명시한다.
|
||||
3. **병원 원문은 인용 전에 걸러야 한다.** 원문 32문장 중 14건이 홍보문·정형 고지·표 조각이었다. 원문을 데이터로 쓰는 모든 경로(recoveryNotes, OCR, 뉴스 제목)에 화이트리스트 필터와 "제외된 문장 목록 눈으로 보기"를 둔다. 필터 없는 "병원 안내 기준" 배지는 의료광고 위반이다.
|
||||
4. **게이트는 데이터 값도 본다.** 좌표 출처의 "실측"이 화면에 새어 나가 빌드가 막혔다. 데이터 필드마다 고객 노출 여부를 정하고 운영 기록(source·coordSource·note)은 렌더하지 않는다.
|
||||
5. **규칙표를 먼저 쓰면 코드·테스트가 따라온다.** 설계 v0.2 §13 의 status(clinic/provisional)·출처 필수 규칙이 그대로 procedures.json 스키마와 게이트 테스트가 됐다. 새 기능은 데이터 규칙표부터 문서화한다.
|
||||
6. **관광공사 데이터는 공급이지 수요가 아니다.** 선호는 화면에서 직접 묻고, 큐레이션이 1순위, 외부 데이터는 보강(2순위)이라는 우선순위를 점수로 코드화했다. 외부 API 를 붙일 때마다 같은 질문(공급인가 수요인가, 누가 1순위인가)을 먼저 한다.
|
||||
7. **디자인 규칙은 텍스트가 아니라 리듬이다.** "흰 카드만 쌓인 페이지"는 홈·영상 페이지의 섹션 리듬(다크 히어로+글래스 통계 → 틴트 → 다크 → 라이트 → 틴트 → 다크)을 안 따라서 완성도가 낮아 보였다. 새 페이지는 기존 페이지 두 개의 구성을 먼저 분석하고 같은 리듬으로 짠다. 웜 그라디언트는 CTA 카드 한 장에만.
|
||||
8. **전역 CSS 와 컴포넌트 클래스는 접두어로 분리한다.** `.kpi` 충돌로 그라디언트가 되살아났다. 컴포넌트 클래스는 `plan-`·`vm-` 처럼 접두어를 쓰고, 새 클래스는 전역 CSS 에 grep 한다.
|
||||
9. **모바일 검증은 하네스로.** 헤드리스 크롬 창 최소 폭 때문에 400px 캡처가 틀렸고, 실제 문제(헤더 겹침)는 아이프레임 하네스에서만 보였다. 8페이지×3사이트 그리드 캡처를 배포 전 기본 절차로 둔다.
|
||||
10. **언어 전환은 전역 하나.** 페이지별 토글은 사용자 관점에서 틀렸다. 영어 홈이 없는 사이트는 영어 입구(/en/plan)를 정해야 하고, 템플릿에 영어 홈을 넣을지는 §5-3 결정에 달렸다.
|
||||
11. **기존 페이지는 haewon 확인 전에 지우거나 리다이렉트하지 않는다.** 새 기능이 기존 페이지와 같은 주제로 보여도 병존이 기본이다(주변 안내 `/stay` 와 회복 일정 `/plan`). **2026-09-12 재발.** 브랜치 통합 머지가 `/stay` 를 떨어뜨렸다. 페이지를 지우려는 의도가 없어도 머지 기준을 잘못 잡으면 같은 결과가 난다. 통합 뒤 페이지 목록을 이전 배포와 대조한다.
|
||||
12. **제품 병목은 세 가지로 좁혀졌다.** 진단 결과가 코드 파일, 리드→큐 미연결, 브랜치 3개 분산. 이 셋을 풀어야 "URL 입력 → 리포트 링크 → 서포터즈 미리보기"가 사람 손 없이 이어진다.
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"a",
|
||||
"a",
|
||||
"a",
|
||||
"a",
|
||||
"a.lang-switch",
|
||||
"a.cta",
|
||||
"main",
|
||||
@ -287,6 +288,7 @@
|
||||
"a",
|
||||
"a",
|
||||
"a",
|
||||
"a",
|
||||
"a.lang-switch",
|
||||
"a.cta",
|
||||
"div.backbar-wrap",
|
||||
@ -515,6 +517,7 @@
|
||||
"a",
|
||||
"a",
|
||||
"a",
|
||||
"a",
|
||||
"a.lang-switch",
|
||||
"a.cta",
|
||||
"div.backbar-wrap",
|
||||
|
||||
@ -75,6 +75,7 @@ document.addEventListener('click',function(e){var a=e.target.closest&&e.target.c
|
||||
<a href="/posts">{u.nav_posts}</a>
|
||||
<a href="/videos">{u.nav_videos}</a>
|
||||
<a href="/newsroom">{u.nav_news}</a>
|
||||
<a href="/stay">{u.nav_stay}</a>
|
||||
<a href="/visit">{u.nav_visit}</a>
|
||||
<a href="/plan">{u.nav_plan}</a>
|
||||
<a href="/about">{u.nav_about}</a>
|
||||
@ -82,6 +83,7 @@ document.addEventListener('click',function(e){var a=e.target.closest&&e.target.c
|
||||
) : (
|
||||
<>
|
||||
<a href="/en">{u.nav_home}</a>
|
||||
<a href="/en/stay">{u.nav_stay}</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>
|
||||
|
||||
@ -6,6 +6,7 @@ export type Lang = 'ko' | 'en';
|
||||
const PAIRS: [string, string][] = [
|
||||
['/', '/en'],
|
||||
['/plan', '/en/plan'],
|
||||
['/stay', '/en/stay'],
|
||||
];
|
||||
export function switchHref(pathname: string, to: Lang): string {
|
||||
const p = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '').replace(/\/$/, '') || '/';
|
||||
@ -21,7 +22,7 @@ export const UI: Record<Lang, Record<string, string>> = {
|
||||
ko: {
|
||||
sample: '샘플 사이트입니다. 정식 공개 전입니다.',
|
||||
brand: '서포터즈',
|
||||
nav_posts: '상담 전 질문', nav_videos: '의료진 영상', nav_news: '뉴스룸', nav_visit: '방문 안내', nav_plan: '회복 일정', nav_about: '매체 소개',
|
||||
nav_posts: '상담 전 질문', nav_videos: '의료진 영상', nav_news: '뉴스룸', nav_stay: '주변 안내', nav_visit: '방문 안내', nav_plan: '회복 일정', nav_about: '매체 소개',
|
||||
cta: '상담 예약',
|
||||
back: '이전 페이지', backAria: '이전 페이지로 돌아가기', totop: '맨 위로',
|
||||
switchLabel: 'EN', switchAria: 'Switch to English',
|
||||
@ -33,7 +34,7 @@ export const UI: Record<Lang, Record<string, string>> = {
|
||||
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: '한국어 전체 보기',
|
||||
nav_home: 'Home', nav_stay: 'Nearby', 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: '한국어로 보기',
|
||||
|
||||
@ -280,3 +280,180 @@ td { color: var(--slate-700); }
|
||||
.site-header .nav a { margin-left: 0; flex: none; font-size: 0.9rem; }
|
||||
}
|
||||
|
||||
/* ── /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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user