"use client";
import type { Block, FormRadioBlockProps } from "@/features/editor/state/editorStore";
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
interface FormRadioPropertiesPanelProps {
block: Block;
selectedBlockId: string | null;
updateBlock: (id: string, partial: any) => void;
}
export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }: FormRadioPropertiesPanelProps) {
if (!selectedBlockId || block.id !== selectedBlockId) return null;
const radioProps = block.props as FormRadioBlockProps;
return (
라디오 필드
{radioProps.groupLabelMode === "image" && (() => {
const source: "url" | "upload" =
radioProps.groupLabelImageSource ??
(radioProps.groupLabelImageUrl && radioProps.groupLabelImageUrl.startsWith("/api/image/")
? "upload"
: "url");
return (
{source === "url" && (
)}
{source === "upload" && (
)}
);
})()}
옵션
{(((radioProps as any).options ?? []) as any[]).map((opt, index) => (
{
const next = [...(((radioProps as any).options ?? []) as any[])];
next[index] = {
...next[index],
label: e.target.value,
};
updateBlock(selectedBlockId, {
options: next,
} as any);
}}
/>
{
const next = [...(((radioProps as any).options ?? []) as any[])];
next[index] = {
...next[index],
value: e.target.value,
};
updateBlock(selectedBlockId, {
options: next,
} as any);
}}
/>
{(() => {
const source: "url" | "upload" =
radioProps.optionImageSource ??
(opt.labelImageUrl && opt.labelImageUrl.startsWith("/api/image/") ? "upload" : "url");
return (
<>
{source === "url" && (
{
const next = [...(((radioProps as any).options ?? []) as any[])];
next[index] = {
...next[index],
labelImageUrl: e.target.value,
};
updateBlock(selectedBlockId, {
options: next,
} as any);
}}
/>
)}
{source === "upload" && (
)}
>
);
})()}
))}
필드 스타일
{/* 라디오 타이포 px 값을 직접 입력받아 em 스케일 변환 로직과 연동한다 */}
{
const raw = radioProps.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 변환 근거로 사용 */}
{
const raw = radioProps.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 입력 컨트롤로 프리뷰 적용 근거 제공 */}
{
const raw = radioProps.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);
}}
/>
{(radioProps.widthMode ?? "full") === "fixed" && (
{
updateBlock(selectedBlockId, {
widthPx: v,
} as any);
}}
/>
)}
updateBlock(selectedBlockId, {
paddingX: v,
} as any)
}
/>
updateBlock(selectedBlockId, {
paddingY: v,
} as any)
}
/>
updateBlock(selectedBlockId, {
optionGapPx: v,
} as any)
}
/>
{
updateBlock(selectedBlockId, {
textColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
textColorCustom: item.color,
} as any);
}}
/>
{
updateBlock(selectedBlockId, {
fillColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
fillColorCustom: item.color,
} as any);
}}
/>
{
updateBlock(selectedBlockId, {
strokeColorCustom: hex,
} as any);
}}
palette={TEXT_COLOR_PALETTE}
onPaletteSelect={(item) => {
updateBlock(selectedBlockId, {
strokeColorCustom: item.color,
} as any);
}}
/>
{
const r = radioProps.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);
}}
/>
);
}