Loading...
Loading...
02-reusable-page/footer/footer-i18n.tsx
/**
* I18n Footer — 국제화 지원 푸터
*
* 'use client' 컴포넌트. 번역 함수(t())를 주입받아 next-intl/react-intl 등과 호환.
* 모든 텍스트가 i18n 키 기반. 시스템 상태 표시기 옵션 포함.
* shadcn CSS 변수 기반 (light/dark 자동 전환).
*
* @source secure-vault-master (260116)
* @extracted 2026-02-18
* @version 1.0.0
*
* @dependencies lucide-react, next/link
*
* @example
* ```tsx
* // next-intl 사용 예
* 'use client';
* import { useTranslations } from 'next-intl';
* import { FooterI18n } from './footer-i18n';
* import { Lock } from 'lucide-react';
*
* export function Footer() {
* const t = useTranslations('footer');
* return (
* <FooterI18n
* t={t}
* brand={{ name: 'SecureVault', icon: <Lock className="h-6 w-6 text-primary" /> }}
* linkSections={{
* product: {
* titleKey: 'product',
* links: [
* { labelKey: 'features', href: '/#features' },
* { labelKey: 'pricing', href: '/pricing' },
* ],
* },
* }}
* bottomBar={{
* copyrightKey: 'copyright',
* status: { label: 'allSystemsOperational', operational: true },
* }}
* />
* );
* }
* ```
*/
'use client';
import Link from 'next/link';
import type { FooterBrandInfo, FooterStatusIndicator } from './_shared/types';
// ============================================
// I18n 전용 타입
// ============================================
/** 번역 함수 시그니처 (next-intl / react-intl 호환) */
export type TranslateFn = (
key: string,
values?: Record<string, string | number>,
) => string;
/** i18n 링크 섹션 정의 */
export interface I18nLinkSection {
/** 섹션 제목 번역 키 */
titleKey: string;
/** 섹션 내 링크 목록 */
links: {
/** 링크 라벨 번역 키 */
labelKey: string;
/** 이동 경로 */
href: string;
}[];
}
/** i18n 하단 바 정의 */
export interface I18nBottomBar {
/** 저작권 텍스트 번역 키 (예: 'copyright') — t(key, { year }) 호출 */
copyrightKey: string;
/** 하단 링크 */
bottomLinks?: {
labelKey: string;
href: string;
}[];
/** 시스템 상태 표시기 */
status?: FooterStatusIndicator;
}
// ============================================
// Props
// ============================================
export interface FooterI18nProps {
/** 번역 함수 */
t: TranslateFn;
/** 브랜드 정보 */
brand: FooterBrandInfo;
/** i18n 키 기반 링크 섹션 (키: 섹션 ID) */
linkSections: Record<string, I18nLinkSection>;
/** 하단 바 (저작권, 링크, 상태) */
bottomBar?: I18nBottomBar;
/** 브랜드 설명 번역 키 */
descriptionKey?: string;
/** 추가 className */
className?: string;
}
// ============================================
// 컴포넌트
// ============================================
export function FooterI18n({
t,
brand,
linkSections,
bottomBar,
descriptionKey,
className = '',
}: FooterI18nProps) {
const currentYear = new Date().getFullYear();
const homeHref = brand.homeHref ?? '/';
const sectionEntries = Object.values(linkSections);
return (
<footer
className={`border-t border-border bg-card/50 ${className}`}
>
<div className="container mx-auto px-4 py-12">
{/* 메인 콘텐츠 */}
<div className="grid grid-cols-2 gap-8 md:grid-cols-6">
{/* 브랜드 섹션 */}
<div className="col-span-2">
<Link href={homeHref} className="mb-4 flex items-center gap-2">
{brand.icon && brand.icon}
<span className="text-xl font-bold">{brand.name}</span>
</Link>
{descriptionKey && (
<p className="max-w-xs text-sm text-muted-foreground">
{t(descriptionKey)}
</p>
)}
</div>
{/* 링크 섹션 */}
{sectionEntries.map((section) => (
<div key={section.titleKey}>
<h3 className="mb-3 text-sm font-semibold">
{t(section.titleKey)}
</h3>
<ul className="space-y-2">
{section.links.map((link) => (
<li key={link.labelKey}>
<Link
href={link.href}
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{t(link.labelKey)}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
{/* 하단 바 */}
{bottomBar && (
<div className="mt-10 border-t border-border pt-6">
<div className="flex flex-col items-center justify-between gap-4 md:flex-row">
<p className="text-sm text-muted-foreground">
{t(bottomBar.copyrightKey, { year: currentYear })}
</p>
<div className="flex items-center gap-6 text-sm text-muted-foreground">
{/* 하단 링크 */}
{bottomBar.bottomLinks?.map((link) => (
<Link
key={link.labelKey}
href={link.href}
className="transition-colors hover:text-foreground"
>
{t(link.labelKey)}
</Link>
))}
{/* 시스템 상태 표시기 */}
{bottomBar.status && (
<div className="flex items-center gap-2">
<span
className={`inline-block h-2 w-2 rounded-full ${
bottomBar.status.operational !== false
? 'bg-green-500'
: 'bg-red-500'
}`}
/>
{bottomBar.status.href ? (
<Link
href={bottomBar.status.href}
className="transition-colors hover:text-foreground"
>
{t(bottomBar.status.label)}
</Link>
) : (
<span>{t(bottomBar.status.label)}</span>
)}
</div>
)}
</div>
</div>
</div>
)}
</div>
</footer>
);
}