i18n 적용
CI / test (push) Failing after 11m12s
CI / e2e (push) Has been cancelled
CI / pr_and_merge (push) Has been cancelled

This commit is contained in:
2025-12-10 15:56:51 +09:00
parent 73e9bc6a1c
commit f71207aeb5
127 changed files with 7346 additions and 2079 deletions
@@ -3,6 +3,8 @@
import type { Block, FormInputBlockProps } from "@/features/editor/state/editorStore";
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
import { useAppLocale } from "@/features/i18n/LocaleProvider";
import { getEditorFormInputPanelMessages } from "@/features/i18n/messages/editorFormInputPanel";
interface FormInputPropertiesPanelProps {
block: Block;
@@ -11,6 +13,8 @@ interface FormInputPropertiesPanelProps {
}
export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }: FormInputPropertiesPanelProps) {
const locale = useAppLocale();
const m = getEditorFormInputPanelMessages(locale);
if (!selectedBlockId || block.id !== selectedBlockId || block.type !== "formInput") return null;
const inputProps = block.props as FormInputBlockProps;
@@ -18,9 +22,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
return (
<div className="space-y-3 text-xs">
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">{m.sectionTitle}</h3>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400">{m.labelTypeLabel}</span>
<select
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 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={inputProps.labelMode ?? "text"}
@@ -30,12 +34,12 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
} as any)
}
>
<option value="text"></option>
<option value="image"></option>
<option value="text">{m.labelTypeOptionText}</option>
<option value="image">{m.labelTypeOptionImage}</option>
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400">{m.fieldLabelLabel}</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"
value={inputProps.label ?? ""}
@@ -47,7 +51,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400">{m.labelDisplayLabel}</span>
<select
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 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={labelDisplay}
@@ -57,14 +61,14 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
} as any)
}
>
<option value="visible"> ()</option>
<option value="hidden"></option>
<option value="floating"> </option>
<option value="visible">{m.labelDisplayOptionVisible}</option>
<option value="hidden">{m.labelDisplayOptionHidden}</option>
<option value="floating">{m.labelDisplayOptionFloating}</option>
</select>
</label>
{labelDisplay === "visible" && (
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark:text-slate-400">
<span></span>
<span>{m.layoutLabel}</span>
<select
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 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={inputProps.labelLayout ?? "stacked"}
@@ -74,15 +78,15 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
} as any)
}
>
<option value="stacked"> ()</option>
<option value="inline"> ()</option>
<option value="stacked">{m.layoutOptionStacked}</option>
<option value="inline">{m.layoutOptionInline}</option>
</select>
</label>
)}
{labelDisplay === "visible" && (inputProps.labelLayout ?? "stacked") === "inline" && (
<NumericPropertyControl
label="라벨/필드 간격 (px)"
unitLabel="(px)"
label={m.labelGapLabel}
unitLabel={m.labelGapUnitLabel}
value={typeof inputProps.labelGapPx === "number" ? inputProps.labelGapPx : 8}
min={0}
max={80}
@@ -103,7 +107,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
{inputProps.labelMode === "image" && (
<>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400"> URL</span>
<span className="text-slate-500 dark:text-slate-400">{m.labelImageUrlLabel}</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"
value={inputProps.labelImageUrl ?? ""}
@@ -115,7 +119,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400">{m.labelImageAltLabel}</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"
value={inputProps.labelImageAlt ?? ""}
@@ -129,7 +133,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
</>
)}
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400">{m.submitKeyLabel}</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"
value={inputProps.formFieldName ?? ""}
@@ -141,10 +145,10 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400">{m.fieldTypeLabel}</span>
<select
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 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="필드 타입"
aria-label={m.fieldTypeAria}
value={inputProps.inputType ?? "text"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -152,16 +156,16 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
} as any)
}
>
<option value="text"></option>
<option value="email"></option>
<option value="textarea"> </option>
<option value="text">{m.fieldTypeOptionText}</option>
<option value="email">{m.fieldTypeOptionEmail}</option>
<option value="textarea">{m.fieldTypeOptionTextarea}</option>
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400">Placeholder</span>
<span className="text-slate-500 dark:text-slate-400">{m.placeholderLabel}</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="Placeholder"
aria-label={m.placeholderAria}
value={inputProps.placeholder ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -171,13 +175,13 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
</label>
<label className="inline-flex items-center gap-2">
<span className="text-slate-500 dark:text-slate-400"> .</span>
<span className="text-slate-500 dark:text-slate-400">{m.requiredNoticeText}</span>
</label>
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3">
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h4>
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">{m.styleSectionTitle}</h4>
<NumericPropertyControl
label="필드 텍스트 크기 (px)"
unitLabel="(px)"
label={m.textSizeLabel}
unitLabel={m.textSizeUnitLabel}
value={(() => {
const raw = inputProps.fontSizeCustom ?? "";
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
@@ -202,8 +206,8 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
{/* 줄간격 px 입력을 노출하여 preview em 변환의 근거를 제공 */}
<NumericPropertyControl
label="필드 줄간격 (px)"
unitLabel="(px)"
label={m.lineHeightLabel}
unitLabel={m.lineHeightUnitLabel}
value={(() => {
const raw = inputProps.lineHeightCustom ?? "";
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
@@ -226,8 +230,8 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
{/* 자간 px 입력으로 폼 입력 타이포를 세밀하게 조정 */}
<NumericPropertyControl
label="필드 자간 (px)"
unitLabel="(px)"
label={m.letterSpacingLabel}
unitLabel={m.letterSpacingUnitLabel}
value={(() => {
const raw = inputProps.letterSpacingCustom ?? "";
const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/);
@@ -249,7 +253,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
}}
/>
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark:text-slate-400">
<span> </span>
<span>{m.textAlignLabel}</span>
<select
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 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
value={inputProps.align ?? "left"}
@@ -259,16 +263,16 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
} as any)
}
>
<option value="left"></option>
<option value="center"></option>
<option value="right"></option>
<option value="left">{m.textAlignOptionLeft}</option>
<option value="center">{m.textAlignOptionCenter}</option>
<option value="right">{m.textAlignOptionRight}</option>
</select>
</label>
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark:text-slate-400">
<span></span>
<span>{m.widthModeLabel}</span>
<select
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 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="너비"
aria-label={m.widthModeAria}
value={inputProps.widthMode ?? "full"}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -276,14 +280,14 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
} as any)
}
>
<option value="auto"></option>
<option value="full"> </option>
<option value="fixed"> </option>
<option value="auto">{m.widthModeOptionAuto}</option>
<option value="full">{m.widthModeOptionFull}</option>
<option value="fixed">{m.widthModeOptionFixed}</option>
</select>
</label>
<NumericPropertyControl
label="필드 고정 너비"
unitLabel="(px)"
label={m.fixedWidthLabel}
unitLabel={m.fixedWidthUnitLabel}
value={inputProps.widthPx ?? 240}
min={80}
max={800}
@@ -300,8 +304,8 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
}}
/>
<NumericPropertyControl
label="필드 가로 패딩 (px)"
unitLabel="(px)"
label={m.paddingXLabel}
unitLabel={m.paddingXUnitLabel}
value={typeof inputProps.paddingX === "number" ? inputProps.paddingX : 12}
min={0}
max={60}
@@ -320,8 +324,8 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
}}
/>
<NumericPropertyControl
label="필드 세로 패딩 (px)"
unitLabel="(px)"
label={m.paddingYLabel}
unitLabel={m.paddingYUnitLabel}
value={typeof inputProps.paddingY === "number" ? inputProps.paddingY : 10}
min={0}
max={40}
@@ -340,9 +344,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
}}
/>
<ColorPickerField
label="필드 텍스트 색상"
ariaLabelColorInput="필드 텍스트 색상 피커"
ariaLabelHexInput="필드 텍스트 색상 HEX"
label={m.textColorLabel}
ariaLabelColorInput={m.textColorPickerAria}
ariaLabelHexInput={m.textColorHexAria}
value={
inputProps.textColorCustom && inputProps.textColorCustom.trim() !== ""
? inputProps.textColorCustom
@@ -361,9 +365,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
}}
/>
<ColorPickerField
label="필드 채움 색상"
ariaLabelColorInput="필드 채움 색상 피커"
ariaLabelHexInput="필드 채움 색상 HEX"
label={m.fillColorLabel}
ariaLabelColorInput={m.fillColorPickerAria}
ariaLabelHexInput={m.fillColorHexAria}
value={
inputProps.fillColorCustom && inputProps.fillColorCustom.trim() !== ""
? inputProps.fillColorCustom
@@ -382,9 +386,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
}}
/>
<ColorPickerField
label="필드 테두리 색상"
ariaLabelColorInput="필드 테두리 색상 피커"
ariaLabelHexInput="필드 테두리 색상 HEX"
label={m.strokeColorLabel}
ariaLabelColorInput={m.strokeColorPickerAria}
ariaLabelHexInput={m.strokeColorHexAria}
value={
inputProps.strokeColorCustom && inputProps.strokeColorCustom.trim() !== ""
? inputProps.strokeColorCustom
@@ -403,7 +407,7 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
}}
/>
<NumericPropertyControl
label="필드 모서리 둥글기"
label={m.borderRadiusLabel}
value={(() => {
const r = inputProps.borderRadius ?? "md";
return r === "none" ? 0 : r === "sm" ? 2 : r === "lg" ? 6 : r === "full" ? 8 : 4;
@@ -412,15 +416,14 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
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 },
{ id: "none", label: m.borderRadiusPresetNone, value: 0 },
{ id: "sm", label: m.borderRadiusPresetSmall, value: 2 },
{ id: "md", label: m.borderRadiusPresetMedium, value: 4 },
{ id: "lg", label: m.borderRadiusPresetLarge, value: 6 },
{ id: "full", label: m.borderRadiusPresetFull, value: 8 },
]}
onChangeValue={(v) => {
const next =
v <= 0 ? "none" : v <= 2 ? "sm" : v <= 5 ? "md" : v <= 7 ? "lg" : "full";
const next = v <= 0 ? "none" : v <= 2 ? "sm" : v <= 5 ? "md" : v <= 7 ? "lg" : "full";
updateBlock(selectedBlockId, {
borderRadius: next,
} as any);