diff --git a/frontend/components/gate-card.tsx b/frontend/components/gate-card.tsx index 6afe96f..4fe7902 100644 --- a/frontend/components/gate-card.tsx +++ b/frontend/components/gate-card.tsx @@ -8,8 +8,8 @@ import { useState } from "react"; import { - GATE_META, type AnalysisReview, type ClipReview, type FetchReview, type FinalReview, - type GateKey, type GateReview, type NarrationReview, + GATE_META, type AnalysisReview, type ClipReview, type DetailSection, type FetchReview, + type FinalReview, type GateKey, type GateReview, type NarrationReview, } from "@/lib/playreel"; // ── 공통 셸 ────────────────────────────────────────────────────────── @@ -85,6 +85,46 @@ function Warn({ children }: { children: React.ReactNode }) { const two = { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "var(--spacing-page-md)", alignItems: "start" } as const; +// 상세페이지 조각은 라벨이 겹친다 — "유의사항"만 일곱 개인 페이지가 흔하다. +// 칩으로 그리면 같은 글자가 줄줄이 서서 무엇을 고르는지 알 수 없어 썸네일을 그린다. +function SectionPicker({ sections, onToggle }: { + sections: DetailSection[]; onToggle: (id: string) => void; +}) { + const chosen = sections.filter((s) => s.selected).length; + return ( + <> +

상세페이지에서 훑기 시작할 지점

+

+ 고른 지점부터 상세페이지를 이어서 훑습니다. 조각을 잘라 붙이는 것이 아닙니다. +

+
+ {sections.map((s, i) => ( + + ))} +
+

+ {sections.length}개 중 {chosen}개 선택 · 캐스팅 스케줄은 항상 포함됩니다. +

+ + ); +} + // ── ① 수집 확인 ─────────────────────────────────────────────────────── export function FetchGate({ data, onChange }: { data: FetchReview; onChange: (edits: { sections: string[]; meta: FetchReview["meta"] }) => void }) { const [sections, setSections] = useState(data.sections); @@ -106,18 +146,11 @@ export function FetchGate({ data, onChange }: { data: FetchReview; onChange: (ed 캐스트 · {meta.cast.join(", ")}

-

영상에 넣을 상세페이지 부분

-
- {sections.map((s) => ( - { const n = sections.map((x) => x.id === s.id ? { ...x, selected: !x.selected } : x); setSections(n); emit(n); }}> - {s.label} - - ))} -
-

- 캐스팅 스케줄은 항상 포함됩니다. -

+ { + const n = sections.map((x) => x.id === id ? { ...x, selected: !x.selected } : x); + setSections(n); emit(n); + }} /> {data.poster_width <= 800 && ( 포스터가 {data.poster_width}px로 작습니다. 다음 단계에서 화질을 2배 보정합니다(2크레딧). 원본 파일이 있으면 무빙포스터 경로에서 직접 올리는 편이 더 선명합니다. )} diff --git a/frontend/lib/playreel.ts b/frontend/lib/playreel.ts index 2c18834..c282a79 100644 --- a/frontend/lib/playreel.ts +++ b/frontend/lib/playreel.ts @@ -158,6 +158,23 @@ const PH = (w: number, h: number, text: string) => `${text}`, )}`; +// 실물이 오는 모양. 심규선 콘서트 상세페이지 실측 19조각이다 — 라벨이 겹치고 대부분 공지다. +// 이 자리에 6개짜리 깔끔한 목을 두었다가 게이트 ①이 같은 글자 열아홉 개만 뱉는 걸 놓쳤다. +const MOCK_SECTIONS: DetailSection[] = ([ + ["keyvisual", "키비주얼", 1108], ["synopsis", "작품 소개", 313], ["fragment", "조각", 244], + ["info", "관람 정보", 270], ["notice", "유의사항", 608], ["info", "관람 정보", 346], + ["fragment", "조각", 270], ["notice", "유의사항", 887], ["schedule", "캐스팅 스케줄", 582], + ["notice", "유의사항", 1149], ["notice", "유의사항", 968], ["info", "관람 정보", 383], + ["discount", "할인 안내", 473], ["fragment", "조각", 285], ["notice", "유의사항", 875], + ["notice", "유의사항", 1063], ["fragment", "조각", 152], ["notice", "유의사항", 1068], + ["info", "관람 정보", 371], +] as const).map(([tag, label, height], i) => ({ + id: `detail_01_s${String(i + 1).padStart(2, "0")}`, + tag, label, height, thumb_url: PH(180, 240, String(i + 1)), + required: tag === "schedule" || undefined, + selected: tag !== "fragment", +})); + export function mockJob(gate: GateKey | "running" | "done" | "failed"): PlayreelJob { const base: PlayreelJob = { id: "mock", kind: "playreel", name: "뮤지컬 〈겨울왕국〉", status: "awaiting_review", stage: null, @@ -178,14 +195,7 @@ export function mockJob(gate: GateKey | "running" | "done" | "failed"): Playreel return { ...base, gate, stages: stages(2), review: { gate, data: { poster_url: PH(300, 420, "포스터 750px"), poster_width: 750, meta: { title: "뮤지컬 〈겨울왕국〉", date_text: "2026.11.25 ~ 2027.03.01", place: "샤롯데씨어터", cast: ["박혜나", "정선아", "이지혜"], genre: "뮤지컬" }, - sections: [ - { id: "s1", tag: "story", label: "작품 소개", thumb_url: PH(160, 90, "소개"), height: 1800, selected: true }, - { id: "s2", tag: "awards", label: "수상·세계관", thumb_url: PH(160, 90, "수상"), height: 900, selected: true }, - { id: "s3", tag: "cast", label: "캐스트", thumb_url: PH(160, 90, "캐스트"), height: 1400, selected: true }, - { id: "s4", tag: "schedule", label: "캐스팅 스케줄", thumb_url: PH(160, 90, "스케줄"), height: 2200, required: true, selected: true }, - { id: "s5", tag: "discount", label: "할인 안내", thumb_url: PH(160, 90, "할인"), height: 700, selected: true }, - { id: "s6", tag: "notice", label: "유의사항", thumb_url: PH(160, 90, "유의"), height: 1200, selected: false }, - ], + sections: MOCK_SECTIONS, } } }; case "analysis_confirm": return { ...base, gate, stages: stages(4), credits_used: 2, review: { gate, data: {