1차 싱크 완료
CI / test (push) Failing after 41m13s
CI / pr_and_merge (push) Has been skipped

This commit is contained in:
2025-11-24 21:32:37 +09:00
parent 7a8ad7c057
commit 672cca5271
83 changed files with 6681 additions and 325 deletions
+7 -1
View File
@@ -42,6 +42,7 @@ export function EditorCanvas(props: EditorCanvasProps) {
projectConfig,
} = props;
const canvasOuterStyle: CSSProperties = {};
const canvasInnerStyle: CSSProperties = {};
const preset = projectConfig.canvasPreset ?? "full";
const widthPx =
@@ -59,14 +60,19 @@ export function EditorCanvas(props: EditorCanvasProps) {
canvasInnerStyle.maxWidth = "1200px";
}
if (projectConfig.bodyBgColorHex && projectConfig.bodyBgColorHex.trim()) {
canvasOuterStyle.backgroundColor = projectConfig.bodyBgColorHex;
}
if (projectConfig.canvasBgColorHex && projectConfig.canvasBgColorHex.trim()) {
canvasInnerStyle.backgroundColor = projectConfig.canvasBgColorHex;
}
return (
<div
className="flex-1 p-4 flex flex-col gap-2 text-sm text-slate-200 border-r border-slate-800 overflow-auto"
className="flex-1 p-4 flex flex-col gap-2 text-sm text-slate-200 border-r border-slate-800 overflow-auto pb-scroll"
data-testid="editor-canvas"
style={canvasOuterStyle}
onClick={(event) => {
if (event.target === event.currentTarget) {
onCanvasEmptyClick();
@@ -2,6 +2,7 @@
import type { Block, ButtonBlockProps, FormBlockProps } from "@/features/editor/state/editorStore";
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
interface FormControllerPanelProps {
block: Block; // type === "form"
@@ -188,6 +189,61 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
/>
</div>
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<ColorPickerField
label="폼 배경색"
ariaLabelColorInput="폼 배경색 피커"
ariaLabelHexInput="폼 배경색 HEX"
value={
formProps.backgroundColorCustom && formProps.backgroundColorCustom.trim() !== ""
? formProps.backgroundColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#e5e7eb"
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
backgroundColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
backgroundColorCustom: item.color,
} as any);
}}
/>
</div>
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
placeholder="예: 성공적으로 전송되었습니다."
value={formProps.successMessage ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
successMessage: e.target.value,
} as any)
}
/>
</label>
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> </span>
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
placeholder="예: 전송 중 오류가 발생했습니다."
value={formProps.errorMessage ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
errorMessage: e.target.value,
} as any)
}
/>
</label>
</div>
<div className="space-y-3 border-t border-slate-800 pt-3 mt-4">
<h3 className="text-[11px] font-semibold text-slate-200"> </h3>
+89 -17
View File
@@ -113,17 +113,14 @@ export default function EditorPage() {
const [projectSlug, setProjectSlug] = useState("");
const [loadSlug, setLoadSlug] = useState("");
const [projectMessage, setProjectMessage] = useState("");
// 멀티 선택(MVP)을 위한 로컬 선택 상태: 단일 선택(selectedBlockId)과 병행 사용한다.
const [selectedBlockIds, setSelectedBlockIds] = useState<string[]>([]);
const [menuOpen, setMenuOpen] = useState(false);
const [activeModal, setActiveModal] = useState<"project" | "json" | null>(null);
const sensors = useSensors(useSensor(PointerSensor));
const editorMainStyle: CSSProperties = {};
if (projectConfig.bodyBgColorHex && projectConfig.bodyBgColorHex.trim()) {
editorMainStyle.backgroundColor = projectConfig.bodyBgColorHex;
}
const startEditing = (id: string, initialText: string) => {
selectBlock(id);
setEditingBlockId(id);
@@ -474,6 +471,20 @@ export default function EditorPage() {
// ArrowUp/ArrowDown을 이용한 선택 블록 이동을 처리한다.
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement | null;
if (target) {
const tagName = target.tagName;
const isInputLike =
tagName === "INPUT" ||
tagName === "TEXTAREA" ||
(target as HTMLElement).isContentEditable;
if (isInputLike) {
// 입력 포커스 상태에서는 에디터 전역 단축키를 비활성화하고,
// 기본 브라우저 편집 동작(삭제/커서 이동 등)을 그대로 둔다.
return;
}
}
// 방향키로 선택된 블록을 위/아래로 이동 (순서 변경이 아니라 선택만 이동)
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
const { blocks: currentBlocks, selectedBlockId: currentSelectedId, selectBlock: storeSelectBlock } =
@@ -518,9 +529,20 @@ export default function EditorPage() {
const isMac = navigator.platform.toLowerCase().includes("mac");
const metaKey = isMac ? event.metaKey : event.ctrlKey;
// Delete / Backspace 로 선택된 블록 삭제
// Delete / Backspace 로 선택된 블록 삭제 (멀티 선택 우선)
if (event.key === "Delete" || event.key === "Backspace") {
const { selectedBlockId: currentSelectedId } = (useEditorStore as any).getState();
const { selectedBlockId: currentSelectedId, blocks: currentBlocks } = (useEditorStore as any).getState();
// 멀티 선택이 존재하면 멀티 삭제를 우선 처리한다.
if (selectedBlockIds.length > 1) {
event.preventDefault();
const selectedSet = new Set(selectedBlockIds);
const nextBlocks = (currentBlocks as Block[]).filter((b) => !selectedSet.has(b.id));
replaceBlocks(nextBlocks);
return;
}
// 단일 선택만 있는 경우 기존 removeBlock 로직 사용
if (currentSelectedId) {
event.preventDefault();
removeBlock(currentSelectedId);
@@ -559,11 +581,12 @@ export default function EditorPage() {
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [undo, redo]);
}, [undo, redo, selectedBlockIds, replaceBlocks, removeBlock]);
const renderBlocks = (targetBlocks: Block[]) =>
targetBlocks.map((block) => {
const isSelected = block.id === selectedBlockId;
const isSelected =
(selectedBlockId != null && block.id === selectedBlockId) || selectedBlockIds.includes(block.id);
const isEditing = block.id === editingBlockId;
return (
@@ -572,6 +595,9 @@ export default function EditorPage() {
<SortableEditorBlock
block={block}
isSelected={isSelected}
selectedBlockId={selectedBlockId ?? null}
selectedBlockIds={selectedBlockIds}
setSelectedBlockIds={setSelectedBlockIds}
isEditing={isEditing}
editingText={editingText}
startEditing={startEditing}
@@ -590,7 +616,7 @@ export default function EditorPage() {
});
return (
<main className="min-h-screen flex flex-col" style={editorMainStyle}>
<main className="h-screen flex flex-col overflow-hidden">
<header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between relative z-20">
<div className="flex items-baseline gap-3">
<h1 className="text-xl font-semibold">Page Editor</h1>
@@ -867,6 +893,9 @@ interface TextInlineToolbarProps {
interface SortableEditorBlockProps {
block: Block;
isSelected: boolean;
selectedBlockId: string | null;
selectedBlockIds: string[];
setSelectedBlockIds: React.Dispatch<React.SetStateAction<string[]>>;
isEditing: boolean;
editingText: string;
startEditing: (id: string, initialText: string) => void;
@@ -884,6 +913,9 @@ interface SortableEditorBlockProps {
function SortableEditorBlock({
block,
isSelected,
selectedBlockId,
selectedBlockIds,
setSelectedBlockIds,
isEditing,
editingText,
startEditing,
@@ -966,6 +998,11 @@ function SortableEditorBlock({
colorClass = `pb-text-color-${colorPalette}`;
}
// 블록 배경색: backgroundColorCustom 이 설정된 경우 편집기 텍스트 컨테이너 배경에도 반영한다.
if (textProps.backgroundColorCustom && textProps.backgroundColorCustom.trim() !== "") {
textStyleOverrides.backgroundColor = textProps.backgroundColorCustom.trim();
}
// 최대 너비: 커스텀 값이 있으면 우선 사용, 없으면 scale 프리셋
const maxWidthScale = textProps.maxWidthScale ?? "none";
if (textProps.maxWidthCustom && textProps.maxWidthCustom.trim() !== "") {
@@ -1027,15 +1064,29 @@ function SortableEditorBlock({
ref={setNodeRef}
style={{ ...baseStyle, ...textStyleOverrides }}
data-testid="editor-block"
data-block-id={block.id}
data-selected={isSelected ? "true" : "false"}
aria-selected={isSelected ? "true" : "false"}
className={`rounded border px-3 py-2 cursor-text transition-colors ${alignClass} ${sizeClass} ${leadingClass} ${weightClass} ${colorClass} ${maxWidthClass} ${decorationClass} ${
isSelected ? "border-sky-500 bg-slate-900/80" : "border-slate-700 bg-slate-900"
}`}
onClick={(event) => {
// 섹션 안 자식 블록을 클릭했을 때 섹션까지 선택되는 것을 막기 위해
// 클릭 이벤트를 여기서 중단하고 현재 블록만 선택한다.
// 멀티 선택(MVP): Cmd/Ctrl+클릭 시 선택 토글, 그 외에는 단일 선택으로 전환한다.
const isMeta = event.metaKey || event.ctrlKey;
if (isMeta) {
event.stopPropagation();
setSelectedBlockIds((prev) => {
if (prev.includes(block.id)) {
return prev.filter((id) => id !== block.id);
}
return [...prev, block.id];
});
return;
}
// 일반 클릭: 현재 블록만 단일 선택으로 유지한다.
event.stopPropagation();
setSelectedBlockIds([block.id]);
selectBlock(block.id);
}}
onDoubleClick={(event) => {
@@ -1358,7 +1409,7 @@ function SortableEditorBlock({
else if (checkboxRadius === "lg" || checkboxRadius === "full") fieldStyle.borderRadius = 9999;
return (
<div className="flex flex-col gap-1 text-xs text-slate-200">
<div className="flex flex-col gap-1 text-xs text-slate-200" style={fieldStyle}>
{/* 체크박스 그룹 타이틀도 텍스트/이미지 모드를 지원한다. */}
{groupLabelMode === "image" && checkboxProps.groupLabelImageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
@@ -1402,7 +1453,9 @@ function SortableEditorBlock({
const variant = buttonProps.variant ?? "solid";
const colorPalette = buttonProps.colorPalette ?? "primary";
const radius = buttonProps.borderRadius ?? "md";
const fullWidth = buttonProps.fullWidth ?? false;
const baseFullWidth = buttonProps.fullWidth ?? false;
const widthMode = buttonProps.widthMode ?? (baseFullWidth ? "full" : "auto");
const isFullWidth = widthMode === "full" || baseFullWidth;
const sizeClassBtn =
size === "xs"
@@ -1445,6 +1498,9 @@ function SortableEditorBlock({
if (buttonProps.textColorCustom && buttonProps.textColorCustom.trim() !== "") {
buttonStyle.color = buttonProps.textColorCustom;
}
if (widthMode === "fixed" && typeof buttonProps.widthPx === "number" && buttonProps.widthPx > 0) {
buttonStyle.width = `${buttonProps.widthPx}px`;
}
if (typeof buttonProps.paddingX === "number" && buttonProps.paddingX >= 0) {
buttonStyle.paddingInline = `${buttonProps.paddingX}px`;
}
@@ -1453,7 +1509,7 @@ function SortableEditorBlock({
}
return (
<div className={fullWidth ? "w-full" : "inline-block"}>
<div className={isFullWidth ? "w-full" : "inline-block"}>
<button
type="button"
className={`pb-btn-base ${sizeClassBtn} ${radiusClassBtn} ${variantClassBtn} w-full`}
@@ -1708,6 +1764,12 @@ function SortableEditorBlock({
})()}
{block.type === "section" && (() => {
const sectionProps = block.props as SectionBlockProps;
const isSectionSelected =
isSelected ||
(selectedBlockId != null &&
allBlocks.some(
(candidate) => candidate.id === selectedBlockId && candidate.sectionId === block.id,
));
const bgClass =
sectionProps.background === "muted"
@@ -1723,6 +1785,13 @@ function SortableEditorBlock({
? "py-10"
: "py-6";
const alignItemsClass =
sectionProps.alignItems === "center"
? "items-center"
: sectionProps.alignItems === "bottom"
? "items-end"
: "items-start";
const columns = sectionProps.columns && sectionProps.columns.length > 0
? sectionProps.columns
: [{ id: `${block.id}_col_fallback`, span: 12 }];
@@ -1738,7 +1807,10 @@ function SortableEditorBlock({
return (
<div
className={`w-full ${bgClass} ${pyClass} rounded border border-dashed border-slate-700`}
data-testid="editor-section"
className={`w-full ${bgClass} ${pyClass} rounded border ${
isSectionSelected ? "border-sky-500" : "border-dashed border-slate-700"
}`}
style={sectionStyle}
>
<div
@@ -1751,7 +1823,7 @@ function SortableEditorBlock({
}}
>
<div
className="flex"
className={`flex ${alignItemsClass}`}
style={{
columnGap:
typeof sectionProps.gapXPx === "number" && sectionProps.gapXPx > 0
+207 -65
View File
@@ -68,7 +68,7 @@ export function BlocksSidebar() {
);
return (
<aside className="w-60 border-r border-slate-800 p-4 text-sm space-y-3">
<aside className="w-60 border-r border-slate-800 p-4 text-sm space-y-3 overflow-y-auto pb-scroll">
<h2 className="font-medium"></h2>
<button
type="button"
@@ -152,71 +152,213 @@ export function BlocksSidebar() {
</button>
</div>
<div className="pt-3 border-t border-slate-800 mt-3 space-y-2">
<div className="pt-3 border-t border-slate-800 mt-3 space-y-3">
<h3 className="text-[11px] font-medium text-slate-300">릿</h3>
<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={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 className="space-y-2">
<p className="text-[10px] font-semibold text-slate-400"> · CTA</p>
<div className="space-y-2">
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddHeroTemplate}
>
Hero 릿
</button>
<div
data-testid="template-preview-hero"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="flex-1 bg-slate-700/70" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug">
Hero ( + + )
</p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddCtaTemplate}
>
CTA 릿
</button>
<div
data-testid="template-preview-cta"
className="shrink-0 flex h-6 w-10 items-center justify-center gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="h-2 w-6 rounded bg-slate-700/70" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug">(CTA) </p>
</div>
</div>
</div>
<div className="space-y-2">
<p className="text-[10px] font-semibold text-slate-400"> </p>
<div className="space-y-2">
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFeaturesTemplate}
>
Features 릿
</button>
<div
data-testid="template-preview-features"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="flex-1 bg-slate-700/70" />
<div className="flex-1 bg-slate-700/50" />
<div className="flex-1 bg-slate-700/70" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug">3 </p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddFaqTemplate}
>
FAQ 릿
</button>
<div
data-testid="template-preview-faq"
className="shrink-0 flex h-6 w-10 flex-col justify-center gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="h-[2px] w-full bg-slate-700/70" />
<div className="h-[2px] w-full bg-slate-700/40" />
<div className="h-[2px] w-full bg-slate-700/70" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug"> (FAQ) </p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddPricingTemplate}
>
Pricing 릿
</button>
<div
data-testid="template-preview-pricing"
className="shrink-0 flex h-6 w-10 items-end gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="h-2 flex-1 bg-slate-700/40" />
<div className="h-4 flex-1 bg-slate-700/70" />
<div className="h-3 flex-1 bg-slate-700/50" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug">/ </p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddBlogTemplate}
>
Blog 릿
</button>
<div
data-testid="template-preview-blog"
className="shrink-0 flex h-6 w-10 items-stretch gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="flex-1 bg-slate-700/70" />
<div className="flex-1 bg-slate-700/50" />
<div className="flex-1 bg-slate-700/70" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug"> </p>
</div>
</div>
</div>
<div className="space-y-2">
<p className="text-[10px] font-semibold text-slate-400">/</p>
<div className="space-y-2">
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddTestimonialsTemplate}
>
Testimonials 릿
</button>
<div
data-testid="template-preview-testimonials"
className="shrink-0 flex h-6 w-10 flex-col justify-center gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="h-[2px] w-full bg-slate-700/70" />
<div className="h-[2px] w-3/4 bg-slate-700/50" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug"> (Testimonials) </p>
</div>
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 rounded border border-slate-700 bg-slate-900 px-3 py-2 text-left text-xs hover:bg-slate-800"
onClick={handleAddTeamTemplate}
>
Team 릿
</button>
<div
data-testid="template-preview-team"
className="shrink-0 flex h-6 w-10 items-end gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="h-3 flex-1 bg-slate-700/60" />
<div className="h-4 flex-1 bg-slate-700/80" />
<div className="h-2 flex-1 bg-slate-700/50" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug"> </p>
</div>
</div>
</div>
<div className="space-y-2">
<p className="text-[10px] font-semibold text-slate-400">/</p>
<div className="space-y-2">
<div className="rounded border border-slate-800 bg-slate-950/50 p-2 space-y-1">
<div className="flex items-center justify-between gap-2">
<button
type="button"
className="flex-1 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
data-testid="template-preview-footer"
className="shrink-0 flex h-6 w-10 flex-col justify-end gap-[2px] rounded border border-slate-700 bg-slate-900"
>
<div className="h-[2px] w-full bg-slate-700/40" />
<div className="h-[2px] w-3/4 bg-slate-700/70" />
</div>
</div>
<p className="text-[10px] text-slate-400 leading-snug"> </p>
</div>
</div>
</div>
</div>
</aside>
);
@@ -341,18 +341,6 @@ export function ButtonPropertiesPanel({ buttonProps, selectedBlockId, updateBloc
}}
/>
</div>
<div className="space-y-1">
<label className="flex items-center justify-between text-xs text-slate-400">
<span> </span>
<input
type="checkbox"
checked={buttonProps.fullWidth ?? false}
onChange={(e) => {
updateBlock(selectedBlockId, { fullWidth: e.target.checked } as any);
}}
/>
</label>
</div>
</>
);
}
@@ -2,6 +2,7 @@
import type { ImageBlockProps } from "@/features/editor/state/editorStore";
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
export type ImagePropertiesPanelProps = {
imageProps: ImageBlockProps;
@@ -127,6 +128,21 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3 text-xs text-slate-400">
<h4 className="text-[11px] font-semibold text-slate-200"> </h4>
{/* 카드 배경색 */}
<div className="space-y-1">
<ColorPickerField
label="카드 배경색"
ariaLabelColorInput="이미지 카드 배경색 피커"
ariaLabelHexInput="이미지 카드 배경색 HEX"
value={imageProps.backgroundColorCustom ?? ""}
onChange={(hex) => {
const next = hex && hex.trim().length > 0 ? hex : undefined;
updateBlock(selectedBlockId, { backgroundColorCustom: next } as any);
}}
palette={TEXT_COLOR_PALETTE}
/>
</div>
{/* 정렬 */}
<label className="flex flex-col gap-1">
<span></span>
@@ -167,6 +167,17 @@ export function ListPropertiesPanel({
}}
/>
<ColorPickerField
label="블록 배경색"
ariaLabelColorInput="리스트 배경색 피커"
ariaLabelHexInput="리스트 배경색 HEX"
value={listProps.backgroundColorCustom ?? ""}
onChange={(hex) => {
updateBlock(selectedBlockId, { backgroundColorCustom: hex } as any);
}}
palette={TEXT_COLOR_PALETTE}
/>
{/* 불릿 스타일 (● / ○ / ■ / 숫자형 / 없음) */}
<label className="flex flex-col gap-1">
<span>릿 </span>
+1 -1
View File
@@ -50,7 +50,7 @@ export function PropertiesSidebar(props: PropertiesSidebarProps) {
return (
<aside
data-testid="properties-sidebar"
className="w-80 p-4 text-sm border-l border-slate-800 flex flex-col gap-4 overflow-auto"
className="w-80 p-4 text-sm border-l border-slate-800 flex flex-col gap-4 overflow-auto pb-scroll"
onKeyDownCapture={(e) => {
// 속성 패널 안에서 발생한 키 입력은 에디터 단축키/텍스트 블록 편집으로 전달되지 않도록 막는다.
e.stopPropagation();
@@ -181,7 +181,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
// 1컬럼인 경우: 12 고정 표시
if (colCount === 1) {
const only = columns[0];
const only = columns[0] ?? { id: `${selectedBlockId}_col_1`, span: 12 };
return (
<label key={only.id} className="flex flex-col gap-1">
<span>{`1열 폭 (고정 12/12)`}</span>
@@ -437,6 +437,21 @@ export function TextPropertiesPanel({
/>
</div>
<div className="space-y-1">
<ColorPickerField
label="블록 배경색"
ariaLabelColorInput="텍스트 블록 배경색 피커"
ariaLabelHexInput="텍스트 블록 배경색 HEX"
value={textProps.backgroundColorCustom ?? ""}
onChange={(hex) => {
updateBlock(selectedBlockId, {
backgroundColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
/>
</div>
<div className="space-y-1">
<label className="flex flex-col gap-1 text-xs text-slate-400">
<span> </span>