194 lines
8.9 KiB
TypeScript
194 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEditorStore } from "@/features/editor/state/editorStore";
|
|
import type { CanvasPreset, ProjectConfig } from "@/features/editor/state/editorStore";
|
|
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
|
|
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
|
|
|
|
export function ProjectPropertiesPanel() {
|
|
const projectConfig = useEditorStore((state) => (state as any).projectConfig as ProjectConfig);
|
|
const updateProjectConfig = useEditorStore(
|
|
(state) => (state as any).updateProjectConfig as (partial: Partial<ProjectConfig>) => void,
|
|
);
|
|
|
|
const handleChangePreset = (preset: CanvasPreset) => {
|
|
if (preset === "mobile") {
|
|
updateProjectConfig({ canvasPreset: preset, canvasWidthPx: 390 });
|
|
} else if (preset === "tablet") {
|
|
updateProjectConfig({ canvasPreset: preset, canvasWidthPx: 768 });
|
|
} else if (preset === "desktop") {
|
|
updateProjectConfig({ canvasPreset: preset, canvasWidthPx: 1200 });
|
|
} else if (preset === "full") {
|
|
updateProjectConfig({ canvasPreset: preset, canvasWidthPx: undefined });
|
|
} else {
|
|
updateProjectConfig({ canvasPreset: preset });
|
|
}
|
|
};
|
|
|
|
const handleChangeCanvasWidth = (value: number) => {
|
|
if (value === 390) {
|
|
handleChangePreset("mobile");
|
|
} else if (value === 768) {
|
|
handleChangePreset("tablet");
|
|
} else if (value === 1200) {
|
|
handleChangePreset("desktop");
|
|
} else {
|
|
updateProjectConfig({ canvasPreset: "custom", canvasWidthPx: value });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4 text-xs text-slate-900 dark:text-slate-200">
|
|
<h3 className="text-sm font-medium text-slate-100">프로젝트 설정</h3>
|
|
|
|
<div className="space-y-1">
|
|
<label className="flex flex-col gap-1">
|
|
<span className="text-slate-500 dark:text-slate-400">프로젝트 제목</span>
|
|
<input
|
|
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
aria-label="프로젝트 제목"
|
|
value={projectConfig.title}
|
|
onChange={(e) => updateProjectConfig({ title: e.target.value })}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="flex flex-col gap-1">
|
|
<span className="text-slate-500 dark:text-slate-400">프로젝트 주소 (slug)</span>
|
|
<input
|
|
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
aria-label="프로젝트 주소 (slug)"
|
|
value={projectConfig.slug}
|
|
onChange={(e) => updateProjectConfig({ slug: e.target.value })}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<NumericPropertyControl
|
|
label="캔버스 너비"
|
|
unitLabel="(px)"
|
|
value={projectConfig.canvasWidthPx ?? 1024}
|
|
min={320}
|
|
max={1920}
|
|
step={10}
|
|
presets={[
|
|
{ id: "mobile", label: "모바일 (390px)", value: 390 },
|
|
{ id: "tablet", label: "태블릿 (768px)", value: 768 },
|
|
{ id: "desktop", label: "데스크톱 (1200px)", value: 1200 },
|
|
]}
|
|
onChangeValue={handleChangeCanvasWidth}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<ColorPickerField
|
|
label="캔버스 배경색"
|
|
ariaLabelColorInput="캔버스 배경색"
|
|
ariaLabelHexInput="캔버스 배경색 HEX"
|
|
value={projectConfig.canvasBgColorHex ?? "#020617"}
|
|
onChange={(hex) => updateProjectConfig({ canvasBgColorHex: hex })}
|
|
palette={TEXT_COLOR_PALETTE}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<ColorPickerField
|
|
label="페이지 배경색"
|
|
ariaLabelColorInput="페이지 배경색"
|
|
ariaLabelHexInput="페이지 배경색 HEX"
|
|
value={projectConfig.bodyBgColorHex ?? "#020617"}
|
|
onChange={(hex) => updateProjectConfig({ bodyBgColorHex: hex })}
|
|
palette={TEXT_COLOR_PALETTE}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2 border-t border-slate-800 pt-3">
|
|
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">SEO / 메타</h4>
|
|
|
|
<label className="flex flex-col gap-1">
|
|
<span className="text-slate-500 dark:text-slate-400">SEO 타이틀</span>
|
|
<input
|
|
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
aria-label="SEO 타이틀"
|
|
placeholder={projectConfig.title || "페이지 제목"}
|
|
value={projectConfig.seoTitle ?? ""}
|
|
onChange={(e) => updateProjectConfig({ seoTitle: e.target.value })}
|
|
/>
|
|
</label>
|
|
|
|
<label className="flex flex-col gap-1">
|
|
<span className="text-slate-500 dark:text-slate-400">메타 디스크립션</span>
|
|
<textarea
|
|
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-900 outline-none focus:border-sky-500 min-h-[56px] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
aria-label="메타 디스크립션"
|
|
placeholder="검색엔진 및 SNS 공유에 노출될 페이지 설명을 입력하세요."
|
|
value={projectConfig.seoDescription ?? ""}
|
|
onChange={(e) => updateProjectConfig({ seoDescription: e.target.value })}
|
|
/>
|
|
</label>
|
|
|
|
<label className="flex flex-col gap-1">
|
|
<span className="text-slate-500 dark:text-slate-400">OG/Twitter 이미지 URL</span>
|
|
<input
|
|
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
aria-label="OG/Twitter 이미지 URL"
|
|
placeholder="예: https://example.com/og-image.png"
|
|
value={projectConfig.seoOgImageUrl ?? ""}
|
|
onChange={(e) => updateProjectConfig({ seoOgImageUrl: e.target.value })}
|
|
/>
|
|
</label>
|
|
|
|
<label className="flex flex-col gap-1">
|
|
<span className="text-slate-500 dark:text-slate-400">Canonical URL</span>
|
|
<input
|
|
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
aria-label="Canonical URL"
|
|
placeholder="예: https://example.com/landing"
|
|
value={projectConfig.seoCanonicalUrl ?? ""}
|
|
onChange={(e) => updateProjectConfig({ seoCanonicalUrl: e.target.value })}
|
|
/>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2 text-[11px] text-slate-600 dark:text-slate-300">
|
|
<input
|
|
type="checkbox"
|
|
className="h-3 w-3 rounded border-slate-300 bg-white text-sky-600 dark:border-slate-600 dark:bg-slate-900"
|
|
aria-label="검색 엔진에 노출하지 않기 (noindex)"
|
|
checked={Boolean(projectConfig.seoNoIndex)}
|
|
onChange={(e) => updateProjectConfig({ seoNoIndex: e.target.checked })}
|
|
/>
|
|
<span>검색 엔진에 노출하지 않기 (noindex)</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="flex flex-col gap-1">
|
|
<span className="text-slate-500 dark:text-slate-400">페이지 head HTML</span>
|
|
<textarea
|
|
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] font-mono text-slate-900 outline-none focus:border-sky-500 min-h-[72px] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
aria-label="페이지 head HTML"
|
|
value={projectConfig.headHtml ?? ""}
|
|
onChange={(e) => updateProjectConfig({ headHtml: e.target.value })}
|
|
placeholder="예: <meta name="description" content="..." />"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="flex flex-col gap-1">
|
|
<span className="text-slate-500 dark:text-slate-400">추적 스크립트</span>
|
|
<textarea
|
|
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] font-mono text-slate-900 outline-none focus:border-sky-500 min-h-[72px] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
aria-label="추적 스크립트"
|
|
value={projectConfig.trackingScript ?? ""}
|
|
onChange={(e) => updateProjectConfig({ trackingScript: e.target.value })}
|
|
placeholder="예: <script>/* GA, Pixel 코드 */</script>"
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|