텍스트 스타일 및 프리뷰 렌더링 정리
CI / test (push) Successful in 11m5s
CI / pr_and_merge (push) Successful in 1m40s
CI / test (pull_request) Failing after 35m42s
CI / pr_and_merge (pull_request) Has been skipped

This commit is contained in:
2025-11-20 23:36:47 +09:00
parent 9e40ee405c
commit 2f59e3781e
19 changed files with 3831 additions and 556 deletions
+227
View File
@@ -0,0 +1,227 @@
"use client";
import { useCallback } from "react";
import { useEditorStore } from "@/features/editor/state/editorStore";
// 좌측 블록/폼/템플릿 추가 사이드바를 분리한 컴포넌트
export function BlocksSidebar() {
const addTextBlock = useEditorStore((state) => state.addTextBlock);
const addButtonBlock = useEditorStore((state) => state.addButtonBlock);
const addImageBlock = useEditorStore((state) => state.addImageBlock);
const addDividerBlock = useEditorStore((state) => state.addDividerBlock);
const addListBlock = useEditorStore((state) => state.addListBlock);
const addSectionBlock = useEditorStore((state) => state.addSectionBlock);
const addFormBlock = useEditorStore((state) => state.addFormBlock);
const addFormInputBlock = useEditorStore((state) => (state as any).addFormInputBlock);
const addFormSelectBlock = useEditorStore((state) => (state as any).addFormSelectBlock);
const addFormCheckboxBlock = useEditorStore((state) => (state as any).addFormCheckboxBlock);
const addFormRadioBlock = useEditorStore((state) => (state as any).addFormRadioBlock);
const addHeroTemplateSection = useEditorStore((state) => state.addHeroTemplateSection);
const addFeaturesTemplateSection = useEditorStore((state) => state.addFeaturesTemplateSection);
const addCtaTemplateSection = useEditorStore((state) => state.addCtaTemplateSection);
const addFaqTemplateSection = useEditorStore((state) => state.addFaqTemplateSection);
const addPricingTemplateSection = useEditorStore((state) => state.addPricingTemplateSection);
const addTestimonialsTemplateSection = useEditorStore((state) => state.addTestimonialsTemplateSection);
const addBlogTemplateSection = useEditorStore((state) => state.addBlogTemplateSection);
const addTeamTemplateSection = useEditorStore((state) => state.addTeamTemplateSection);
const addFooterTemplateSection = useEditorStore((state) => state.addFooterTemplateSection);
// 버튼 핸들러를 useCallback으로 래핑해 불필요한 재생성을 줄인다.
const handleAddText = useCallback(() => addTextBlock(), [addTextBlock]);
const handleAddButton = useCallback(() => addButtonBlock(), [addButtonBlock]);
const handleAddImage = useCallback(() => addImageBlock(), [addImageBlock]);
const handleAddDivider = useCallback(() => addDividerBlock(), [addDividerBlock]);
const handleAddList = useCallback(() => addListBlock(), [addListBlock]);
const handleAddSection = useCallback(() => addSectionBlock(), [addSectionBlock]);
const handleAddFormBlock = useCallback(() => addFormBlock(), [addFormBlock]);
const handleAddFormInput = useCallback(() => addFormInputBlock(), [addFormInputBlock]);
const handleAddFormSelect = useCallback(() => addFormSelectBlock(), [addFormSelectBlock]);
const handleAddFormRadio = useCallback(() => addFormRadioBlock(), [addFormRadioBlock]);
const handleAddFormCheckbox = useCallback(() => addFormCheckboxBlock(), [addFormCheckboxBlock]);
const handleAddHeroTemplate = useCallback(
() => addHeroTemplateSection(),
[addHeroTemplateSection],
);
const handleAddFeaturesTemplate = useCallback(
() => addFeaturesTemplateSection(),
[addFeaturesTemplateSection],
);
const handleAddCtaTemplate = useCallback(() => addCtaTemplateSection(), [addCtaTemplateSection]);
const handleAddFaqTemplate = useCallback(() => addFaqTemplateSection(), [addFaqTemplateSection]);
const handleAddPricingTemplate = useCallback(
() => addPricingTemplateSection(),
[addPricingTemplateSection],
);
const handleAddTestimonialsTemplate = useCallback(
() => addTestimonialsTemplateSection(),
[addTestimonialsTemplateSection],
);
const handleAddBlogTemplate = useCallback(() => addBlogTemplateSection(), [addBlogTemplateSection]);
const handleAddTeamTemplate = useCallback(() => addTeamTemplateSection(), [addTeamTemplateSection]);
const handleAddFooterTemplate = useCallback(
() => addFooterTemplateSection(),
[addFooterTemplateSection],
);
return (
<aside className="w-60 border-r border-slate-800 p-4 text-sm space-y-3">
<h2 className="font-medium"></h2>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddText}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddButton}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddImage}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddDivider}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddList}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddSection}
>
</button>
<div className="pt-3 border-t border-slate-800 mt-3 space-y-2">
<h3 className="text-[11px] font-medium text-slate-300"> </h3>
<button
type="button"
className="w-full rounded border border-emerald-700 bg-emerald-950 px-3 py-2 text-left text-xs text-emerald-100 hover:bg-emerald-900"
onClick={handleAddFormBlock}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFormInput}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFormSelect}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFormRadio}
>
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFormCheckbox}
>
</button>
</div>
<div className="pt-3 border-t border-slate-800 mt-3 space-y-2">
<h3 className="text-[11px] font-medium text-slate-300">릿</h3>
<button
type="button"
className="w-full rounded border border-sky-700 bg-sky-950 px-3 py-2 text-left text-xs text-sky-100 hover:bg-sky-900"
onClick={handleAddHeroTemplate}
>
Hero 릿
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFeaturesTemplate}
>
Features 릿
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddCtaTemplate}
>
CTA 릿
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFaqTemplate}
>
FAQ 릿
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddPricingTemplate}
>
Pricing 릿
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddTestimonialsTemplate}
>
Testimonials 릿
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddBlogTemplate}
>
Blog 릿
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddTeamTemplate}
>
Team 릿
</button>
<button
type="button"
className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFooterTemplate}
>
Footer 릿
</button>
</div>
<p className="text-xs text-slate-500">
Text / Image / Button / Divider / List / Section .
</p>
</aside>
);
}
+203
View File
@@ -0,0 +1,203 @@
"use client";
import type { Block, ButtonBlockProps, FormBlockProps, ListBlockProps, SectionBlockProps, TextBlockProps } from "@/features/editor/state/editorStore";
import { ButtonPropertiesPanel } from "./ButtonPropertiesPanel";
import { TextPropertiesPanel } from "./TextPropertiesPanel";
import { ListPropertiesPanel } from "./ListPropertiesPanel";
import { DividerPropertiesPanel } from "./DividerPropertiesPanel";
import { ImagePropertiesPanel } from "./ImagePropertiesPanel";
import { SectionPropertiesPanel } from "./SectionPropertiesPanel";
import { FormInputPropertiesPanel } from "../forms/FormInputPropertiesPanel";
import { FormSelectPropertiesPanel } from "../forms/FormSelectPropertiesPanel";
import { FormCheckboxPropertiesPanel } from "../forms/FormCheckboxPropertiesPanel";
import { FormRadioPropertiesPanel } from "../forms/FormRadioPropertiesPanel";
import { FormControllerPanel } from "../forms/FormControllerPanel";
interface PropertiesSidebarProps {
blocks: Block[];
selectedBlockId: string | null;
updateBlock: (id: string, partial: any) => void;
removeBlock: (id: string) => void;
duplicateBlock: (id: string) => void;
// 텍스트 속성 패널에서 사용하는 인라인 편집 상태를 그대로 전달한다.
editingBlockId: string | null;
setEditingText: (value: string) => void;
}
// 우측 속성 패널을 분리한 컴포넌트
export function PropertiesSidebar(props: PropertiesSidebarProps) {
const {
blocks,
selectedBlockId,
updateBlock,
removeBlock,
duplicateBlock,
editingBlockId,
setEditingText,
} = props;
return (
<aside
className="w-80 p-4 text-sm border-l border-slate-800 flex flex-col gap-4 overflow-auto"
onKeyDownCapture={(e) => {
// 속성 패널 안에서 발생한 키 입력은 에디터 단축키/텍스트 블록 편집으로 전달되지 않도록 막는다.
e.stopPropagation();
}}
>
<h2 className="font-medium mb-2"> </h2>
{selectedBlockId ? (
<div className="space-y-3">
<div className="flex gap-2 text-[11px]">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-slate-100 hover:bg-red-900/60 hover:border-red-700"
onClick={() => removeBlock(selectedBlockId)}
>
</button>
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-2 py-1 text-slate-100 hover:bg-slate-800"
onClick={() => duplicateBlock(selectedBlockId)}
>
</button>
</div>
{(() => {
const selectedBlock = blocks.find((b) => b.id === selectedBlockId);
if (!selectedBlock) return null;
if (selectedBlock.type === "text") {
const textProps = selectedBlock.props as TextBlockProps;
return (
<TextPropertiesPanel
textProps={textProps}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
editingBlockId={editingBlockId}
setEditingText={setEditingText}
/>
);
}
if (selectedBlock.type === "button") {
const buttonProps = selectedBlock.props as ButtonBlockProps;
return (
<ButtonPropertiesPanel
buttonProps={buttonProps}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "list") {
const listProps = selectedBlock.props as ListBlockProps;
return (
<ListPropertiesPanel
listProps={listProps}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "divider") {
const dividerProps = selectedBlock.props as any;
return (
<DividerPropertiesPanel
dividerProps={dividerProps}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "image") {
const imageProps = selectedBlock.props as any;
return (
<ImagePropertiesPanel
imageProps={imageProps}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "section") {
const sectionProps = selectedBlock.props as SectionBlockProps;
return (
<SectionPropertiesPanel
sectionProps={sectionProps}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "formInput") {
return (
<FormInputPropertiesPanel
block={selectedBlock}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "formSelect") {
return (
<FormSelectPropertiesPanel
block={selectedBlock}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "formCheckbox") {
return (
<FormCheckboxPropertiesPanel
block={selectedBlock}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "formRadio") {
return (
<FormRadioPropertiesPanel
block={selectedBlock}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
if (selectedBlock.type === "form") {
return (
<FormControllerPanel
block={selectedBlock}
blocks={blocks}
selectedBlockId={selectedBlockId}
updateBlock={(id, partial) => updateBlock(id, partial as any)}
/>
);
}
return null;
})()}
</div>
) : (
<p className="text-xs text-slate-500"> .</p>
)}
</aside>
);
}