97 lines
4.3 KiB
TypeScript
97 lines
4.3 KiB
TypeScript
"use client";
|
||
|
||
import DisabledNotice from "@/components/disabled-notice";
|
||
import { FEATURES } from "@/lib/features";
|
||
import { use, useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import MetadataCard from "@/components/metadata-card";
|
||
import { InternalOnlyWarning, LicenseBadge } from "@/components/license-badge";
|
||
import { apiFetch, archiveKind, type ArchiveEntry } from "@/lib/api";
|
||
|
||
function ArchiveDetailPageInner({ params }: { params: Promise<{ slug: string }> }) {
|
||
const { slug } = use(params);
|
||
const [entry, setEntry] = useState<ArchiveEntry | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
apiFetch<ArchiveEntry>(`/api/archive/${slug}`).then(setEntry).catch((e) => setError(e.message));
|
||
}, [slug]);
|
||
|
||
if (error) return <p style={{ color: "rgb(255,122,122)" }}>{error}</p>;
|
||
if (!entry) return <p style={{ color: "var(--text-teal-1)" }}>불러오는 중…</p>;
|
||
|
||
return (
|
||
<div>
|
||
<div style={{ display: "flex", alignItems: "flex-start", marginBottom: "1rem" }}>
|
||
<Link href="/archive" className="btn-back">‹ 아카이브</Link>
|
||
</div>
|
||
<span className="tag" style={{ height: 20, padding: "0 8px", fontSize: "var(--text-xs)" }}>{archiveKind(entry)}</span>
|
||
<h1 className="page-title" style={{ textAlign: "left", margin: "0.5rem 0 1.5rem" }}>
|
||
{entry.metadata?.event_name ?? entry.name}
|
||
</h1>
|
||
|
||
<div className="archive-detail">
|
||
<div className="card" style={{ padding: "var(--s-4)" }}>
|
||
{entry.video_url ? (
|
||
<video src={entry.video_url} controls style={{ width: "100%", borderRadius: "var(--r-sm)", display: "block" }} />
|
||
) : (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img src={entry.poster_url} alt={entry.name} style={{ width: "100%", borderRadius: "var(--r-sm)" }} />
|
||
)}
|
||
{entry.video_url && (
|
||
<div style={{ display: "flex", justifyContent: "center", marginTop: "var(--s-3)" }}>
|
||
<a href={entry.video_url} download className="btn-tonal-mint">MP4 다운로드</a>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "var(--s-5)" }}>
|
||
{entry.metadata && <MetadataCard meta={entry.metadata} />}
|
||
|
||
{entry.narration && (
|
||
<div className="card" style={{ padding: "var(--s-5)" }}>
|
||
<p className="eyebrow" style={{ margin: "0 0 var(--s-3)" }}>
|
||
나레이션
|
||
</p>
|
||
<ol style={{ margin: 0, paddingLeft: 18, fontSize: 15, lineHeight: 2 }}>
|
||
{entry.narration.map((s, i) => <li key={i}>{s}</li>)}
|
||
</ol>
|
||
</div>
|
||
)}
|
||
|
||
{entry.f2_variants.length > 0 && (
|
||
<div>
|
||
<p className="eyebrow" style={{ margin: "0 0 var(--s-3)" }}>
|
||
스타일 변형
|
||
</p>
|
||
{entry.f2_variants.some((v) => v.license === "internal-only") && (
|
||
<InternalOnlyWarning />
|
||
)}
|
||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: "var(--s-3)", marginTop: "var(--s-3)" }}>
|
||
{entry.f2_variants.map((v) => (
|
||
<div key={v.template_id} className="card" style={{ overflow: "hidden" }}>
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img src={v.image_url} alt={v.name_ko ?? v.template_id} style={{ width: "100%", display: "block" }} />
|
||
<div style={{ padding: "var(--s-2)", display: "flex", flexDirection: "column", alignItems: "center", gap: 4 }}>
|
||
<p style={{ margin: 0, fontSize: 12, fontWeight: 600, color: "var(--text-teal-1)", textAlign: "center" }}>
|
||
{v.name_ko ?? v.template_id}
|
||
</p>
|
||
<LicenseBadge license={v.license ?? "internal-only"} />
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
export default function ArchiveDetailPage(props: Parameters<typeof ArchiveDetailPageInner>[0]) {
|
||
if (!FEATURES.archive) return <DisabledNotice title="아카이브" backHref="/" />;
|
||
return <ArchiveDetailPageInner {...props} />;
|
||
}
|