- 지역 이야기(가요·인물·연표·엽서·퀴즈) 생성 경로: 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>
119 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
}
|