Files
page-builder/src/app/editor/forms/FormCheckboxPropertiesPanel.tsx
T
jaybe 4e4c9cd37a
CI / test (push) Successful in 11m2s
CI / pr_and_merge (push) Successful in 1m19s
CI / test (pull_request) Successful in 51m33s
CI / pr_and_merge (pull_request) Has been skipped
섹션 삭제 후 텍스트 블록 추가 버그 수정 및 버튼/구분선 스타일-속성 패널 동기화
2025-11-22 15:18:32 +09:00

446 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import type { Block, FormCheckboxBlockProps } from "@/features/editor/state/editorStore";
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
interface FormCheckboxPropertiesPanelProps {
block: Block;
selectedBlockId: string | null;
updateBlock: (id: string, partial: any) => void;
}
export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBlock }: FormCheckboxPropertiesPanelProps) {
if (!selectedBlockId || block.id !== selectedBlockId) return null;
const checkboxProps = block.props as FormCheckboxBlockProps;
return (
<div className="space-y-3 text-xs">
<h3 className="text-[11px] font-semibold text-slate-200">체크박스 필드</h3>
<label className="flex flex-col gap-1">
<span className="text-slate-400">그룹 타이틀 타입</span>
<select
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
value={checkboxProps.groupLabelMode ?? "text"}
onChange={(e) =>
updateBlock(selectedBlockId, {
groupLabelMode: e.target.value,
} as any)
}
>
<option value="text">텍스트</option>
<option value="image">이미지</option>
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-400">그룹 타이틀</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"
value={checkboxProps.groupLabel ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
groupLabel: e.target.value,
} as any)
}
/>
</label>
{checkboxProps.groupLabelMode === "image" && (
<label className="flex flex-col gap-1">
<span className="text-slate-400">그룹 타이틀 이미지 URL</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"
value={checkboxProps.groupLabelImageUrl ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
groupLabelImageUrl: e.target.value,
} as any)
}
/>
</label>
)}
<label className="flex flex-col gap-1">
<span className="text-slate-400">전송 </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"
value={checkboxProps.formFieldName ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
formFieldName: e.target.value,
} as any)
}
/>
</label>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-400">옵션</span>
<button
type="button"
className="rounded border border-slate-700 bg-slate-900 px-2 py-0.5 text-[10px] text-slate-200 hover:bg-slate-800"
onClick={() => {
const next = Array.isArray((checkboxProps as any).options)
? [...(checkboxProps as any).options]
: [];
next.push({
label: "새 옵션",
value: `option_${next.length + 1}`,
});
updateBlock(selectedBlockId, {
options: next,
} as any);
}}
>
옵션 추가
</button>
</div>
<div className="space-y-2">
{(((checkboxProps as any).options ?? []) as any[]).map((opt, index) => (
<div key={opt.value ?? index} className="space-y-1 rounded border border-slate-800 p-2">
<div className="flex items-center gap-1">
<input
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
placeholder="라벨"
value={opt.label ?? ""}
onChange={(e) => {
const next = [...(((checkboxProps as any).options ?? []) as any[])];
next[index] = {
...next[index],
label: e.target.value,
};
updateBlock(selectedBlockId, {
options: next,
} as any);
}}
/>
<input
className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
placeholder="값(value)"
value={opt.value ?? ""}
onChange={(e) => {
const next = [...(((checkboxProps as any).options ?? []) as any[])];
next[index] = {
...next[index],
value: e.target.value,
};
updateBlock(selectedBlockId, {
options: next,
} as any);
}}
/>
<button
type="button"
className="h-7 w-7 rounded border border-slate-700 bg-slate-950 text-[10px] text-slate-300 hover:bg-red-900/60 hover:border-red-700"
onClick={() => {
const next = (((checkboxProps as any).options ?? []) as any[]).filter((_, i) => i !== index);
updateBlock(selectedBlockId, {
options: next,
} as any);
}}
>
×
</button>
</div>
<div className="flex flex-col gap-1">
<input
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
placeholder="옵션 이미지 URL (선택)"
value={opt.labelImageUrl ?? ""}
onChange={(e) => {
const next = [...(((checkboxProps as any).options ?? []) as any[])];
next[index] = {
...next[index],
labelImageUrl: e.target.value,
};
updateBlock(selectedBlockId, {
options: next,
} as any);
}}
/>
</div>
</div>
))}
</div>
</div>
<label className="inline-flex items-center gap-2">
<input
type="checkbox"
className="h-3 w-3 rounded border border-slate-600 bg-slate-900"
checked={Boolean(checkboxProps.required)}
onChange={(e) =>
updateBlock(selectedBlockId, {
required: e.target.checked,
} as any)
}
/>
<span className="text-slate-400">필수 필드</span>
</label>
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3">
<h4 className="text-[11px] font-semibold text-slate-200">필드 스타일</h4>
{/* px 기반 체크박스 타이포 입력값을 노출하여 em 스케일 변환을 위한 근거를 남긴다 */}
<NumericPropertyControl
label="체크박스 텍스트 크기 (px)"
unitLabel="(px)"
value={(() => {
const raw = checkboxProps.fontSizeCustom ?? "";
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
if (match) return Number(match[1]);
return 14;
})()}
min={8}
max={40}
step={1}
presets={[
{ id: "xs", label: "XS", value: 12 },
{ id: "sm", label: "S", value: 14 },
{ id: "md", label: "M", value: 16 },
{ id: "lg", label: "L", value: 18 },
{ id: "xl", label: "XL", value: 20 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
fontSizeCustom: `${v}px`,
} as any);
}}
/>
{/* 줄간격 px 입력값을 노출해 preview em 변환과 TDD 케이스를 연동한다 */}
<NumericPropertyControl
label="체크박스 줄간격 (px)"
unitLabel="(px)"
value={(() => {
const raw = checkboxProps.lineHeightCustom ?? "";
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
if (match) return Number(match[1]);
return 20;
})()}
min={8}
max={60}
step={1}
presets={[
{ id: "tight", label: "타이트", value: 18 },
{ id: "normal", label: "보통", value: 24 },
{ id: "loose", label: "루즈", value: 32 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
lineHeightCustom: `${v}px`,
} as any);
}}
/>
{/* 자간 px 입력을 통한 폼 체크박스 미세 조정값을 노출 */}
<NumericPropertyControl
label="체크박스 자간 (px)"
unitLabel="(px)"
value={(() => {
const raw = checkboxProps.letterSpacingCustom ?? "";
const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/);
if (match) return Number(match[1]);
return 0;
})()}
min={-10}
max={20}
step={0.5}
presets={[
{ id: "tight", label: "좁게", value: -1 },
{ id: "normal", label: "보통", value: 0 },
{ id: "wide", label: "넓게", value: 2 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
letterSpacingCustom: `${v}px`,
} as any);
}}
/>
<label className="flex flex-col gap-1 text-[11px] text-slate-400">
<span>필드 너비</span>
<select
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
value={checkboxProps.widthMode ?? "full"}
onChange={(e) =>
updateBlock(selectedBlockId, {
widthMode: e.target.value,
} as any)
}
>
<option value="auto">자동</option>
<option value="full">전체 </option>
<option value="fixed">고정 </option>
</select>
</label>
{(checkboxProps.widthMode ?? "full") === "fixed" && (
<NumericPropertyControl
label="필드 고정 너비"
unitLabel="(px)"
value={checkboxProps.widthPx ?? 240}
min={80}
max={800}
step={10}
presets={[
{ id: "sm", label: "작게", value: 200 },
{ id: "md", label: "보통", value: 280 },
{ id: "lg", label: "넓게", value: 360 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
widthPx: v,
} as any);
}}
/>
)}
<NumericPropertyControl
label="체크박스 가로 패딩 (px)"
unitLabel="(px)"
value={typeof checkboxProps.paddingX === "number" ? checkboxProps.paddingX : 12}
min={0}
max={60}
step={2}
presets={[
{ id: "xs", label: "아주 얇게", value: 6 },
{ id: "sm", label: "얇게", value: 10 },
{ id: "md", label: "보통", value: 12 },
{ id: "lg", label: "넓게", value: 16 },
{ id: "xl", label: "아주 넓게", value: 20 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
paddingX: v,
} as any)
}
/>
<NumericPropertyControl
label="체크박스 세로 패딩 (px)"
unitLabel="(px)"
value={typeof checkboxProps.paddingY === "number" ? checkboxProps.paddingY : 10}
min={0}
max={40}
step={1}
presets={[
{ id: "xs", label: "아주 얇게", value: 6 },
{ id: "sm", label: "얇게", value: 8 },
{ id: "md", label: "보통", value: 10 },
{ id: "lg", label: "넓게", value: 12 },
{ id: "xl", label: "아주 넓게", value: 16 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
paddingY: v,
} as any)
}
/>
<NumericPropertyControl
label="체크박스 옵션 간격 (px)"
unitLabel="(px)"
value={typeof checkboxProps.optionGapPx === "number" ? checkboxProps.optionGapPx : 4}
min={0}
max={40}
step={1}
presets={[
{ id: "tight", label: "타이트", value: 2 },
{ id: "normal", label: "보통", value: 4 },
{ id: "relaxed", label: "느슨", value: 8 },
{ id: "extra", label: "넓게", value: 12 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
optionGapPx: v,
} as any)
}
/>
<ColorPickerField
label="텍스트 색상"
ariaLabelColorInput="체크박스 텍스트 색상 피커"
ariaLabelHexInput="체크박스 텍스트 색상 HEX"
value={
checkboxProps.textColorCustom && checkboxProps.textColorCustom.trim() !== ""
? checkboxProps.textColorCustom
: "#f9fafb"
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
textColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
textColorCustom: item.color,
} as any);
}}
/>
<ColorPickerField
label="배경 색상"
ariaLabelColorInput="체크박스 배경 색상 피커"
ariaLabelHexInput="체크박스 배경 색상 HEX"
value={
checkboxProps.fillColorCustom && checkboxProps.fillColorCustom.trim() !== ""
? checkboxProps.fillColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
fillColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
fillColorCustom: item.color,
} as any);
}}
/>
<ColorPickerField
label="테두리 색상"
ariaLabelColorInput="체크박스 테두리 색상 피커"
ariaLabelHexInput="체크박스 테두리 색상 HEX"
value={
checkboxProps.strokeColorCustom && checkboxProps.strokeColorCustom.trim() !== ""
? checkboxProps.strokeColorCustom
: TEXT_COLOR_PALETTE[0]?.color ?? "#0ea5e9"
}
onChange={(hex) => {
updateBlock(selectedBlockId, {
strokeColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
strokeColorCustom: item.color,
} as any);
}}
/>
<NumericPropertyControl
label="필드 모서리 둥글기"
value={(() => {
const r = checkboxProps.borderRadius ?? "md";
return r === "none" ? 0 : r === "sm" ? 2 : r === "lg" ? 6 : r === "full" ? 8 : 4;
})()}
min={0}
max={8}
step={1}
presets={[
{ id: "none", label: "없음", value: 0 },
{ id: "sm", label: "작게", value: 2 },
{ id: "md", label: "보통", value: 4 },
{ id: "lg", label: "크게", value: 6 },
{ id: "full", label: "완전 둥글게", value: 8 },
]}
onChangeValue={(v) => {
const next =
v <= 0 ? "none" : v <= 2 ? "sm" : v <= 5 ? "md" : v <= 7 ? "lg" : "full";
updateBlock(selectedBlockId, {
borderRadius: next,
} as any);
}}
/>
</div>
</div>
);
}