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, FormCheckboxBlockProps } 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 { getEditorFormCheckboxPanelMessages } from "@/features/i18n/messages/editorFormCheckboxPanel";
interface FormCheckboxPropertiesPanelProps {
block: Block;
@@ -11,6 +13,8 @@ interface FormCheckboxPropertiesPanelProps {
}
export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBlock }: FormCheckboxPropertiesPanelProps) {
const locale = useAppLocale();
const m = getEditorFormCheckboxPanelMessages(locale);
if (!selectedBlockId || block.id !== selectedBlockId) return null;
const checkboxProps = block.props as FormCheckboxBlockProps;
@@ -18,10 +22,10 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
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.groupTitleTypeLabel}</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={checkboxProps.groupLabelMode ?? "text"}
@@ -31,13 +35,13 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
} as any)
}
>
<option value="text"></option>
<option value="image"></option>
<option value="text">{m.groupTitleTypeOptionText}</option>
<option value="image">{m.groupTitleTypeOptionImage}</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.groupTitleDisplayLabel}</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={groupLabelDisplay}
@@ -47,14 +51,14 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
} as any)
}
>
<option value="visible"> ()</option>
<option value="hidden"></option>
<option value="visible">{m.groupTitleDisplayOptionVisible}</option>
<option value="hidden">{m.groupTitleDisplayOptionHidden}</option>
</select>
</label>
{groupLabelDisplay === "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={checkboxProps.labelLayout ?? "stacked"}
@@ -64,14 +68,14 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
} as any)
}
>
<option value="stacked"> ()</option>
<option value="inline"> ()</option>
<option value="stacked">{m.layoutOptionStacked}</option>
<option value="inline">{m.layoutOptionInline}</option>
</select>
</label>
)}
<label className="flex flex-col gap-1 text-[11px] text-slate-500 dark:text-slate-400">
<span> </span>
<span>{m.optionLayoutLabel}</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={checkboxProps.optionLayout ?? "stacked"}
@@ -81,14 +85,14 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
} as any)
}
>
<option value="stacked"> ()</option>
<option value="inline"> ()</option>
<option value="stacked">{m.layoutOptionStacked}</option>
<option value="inline">{m.layoutOptionInline}</option>
</select>
</label>
{groupLabelDisplay === "visible" && (checkboxProps.labelLayout ?? "stacked") === "inline" && (
<NumericPropertyControl
label="라벨/필드 간격 (px)"
label={m.labelGapLabel}
unitLabel="(px)"
value={typeof checkboxProps.labelGapPx === "number" ? checkboxProps.labelGapPx : 8}
min={0}
@@ -109,7 +113,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
)}
<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.groupTitleLabel}</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={checkboxProps.groupLabel ?? ""}
@@ -131,7 +135,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
return (
<div className="space-y-1">
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-400">{m.groupTitleImageSourceLabel}</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={source}
@@ -141,14 +145,14 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
} as any)
}
>
<option value="url">URL</option>
<option value="upload"> </option>
<option value="url">{m.groupTitleImageSourceOptionUrl}</option>
<option value="upload">{m.groupTitleImageSourceOptionUpload}</option>
</select>
</label>
{source === "url" && (
<label className="flex flex-col gap-1">
<span className="text-slate-400"> URL</span>
<span className="text-slate-400">{m.groupTitleImageUrlLabel}</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={checkboxProps.groupLabelImageUrl ?? ""}
@@ -163,12 +167,12 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
{source === "upload" && (
<label className="flex flex-col gap-1">
<span className="text-slate-400"> </span>
<span className="text-slate-400">{m.groupTitleImageUploadLabel}</span>
<input
type="file"
accept="image/*"
className="w-full text-[11px] text-slate-300 file:mr-2 file:rounded file:border file:border-slate-700 file:bg-slate-800 file:px-2 file:py-1 file:text-[11px] file:text-slate-100 hover:file:bg-slate-700"
aria-label="그룹 타이틀 이미지 파일 업로드"
aria-label={m.groupTitleImageUploadAria}
onChange={async (event) => {
const file = event.target.files?.[0];
if (!file) return;
@@ -208,7 +212,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
})()}
<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={checkboxProps.formFieldName ?? ""}
@@ -222,7 +226,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-500 dark:text-slate-400"></span>
<span className="text-slate-500 dark:text-slate-400">{m.optionsLabel}</span>
<button
type="button"
className="rounded border border-slate-300 bg-white px-2 py-0.5 text-[10px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
@@ -239,12 +243,12 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
} as any);
}}
>
{m.addOptionButtonLabel}
</button>
</div>
<div className="space-y-2">
<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.optionImageSourceLabel}</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={(() => {
@@ -259,8 +263,8 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
} as any)
}
>
<option value="url">URL</option>
<option value="upload"> </option>
<option value="url">{m.optionImageSourceOptionUrl}</option>
<option value="upload">{m.optionImageSourceOptionUpload}</option>
</select>
</label>
@@ -269,7 +273,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-center gap-1">
<input
className="flex-1 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"
placeholder="라벨"
placeholder={m.optionLabelPlaceholder}
value={opt.label ?? ""}
onChange={(e) => {
const next = [...(((checkboxProps as any).options ?? []) as any[])];
@@ -284,7 +288,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
/>
<input
className="flex-1 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"
placeholder="값(value)"
placeholder={m.optionValuePlaceholder}
value={opt.value ?? ""}
onChange={(e) => {
const next = [...(((checkboxProps as any).options ?? []) as any[])];
@@ -321,7 +325,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
{source === "url" && (
<input
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"
placeholder="옵션 이미지 URL (선택)"
placeholder={m.optionImageUrlPlaceholder}
value={opt.labelImageUrl ?? ""}
onChange={(e) => {
const next = [...(((checkboxProps as any).options ?? []) as any[])];
@@ -337,12 +341,12 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
)}
{source === "upload" && (
<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.optionImageUploadLabel}</span>
<input
type="file"
accept="image/*"
className="w-full text-[11px] text-slate-300 file:mr-2 file:rounded file:border file:border-slate-700 file:bg-slate-800 file:px-2 file:py-1 file:text-[11px] file:text-slate-100 hover:file:bg-slate-700"
aria-label="옵션 이미지 파일 업로드"
aria-label={m.optionImageUploadAria}
onChange={async (event) => {
const file = event.target.files?.[0];
if (!file) return;
@@ -393,15 +397,15 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
</div>
<label className="inline-flex items-center gap-2">
<span className="text-slate-500"> .</span>
<span className="text-slate-500">{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>
{/* px 기반 체크박스 타이포 입력값을 노출하여 em 스케일 변환을 위한 근거를 남긴다 */}
<NumericPropertyControl
label="체크박스 텍스트 크기 (px)"
unitLabel="(px)"
label={m.textSizeLabel}
unitLabel={m.textSizeUnitLabel}
value={(() => {
const raw = checkboxProps.fontSizeCustom ?? "";
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
@@ -418,16 +422,16 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
{ id: "lg", label: "L", value: 18 },
{ id: "xl", label: "XL", value: 20 },
]}
onChangeValue={(v) => {
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
fontSizeCustom: `${v}px`,
} as any);
}}
} as any)
}
/>
{/* 줄간격 px 입력값을 노출해 preview em 변환과 TDD 케이스를 연동한다 */}
<NumericPropertyControl
label="체크박스 줄간격 (px)"
unitLabel="(px)"
label={m.lineHeightLabel}
unitLabel={m.lineHeightUnitLabel}
value={(() => {
const raw = checkboxProps.lineHeightCustom ?? "";
const match = raw.match(/([0-9]+(?:\.[0-9]+)?)/);
@@ -442,16 +446,16 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
{ id: "normal", label: "보통", value: 24 },
{ id: "loose", label: "루즈", value: 32 },
]}
onChangeValue={(v) => {
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
lineHeightCustom: `${v}px`,
} as any);
}}
} as any)
}
/>
{/* 자간 px 입력을 통한 폼 체크박스 미세 조정값을 노출 */}
<NumericPropertyControl
label="체크박스 자간 (px)"
unitLabel="(px)"
label={m.letterSpacingLabel}
unitLabel={m.letterSpacingUnitLabel}
value={(() => {
const raw = checkboxProps.letterSpacingCustom ?? "";
const match = raw.match(/(-?[0-9]+(?:\.[0-9]+)?)/);
@@ -466,14 +470,14 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
{ id: "normal", label: "보통", value: 0 },
{ id: "wide", label: "넓게", value: 2 },
]}
onChangeValue={(v) => {
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
letterSpacingCustom: `${v}px`,
} as any);
}}
} as any)
}
/>
<label className="flex flex-col gap-1 text-[11px] 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"
value={checkboxProps.widthMode ?? "full"}
@@ -483,15 +487,15 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
} 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>
{(checkboxProps.widthMode ?? "full") === "fixed" && (
<NumericPropertyControl
label="필드 고정 너비"
unitLabel="(px)"
label={m.fixedWidthLabel}
unitLabel={m.fixedWidthUnitLabel}
value={checkboxProps.widthPx ?? 240}
min={80}
max={800}
@@ -501,16 +505,16 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
{ id: "md", label: "보통", value: 280 },
{ id: "lg", label: "넓게", value: 360 },
]}
onChangeValue={(v) => {
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
widthPx: v,
} as any);
}}
} as any)
}
/>
)}
<NumericPropertyControl
label="체크박스 가로 패딩 (px)"
unitLabel="(px)"
label={m.paddingXLabel}
unitLabel={m.paddingXUnitLabel}
value={typeof checkboxProps.paddingX === "number" ? checkboxProps.paddingX : 12}
min={0}
max={60}
@@ -529,8 +533,8 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
}
/>
<NumericPropertyControl
label="체크박스 세로 패딩 (px)"
unitLabel="(px)"
label={m.paddingYLabel}
unitLabel={m.paddingYUnitLabel}
value={typeof checkboxProps.paddingY === "number" ? checkboxProps.paddingY : 10}
min={0}
max={40}
@@ -549,8 +553,8 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
}
/>
<NumericPropertyControl
label="체크박스 옵션 간격 (px)"
unitLabel="(px)"
label={m.optionGapLabel}
unitLabel={m.optionGapUnitLabel}
value={typeof checkboxProps.optionGapPx === "number" ? checkboxProps.optionGapPx : 4}
min={0}
max={40}
@@ -568,9 +572,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
}
/>
<ColorPickerField
label="텍스트 색상"
ariaLabelColorInput="체크박스 텍스트 색상 피커"
ariaLabelHexInput="체크박스 텍스트 색상 HEX"
label={m.textColorLabel}
ariaLabelColorInput={m.textColorPickerAria}
ariaLabelHexInput={m.textColorHexAria}
value={
checkboxProps.textColorCustom && checkboxProps.textColorCustom.trim() !== ""
? checkboxProps.textColorCustom
@@ -589,9 +593,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
}}
/>
<ColorPickerField
label="배경 색상"
ariaLabelColorInput="체크박스 배경 색상 피커"
ariaLabelHexInput="체크박스 배경 색상 HEX"
label={m.fillColorLabel}
ariaLabelColorInput={m.fillColorPickerAria}
ariaLabelHexInput={m.fillColorHexAria}
value={
checkboxProps.fillColorCustom && checkboxProps.fillColorCustom.trim() !== ""
? checkboxProps.fillColorCustom
@@ -610,9 +614,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
}}
/>
<ColorPickerField
label="테두리 색상"
ariaLabelColorInput="체크박스 테두리 색상 피커"
ariaLabelHexInput="체크박스 테두리 색상 HEX"
label={m.strokeColorLabel}
ariaLabelColorInput={m.strokeColorPickerAria}
ariaLabelHexInput={m.strokeColorHexAria}
value={
checkboxProps.strokeColorCustom && checkboxProps.strokeColorCustom.trim() !== ""
? checkboxProps.strokeColorCustom
@@ -631,7 +635,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
}}
/>
<NumericPropertyControl
label="필드 모서리 둥글기"
label={m.borderRadiusLabel}
value={(() => {
const r = checkboxProps.borderRadius ?? "md";
return r === "none" ? 0 : r === "sm" ? 2 : r === "lg" ? 6 : r === "full" ? 8 : 4;
@@ -640,11 +644,11 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
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 =