o2o-site-AEO/solution/site/src/sections/SiteFooter.tsx
Mina Choi 328d9e18ee [feat] solution: 지역 이야기 생성 · 발행본 섹션 손질 · 마이그레이션 주석 축약
- 지역 이야기(가요·인물·연표·엽서·퀴즈) 생성 경로: story_service · grounding/story ·
  section_prompts. 지금까지 만들 자리가 없어 시안에만 손으로 넣은 3만 자였다
- 발행본 섹션: ItinerarySection · Carousel 레일 자동재생(use-rail-autoplay) ·
  Festival · LocalGuide · Weather · Gallery · Header/Footer
- 목업 payload 를 payloads-mockup/ 으로 분리 — 발행 대상과 섞이지 않게
- DB 새 구조 후속: site_payload · local_content_crud 조인 정리 · 테스트
- 마이그레이션 주석 축약: 9개 파일 합계 주석 비율 48% → 25%.
  실측과 밟은 함정만 남기고 논증은 커밋 메시지로 옮겼다

검증: site·frontend 빌드 통과

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

119 lines
5.4 KiB
TypeScript

import {ExternalLink, Mail, MapPin, Phone} from 'lucide-react';
import {useSite} from '@site/lib/site-context';
import {isoDate} from '@site/lib/format';
// 채널 이름표는 예약·문의 섹션과 같은 표를 쓴다 — 같은 채널이 자리마다 다른 이름으로 뜨면 안 된다.
import {channelLabel, publicLinks} from '@site/lib/derive';
/**
* 푸터.
*
* ★ `bg-zinc-950` 이었다. 사장님이 어떤 템플릿을 골라도 바닥만 검정이라 마지막 섹션과
* 색이 튀었다. 어두운 면은 템플릿이 정한 것(`--tpl-inverse`)을 쓴다.
* ★ 모바일에서는 하단 고정 탭에 가리지 않게 아래 여백을 더 준다.
*/
export function SiteFooter() {
const payload = useSite();
const {place, site} = payload;
// 확정된 채널만 — 확정 전 URL 은 동명 업소일 수 있다.
// ★ sameAs 와 같은 목록이어야 한다 — 화면에 없는 주소가 구조화 데이터에 있으면 발행이 막힌다.
const links = publicLinks(payload);
const address = place.roadAddress ?? place.address;
return (
<footer
className="w-full pb-28 pt-14 md:pb-14"
style={{
backgroundColor: 'var(--tpl-inverse, #1c1917)',
color: 'var(--tpl-bg, #ffffff)',
}}
>
<div className="shell">
<div className="grid grid-cols-1 gap-8 border-b border-current/15 pb-10 md:grid-cols-12">
<div className="space-y-4 md:col-span-7">
<p className="serif text-[length:var(--fs-lead)] font-bold">{place.name}</p>
<div className="space-y-2 text-[length:var(--fs-sm)] opacity-75">
{address && (
<p className="flex items-start gap-2">
<MapPin className="mt-1 size-4 shrink-0 opacity-60" />
<span>{address}</span>
</p>
)}
{place.phone && (
<p className="flex items-center gap-2">
<Phone className="size-4 shrink-0 opacity-60" />
<a href={`tel:${place.phone}`} className="underline-offset-2 hover:underline">
{place.phone}
</a>
</p>
)}
{place.email && (
<p className="flex items-center gap-2">
<Mail className="size-4 shrink-0 opacity-60" />
<a href={`mailto:${place.email}`} className="underline-offset-2 hover:underline">
{place.email}
</a>
</p>
)}
</div>
</div>
{links.length > 0 && (
<nav aria-label="공식 채널" className="md:col-span-5">
<h2 className="label mb-3 !text-current opacity-60"> </h2>
<ul className="flex flex-wrap gap-2">
{links.map((link) => (
<li key={link.url}>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-lg border border-current/25 px-3 py-2 text-[length:var(--fs-xs)] font-medium transition-colors hover:bg-current/10"
>
<span>{channelLabel(link)}</span>
<ExternalLink className="size-3.5" />
</a>
</li>
))}
</ul>
</nav>
)}
</div>
<div className="flex flex-col gap-3 pt-6 text-[length:var(--fs-xs)] opacity-55 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<span>: {place.name}</span>
{place.legal?.representative && <span>: {place.legal.representative}</span>}
{place.legal?.businessRegistrationNumber && (
<span>: {place.legal.businessRegistrationNumber}</span>
)}
{place.legal?.mailOrderNumber && (
<span>: {place.legal.mailOrderNumber}</span>
)}
{place.legal?.licenseNumber && (
<span>
{place.legal.licenseLabel ?? '인허가번호'}: {place.legal.licenseNumber}
</span>
)}
</div>
{/*
★ 누가 썼고 언제 갱신했는지를 화면에도 적는다. head 의 meta[author] 만으로는
"화면에 없는 주장" 이고, E-E-A-T 채점도 byline 을 본문에서 찾는다.
<time datetime> 은 사람이 읽는 표기와 기계가 읽는 값을 한 요소에 둔다.
★ 화면 표기를 ISO 로 바꿨다 (2026-09-04, AEO 진단 '본문 날짜 표기 없음' 0/3)
"2026년 9월 2일" 은 사람은 읽지만 **날짜를 찾는 쪽은 못 읽는다.** 답변 엔진은
본문에서 YYYY-MM-DD 를 찾아 최신성을 매긴다. datetime 속성에만 있고 화면에 없으면
"화면에 없는 주장" 과 같은 처지가 된다 — 그래서 눈에 보이는 글자도 ISO 로 적는다.
*/}
<p className="byline shrink-0">
<span className="author">· {place.name}</span>
{' · 최종 업데이트: '}
<time dateTime={site.updatedAt}>{isoDate(site.updatedAt)}</time>
</p>
</div>
</div>
</footer>
);
}