191 lines
6.1 KiB
TypeScript
191 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import type { CSSProperties } from "react";
|
|
import { useEffect } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { useEditorStore } from "@/features/editor/state/editorStore";
|
|
import { PublicPageRenderer } from "@/features/editor/components/PublicPageRenderer";
|
|
|
|
export default function PreviewPage() {
|
|
const router = useRouter();
|
|
const blocks = useEditorStore((state) => state.blocks);
|
|
const projectConfig = useEditorStore((state) => (state as any).projectConfig);
|
|
const replaceBlocks = useEditorStore((state) => state.replaceBlocks);
|
|
const updateProjectConfig = useEditorStore((state) => state.updateProjectConfig);
|
|
const resetHistory = useEditorStore((state) => (state as any).resetHistory as () => void);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const checkAuth = async () => {
|
|
try {
|
|
const res = await fetch("/api/auth/me");
|
|
if (!res.ok && res.status === 401 && !cancelled) {
|
|
router.push("/login");
|
|
}
|
|
} catch {
|
|
// 네트워크 오류 등은 일단 무시하고, 페이지 접근을 막지 않는다.
|
|
}
|
|
};
|
|
|
|
void checkAuth();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [router]);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === "undefined") return;
|
|
|
|
let slug = (projectConfig as any)?.slug?.trim?.();
|
|
|
|
if (!slug) {
|
|
try {
|
|
const url = new URL(window.location.href);
|
|
const fromQuery = url.searchParams.get("slug")?.trim();
|
|
|
|
if (fromQuery && fromQuery !== slug) {
|
|
slug = fromQuery;
|
|
updateProjectConfig({ slug: fromQuery } as any);
|
|
}
|
|
} catch {
|
|
// URL 파싱 오류는 무시하고, autosave 복원 없이 진행한다.
|
|
}
|
|
}
|
|
|
|
if (!slug) return;
|
|
|
|
const key = `pb:autosave:${slug}`;
|
|
const raw = window.localStorage.getItem(key);
|
|
if (!raw) return;
|
|
|
|
try {
|
|
const parsed = JSON.parse(raw) as { blocks?: any[]; projectConfig?: any };
|
|
if (Array.isArray(parsed.blocks)) {
|
|
replaceBlocks(parsed.blocks as any);
|
|
}
|
|
if (parsed.projectConfig) {
|
|
updateProjectConfig(parsed.projectConfig as any);
|
|
}
|
|
|
|
// 프리뷰 진입 시 autosave 프로젝트를 복원하는 경우에도 에디터 히스토리는 새 프로젝트 기준으로만 유지되도록 초기화한다.
|
|
resetHistory();
|
|
} catch {
|
|
return;
|
|
}
|
|
}, [projectConfig?.slug, replaceBlocks, updateProjectConfig]);
|
|
|
|
const canvasStyle: CSSProperties = {};
|
|
const preset = projectConfig?.canvasPreset ?? "full";
|
|
|
|
const mainStyle: CSSProperties = {};
|
|
if (projectConfig?.bodyBgColorHex && projectConfig.bodyBgColorHex.trim()) {
|
|
mainStyle.backgroundColor = projectConfig.bodyBgColorHex;
|
|
}
|
|
|
|
const handleExportZip = async () => {
|
|
try {
|
|
const payload = {
|
|
blocks,
|
|
projectConfig,
|
|
};
|
|
|
|
const response = await fetch("/api/export", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return;
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const slug = (projectConfig?.slug ?? "").trim() || "page-builder-export";
|
|
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = `${slug}.zip`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
} catch (error) {
|
|
console.error("프리뷰 ZIP 내보내기 중 오류", error);
|
|
}
|
|
};
|
|
|
|
const widthPx =
|
|
typeof projectConfig?.canvasWidthPx === "number" && projectConfig.canvasWidthPx > 0
|
|
? projectConfig.canvasWidthPx
|
|
: null;
|
|
|
|
if (widthPx != null) {
|
|
canvasStyle.maxWidth = `${widthPx}px`;
|
|
} else if (preset === "mobile") {
|
|
canvasStyle.maxWidth = "390px";
|
|
} else if (preset === "tablet") {
|
|
canvasStyle.maxWidth = "768px";
|
|
} else if (preset === "desktop") {
|
|
canvasStyle.maxWidth = "1200px";
|
|
}
|
|
|
|
if (projectConfig?.canvasBgColorHex && projectConfig.canvasBgColorHex.trim()) {
|
|
canvasStyle.backgroundColor = projectConfig.canvasBgColorHex;
|
|
}
|
|
|
|
return (
|
|
<main className="min-h-screen flex flex-col bg-slate-950 text-slate-50" style={mainStyle}>
|
|
<header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-xl font-semibold">Page Preview</h1>
|
|
<p className="text-xs text-slate-400">빌더로 만든 페이지를 미리 보는 화면</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-xs">
|
|
<button
|
|
type="button"
|
|
className="inline-flex items-center rounded border border-slate-700 bg-slate-900 px-3 py-1 text-slate-100 hover:bg-slate-800"
|
|
onClick={() => {
|
|
void handleExportZip();
|
|
}}
|
|
>
|
|
페이지 파일로 내보내기 (ZIP)
|
|
</button>
|
|
<Link
|
|
href="/projects"
|
|
className="inline-flex items-center rounded border border-slate-700 bg-slate-900 px-3 py-1 text-xs text-slate-100 hover:bg-slate-800"
|
|
>
|
|
프로젝트 목록
|
|
</Link>
|
|
<Link
|
|
href="/editor"
|
|
className="inline-flex items-center rounded border border-slate-700 bg-slate-900 px-3 py-1 text-xs text-slate-100 hover:bg-slate-800"
|
|
>
|
|
에디터로 돌아가기
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
{/* 에디터 크롬 없이 순수 페이지 형태로 블록들을 렌더링하되, projectConfig 에 따라 최대 폭을 제한한다. */}
|
|
<section className="flex flex-1 overflow-auto">
|
|
<div className="flex-1 flex justify-center px-4 py-6">
|
|
<div
|
|
data-testid="preview-canvas-inner"
|
|
className="w-full"
|
|
style={canvasStyle}
|
|
>
|
|
<PublicPageRenderer
|
|
blocks={blocks}
|
|
projectSlug={(projectConfig?.slug ?? "").trim() || undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|