오류 수정
CI / test (push) Failing after 5m41s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Has been skipped

This commit is contained in:
2025-12-12 18:04:31 +09:00
parent 6804665b95
commit 4840a530b6
205 changed files with 1802 additions and 9887 deletions
@@ -99,10 +99,10 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
max={80}
step={2}
presets={[
{ id: "tight", label: "타이트", value: 4 },
{ id: "normal", label: "보통", value: 8 },
{ id: "relaxed", label: "느슨", value: 12 },
{ id: "extra", label: "넓게", value: 16 },
{ id: "tight", label: "Tight", value: 4 },
{ id: "normal", label: "Normal", value: 8 },
{ id: "relaxed", label: "Relaxed", value: 12 },
{ id: "extra", label: "Extra", value: 16 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -235,7 +235,7 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
? [...(checkboxProps as any).options]
: [];
next.push({
label: "새 옵션",
label: "New option",
value: `option_${next.length + 1}`,
});
updateBlock(selectedBlockId, {
@@ -442,9 +442,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
max={60}
step={1}
presets={[
{ id: "tight", label: "타이트", value: 18 },
{ id: "normal", label: "보통", value: 24 },
{ id: "loose", label: "루즈", value: 32 },
{ id: "tight", label: "Tight", value: 18 },
{ id: "normal", label: "Normal", value: 24 },
{ id: "loose", label: "Loose", value: 32 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -466,9 +466,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
max={20}
step={0.5}
presets={[
{ id: "tight", label: "좁게", value: -1 },
{ id: "normal", label: "보통", value: 0 },
{ id: "wide", label: "넓게", value: 2 },
{ id: "tight", label: "Tight", value: -1 },
{ id: "normal", label: "Normal", value: 0 },
{ id: "wide", label: "Wide", value: 2 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -501,9 +501,9 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
max={800}
step={10}
presets={[
{ id: "sm", label: "작게", value: 200 },
{ id: "md", label: "보통", value: 280 },
{ id: "lg", label: "넓게", value: 360 },
{ id: "sm", label: "Small", value: 200 },
{ id: "md", label: "Medium", value: 280 },
{ id: "lg", label: "Large", value: 360 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -520,11 +520,11 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
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 },
{ id: "xs", label: "Extra thin", value: 6 },
{ id: "sm", label: "Thin", value: 10 },
{ id: "md", label: "Normal", value: 12 },
{ id: "lg", label: "Wide", value: 16 },
{ id: "xl", label: "Extra wide", value: 20 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -540,11 +540,11 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
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 },
{ id: "xs", label: "Extra thin", value: 6 },
{ id: "sm", label: "Thin", value: 8 },
{ id: "md", label: "Normal", value: 10 },
{ id: "lg", label: "Wide", value: 12 },
{ id: "xl", label: "Extra wide", value: 16 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -560,10 +560,10 @@ export function FormCheckboxPropertiesPanel({ block, selectedBlockId, updateBloc
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 },
{ id: "tight", label: "Tight", value: 2 },
{ id: "normal", label: "Normal", value: 4 },
{ id: "relaxed", label: "Relaxed", value: 8 },
{ id: "extra", label: "Extra", value: 12 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
+7 -5
View File
@@ -216,6 +216,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
<option value="fixed">{m.formWidthModeOptionFixed}</option>
</select>
</label>
{(formProps.formWidthMode ?? "auto") === "fixed" && (
<NumericPropertyControl
label={m.formFixedWidthLabel}
@@ -225,9 +226,9 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
max={1200}
step={10}
presets={[
{ id: "sm", label: "좁게", value: 320 },
{ id: "md", label: "보통", value: 480 },
{ id: "lg", label: "넓게", value: 640 },
{ id: "sm", label: m.formFixedWidthPresetNarrow, value: 320 },
{ id: "md", label: m.formFixedWidthPresetNormal, value: 480 },
{ id: "lg", label: m.formFixedWidthPresetWide, value: 640 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -236,6 +237,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
}
/>
)}
<NumericPropertyControl
label={m.marginYLabel}
unitLabel="(px)"
@@ -352,7 +354,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
} as any);
}}
/>
<span></span>
<span>{m.requiredLabel}</span>
</label>
</label>
);
@@ -371,7 +373,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
} as any)
}
>
<option value=""> </option>
<option value="">{m.submitButtonNoneOptionLabel}</option>
{blocks
.filter((b) => b.type === "button")
.map((buttonBlock) => {
@@ -92,10 +92,10 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
max={80}
step={2}
presets={[
{ id: "tight", label: "타이트", value: 4 },
{ id: "normal", label: "보통", value: 8 },
{ id: "relaxed", label: "느슨", value: 12 },
{ id: "extra", label: "넓게", value: 16 },
{ id: "tight", label: "Tight", value: 4 },
{ id: "normal", label: "Normal", value: 8 },
{ id: "relaxed", label: "Relaxed", value: 12 },
{ id: "extra", label: "Extra", value: 16 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -218,9 +218,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
max={60}
step={1}
presets={[
{ id: "tight", label: "타이트", value: 18 },
{ id: "normal", label: "보통", value: 24 },
{ id: "loose", label: "루즈", value: 32 },
{ id: "tight", label: "Tight", value: 18 },
{ id: "normal", label: "Normal", value: 24 },
{ id: "loose", label: "Loose", value: 32 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
@@ -242,9 +242,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
max={20}
step={0.5}
presets={[
{ id: "tight", label: "좁게", value: -1 },
{ id: "normal", label: "보통", value: 0 },
{ id: "wide", label: "넓게", value: 2 },
{ id: "tight", label: "Tight", value: -1 },
{ id: "normal", label: "Normal", value: 0 },
{ id: "wide", label: "Wide", value: 2 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
@@ -293,9 +293,9 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
max={800}
step={10}
presets={[
{ id: "sm", label: "작게", value: 200 },
{ id: "md", label: "보통", value: 280 },
{ id: "lg", label: "넓게", value: 360 },
{ id: "sm", label: "Small", value: 200 },
{ id: "md", label: "Medium", value: 280 },
{ id: "lg", label: "Large", value: 360 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
@@ -311,11 +311,11 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
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 },
{ id: "xs", label: "Extra thin", value: 6 },
{ id: "sm", label: "Thin", value: 10 },
{ id: "md", label: "Normal", value: 12 },
{ id: "lg", label: "Wide", value: 16 },
{ id: "xl", label: "Extra wide", value: 20 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
@@ -331,11 +331,11 @@ export function FormInputPropertiesPanel({ block, selectedBlockId, updateBlock }
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 },
{ id: "xs", label: "Extra thin", value: 6 },
{ id: "sm", label: "Thin", value: 8 },
{ id: "md", label: "Normal", value: 10 },
{ id: "lg", label: "Wide", value: 12 },
{ id: "xl", label: "Extra wide", value: 16 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
@@ -25,7 +25,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
<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={radioProps.groupLabelMode ?? "text"}
@@ -35,13 +35,13 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
} 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}
@@ -51,14 +51,14 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
} 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={radioProps.labelLayout ?? "stacked"}
@@ -68,14 +68,14 @@ export function FormRadioPropertiesPanel({ 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>
)}
<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={radioProps.optionLayout ?? "stacked"}
@@ -85,24 +85,24 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
} as any)
}
>
<option value="stacked"> ()</option>
<option value="inline"> ()</option>
<option value="stacked">{m.optionLayoutOptionStacked}</option>
<option value="inline">{m.optionLayoutOptionInline}</option>
</select>
</label>
{groupLabelDisplay === "visible" && (radioProps.labelLayout ?? "stacked") === "inline" && (
<NumericPropertyControl
label="라벨/필드 간격 (px)"
label={m.labelGapLabel}
unitLabel="(px)"
value={typeof radioProps.labelGapPx === "number" ? radioProps.labelGapPx : 8}
min={0}
max={80}
step={2}
presets={[
{ id: "tight", label: "타이트", value: 4 },
{ id: "normal", label: "보통", value: 8 },
{ id: "relaxed", label: "느슨", value: 12 },
{ id: "extra", label: "넓게", value: 16 },
{ id: "tight", label: "Tight", value: 4 },
{ id: "normal", label: "Normal", value: 8 },
{ id: "relaxed", label: "Relaxed", value: 12 },
{ id: "extra", label: "Extra", value: 16 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -113,7 +113,7 @@ export function FormRadioPropertiesPanel({ 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.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={radioProps.groupLabel ?? ""}
@@ -135,7 +135,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
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}
@@ -145,14 +145,14 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
} 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={radioProps.groupLabelImageUrl ?? ""}
@@ -167,12 +167,12 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
{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;
@@ -212,7 +212,7 @@ export function FormRadioPropertiesPanel({ 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={radioProps.formFieldName ?? ""}
@@ -226,7 +226,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
<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-red-900/60 dark:hover:border-red-700"
@@ -235,7 +235,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
? [...(radioProps as any).options]
: [];
next.push({
label: "새 옵션",
label: "New option",
value: `option_${next.length + 1}`,
});
updateBlock(selectedBlockId, {
@@ -243,12 +243,12 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
} 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={(() => {
@@ -263,8 +263,8 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
} 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>
@@ -273,7 +273,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
<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 = [...(((radioProps as any).options ?? []) as any[])];
@@ -288,7 +288,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
/>
<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 = [...(((radioProps as any).options ?? []) as any[])];
@@ -325,7 +325,7 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
{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 = [...(((radioProps as any).options ?? []) as any[])];
@@ -341,12 +341,12 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
)}
{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;
@@ -442,9 +442,9 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
max={60}
step={1}
presets={[
{ id: "tight", label: "타이트", value: 18 },
{ id: "normal", label: "보통", value: 24 },
{ id: "loose", label: "루즈", value: 32 },
{ id: "tight", label: "Tight", value: 18 },
{ id: "normal", label: "Normal", value: 24 },
{ id: "loose", label: "Loose", value: 32 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
@@ -466,9 +466,9 @@ export function FormRadioPropertiesPanel({ block, selectedBlockId, updateBlock }
max={20}
step={0.5}
presets={[
{ id: "tight", label: "좁게", value: -1 },
{ id: "normal", label: "보통", value: 0 },
{ id: "wide", label: "넓게", value: 2 },
{ id: "tight", label: "Tight", value: -1 },
{ id: "normal", label: "Normal", value: 0 },
{ id: "wide", label: "Wide", value: 2 },
]}
onChangeValue={(v) => {
updateBlock(selectedBlockId, {
@@ -83,10 +83,10 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
max={80}
step={2}
presets={[
{ id: "tight", label: "타이트", value: 4 },
{ id: "normal", label: "보통", value: 8 },
{ id: "relaxed", label: "느슨", value: 12 },
{ id: "extra", label: "넓게", value: 16 },
{ id: "tight", label: "Tight", value: 4 },
{ id: "normal", label: "Normal", value: 8 },
{ id: "relaxed", label: "Relaxed", value: 12 },
{ id: "extra", label: "Extra", value: 16 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -133,7 +133,7 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
? [...(selectProps as any).options]
: [];
next.push({
label: "새 옵션",
label: "New option",
value: `option_${next.length + 1}`,
});
updateBlock(selectedBlockId, {
@@ -240,9 +240,9 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
max={60}
step={1}
presets={[
{ id: "tight", label: "타이트", value: 18 },
{ id: "normal", label: "보통", value: 24 },
{ id: "loose", label: "루즈", value: 32 },
{ id: "tight", label: "Tight", value: 18 },
{ id: "normal", label: "Normal", value: 24 },
{ id: "loose", label: "Loose", value: 32 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -264,9 +264,9 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
max={20}
step={0.5}
presets={[
{ id: "tight", label: "좁게", value: -1 },
{ id: "normal", label: "보통", value: 0 },
{ id: "wide", label: "넓게", value: 2 },
{ id: "tight", label: "Tight", value: -1 },
{ id: "normal", label: "Normal", value: 0 },
{ id: "wide", label: "Wide", value: 2 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -285,9 +285,9 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
} as any)
}
>
<option value="auto"></option>
<option value="full"></option>
<option value="fixed"></option>
<option value="auto">Auto</option>
<option value="full">Full</option>
<option value="fixed">Fixed</option>
</select>
</label>
{(selectProps.widthMode ?? "full") === "fixed" && (
@@ -299,9 +299,9 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
max={800}
step={10}
presets={[
{ id: "sm", label: "작게", value: 200 },
{ id: "md", label: "보통", value: 280 },
{ id: "lg", label: "넓게", value: 360 },
{ id: "sm", label: "Small", value: 200 },
{ id: "md", label: "Medium", value: 280 },
{ id: "lg", label: "Large", value: 360 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -318,11 +318,11 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
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 },
{ id: "xs", label: "Extra thin", value: 6 },
{ id: "sm", label: "Thin", value: 10 },
{ id: "md", label: "Normal", value: 12 },
{ id: "lg", label: "Wide", value: 16 },
{ id: "xl", label: "Extra wide", value: 20 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -338,11 +338,11 @@ export function FormSelectPropertiesPanel({ block, selectedBlockId, updateBlock
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 },
{ id: "xs", label: "Extra thin", value: 6 },
{ id: "sm", label: "Thin", value: 8 },
{ id: "md", label: "Normal", value: 10 },
{ id: "lg", label: "Wide", value: 12 },
{ id: "xl", label: "Extra wide", value: 16 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
+13 -8
View File
@@ -206,7 +206,7 @@ function EditorPageInner() {
}
updateProjectConfig({
title: "새 페이지",
title: "New page",
slug: "my-landing",
canvasPreset: "full",
canvasBgColorHex: "#020617",
@@ -1084,6 +1084,7 @@ function EditorPageInner() {
listItemToolbarMoveDownLabel={m.listItemToolbarMoveDownLabel}
listItemToolbarIndentLabel={m.listItemToolbarIndentLabel}
listItemToolbarOutdentLabel={m.listItemToolbarOutdentLabel}
imageBlockEmptyPreviewPlaceholder={m.imageBlockEmptyPreviewPlaceholder}
/>
</Fragment>
);
@@ -1425,6 +1426,7 @@ interface SortableEditorBlockProps {
listItemToolbarMoveDownLabel: string;
listItemToolbarIndentLabel: string;
listItemToolbarOutdentLabel: string;
imageBlockEmptyPreviewPlaceholder: string;
}
function SortableEditorBlock({
@@ -1449,6 +1451,7 @@ function SortableEditorBlock({
listItemToolbarMoveDownLabel,
listItemToolbarIndentLabel,
listItemToolbarOutdentLabel,
imageBlockEmptyPreviewPlaceholder,
}: SortableEditorBlockProps) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: block.id,
@@ -1560,7 +1563,7 @@ function SortableEditorBlock({
<button
type="button"
className="mt-0.5 h-4 w-4 rounded border border-slate-300 bg-slate-50 text-[10px] flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400 dark:hover:bg-slate-800"
aria-label="블록 드래그 핸들"
aria-label="Block drag handle"
{...listeners}
{...attributes}
>
@@ -1571,7 +1574,7 @@ function SortableEditorBlock({
isEditing ? (
<textarea
className="w-full bg-transparent outline-none resize-none text-sm"
aria-label="텍스트 블록 내용 편집"
aria-label="Edit text block content"
value={editingText}
onChange={(e) => setEditingText(e.target.value)}
onBlur={commitEditing}
@@ -1715,8 +1718,8 @@ function SortableEditorBlock({
const options = Array.isArray(selectProps.options) && selectProps.options.length > 0
? selectProps.options
: [
{ label: "옵션 1", value: "option_1" },
{ label: "옵션 2", value: "option_2" },
{ label: "Option 1", value: "option_1" },
{ label: "Option 2", value: "option_2" },
];
const selectTokens = computeFormSelectEditorTokens(selectProps);
@@ -2080,7 +2083,7 @@ function SortableEditorBlock({
/>
) : (
<span className="text-[10px] text-slate-900 dark:text-slate-500 px-2 py-4">
URL .
{imageBlockEmptyPreviewPlaceholder}
</span>
)}
</div>
@@ -2147,7 +2150,7 @@ function SortableEditorBlock({
: [
{
id: `${block.id}_item_1`,
text: "리스트 아이템 1",
text: "List item 1",
children: [],
},
];
@@ -2350,6 +2353,8 @@ interface ColumnDroppableProps {
function ColumnDroppable({ id, sectionId, columnId, basis, blocks, renderBlocks, span }: ColumnDroppableProps) {
const { setNodeRef } = useDroppable({ id });
const { over } = useDndContext();
const locale = useAppLocale();
const editorMessages = getEditorMessages(locale);
// 현재 드롭 대상이 이 컬럼이거나, 이 컬럼 안 블록들의 dropzone-before / dropzone-after 인 경우
// 컬럼 박스를 강조해서 사용자가 어느 컬럼으로 이동 중인지 명확히 보여준다.
@@ -2400,7 +2405,7 @@ function ColumnDroppable({ id, sectionId, columnId, basis, blocks, renderBlocks,
>
{blocks.length === 0 ? (
<span className="text-[11px] px-2 text-center text-slate-900 dark:text-slate-300">
{`컬럼 영역 (span ${span}/12)`}
{`${editorMessages.columnAreaLabel} (span ${span}/12)`}
</span>
) : (
<SortableContext items={blocks.map((b) => b.id)} strategy={verticalListSortingStrategy}>
+37 -22
View File
@@ -50,43 +50,58 @@ export function BlocksSidebar() {
const addFooterTemplateSection = useEditorStore((state) => state.addFooterTemplateSection);
// 버튼 핸들러를 useCallback으로 래핑해 불필요한 재생성을 줄인다.
const handleAddText = useCallback(() => addTextBlock(), [addTextBlock]);
const handleAddButton = useCallback(() => addButtonBlock(), [addButtonBlock]);
const handleAddText = useCallback(() => addTextBlock(locale), [addTextBlock, locale]);
const handleAddButton = useCallback(() => addButtonBlock(locale), [addButtonBlock, locale]);
const handleAddImage = useCallback(() => addImageBlock(), [addImageBlock]);
const handleAddVideo = useCallback(() => addVideoBlock(), [addVideoBlock]);
const handleAddDivider = useCallback(() => addDividerBlock(), [addDividerBlock]);
const handleAddList = useCallback(() => addListBlock(), [addListBlock]);
const handleAddList = useCallback(() => addListBlock(locale), [addListBlock, locale]);
const handleAddSection = useCallback(() => addSectionBlock(), [addSectionBlock]);
const handleAddFormBlock = useCallback(() => addFormBlock(), [addFormBlock]);
const handleAddFormInput = useCallback(() => addFormInputBlock(), [addFormInputBlock]);
const handleAddFormSelect = useCallback(() => addFormSelectBlock(), [addFormSelectBlock]);
const handleAddFormRadio = useCallback(() => addFormRadioBlock(), [addFormRadioBlock]);
const handleAddFormCheckbox = useCallback(() => addFormCheckboxBlock(), [addFormCheckboxBlock]);
const handleAddFormBlock = useCallback(() => addFormBlock(locale), [addFormBlock, locale]);
const handleAddFormInput = useCallback(() => addFormInputBlock(locale), [addFormInputBlock, locale]);
const handleAddFormSelect = useCallback(() => addFormSelectBlock(locale), [addFormSelectBlock, locale]);
const handleAddFormRadio = useCallback(() => addFormRadioBlock(locale), [addFormRadioBlock, locale]);
const handleAddFormCheckbox = useCallback(
() => addFormCheckboxBlock(locale),
[addFormCheckboxBlock, locale],
);
const handleAddHeroTemplate = useCallback(
() => addHeroTemplateSection(),
[addHeroTemplateSection],
() => addHeroTemplateSection(locale),
[addHeroTemplateSection, locale],
);
const handleAddFeaturesTemplate = useCallback(
() => addFeaturesTemplateSection(),
[addFeaturesTemplateSection],
() => addFeaturesTemplateSection(locale),
[addFeaturesTemplateSection, locale],
);
const handleAddCtaTemplate = useCallback(
() => addCtaTemplateSection(locale),
[addCtaTemplateSection, locale],
);
const handleAddFaqTemplate = useCallback(
() => addFaqTemplateSection(locale),
[addFaqTemplateSection, locale],
);
const handleAddCtaTemplate = useCallback(() => addCtaTemplateSection(), [addCtaTemplateSection]);
const handleAddFaqTemplate = useCallback(() => addFaqTemplateSection(), [addFaqTemplateSection]);
const handleAddPricingTemplate = useCallback(
() => addPricingTemplateSection(),
[addPricingTemplateSection],
() => addPricingTemplateSection(locale),
[addPricingTemplateSection, locale],
);
const handleAddTestimonialsTemplate = useCallback(
() => addTestimonialsTemplateSection(),
[addTestimonialsTemplateSection],
() => addTestimonialsTemplateSection(locale),
[addTestimonialsTemplateSection, locale],
);
const handleAddBlogTemplate = useCallback(
() => addBlogTemplateSection(locale),
[addBlogTemplateSection, locale],
);
const handleAddTeamTemplate = useCallback(
() => addTeamTemplateSection(locale),
[addTeamTemplateSection, locale],
);
const handleAddBlogTemplate = useCallback(() => addBlogTemplateSection(), [addBlogTemplateSection]);
const handleAddTeamTemplate = useCallback(() => addTeamTemplateSection(), [addTeamTemplateSection]);
const handleAddFooterTemplate = useCallback(
() => addFooterTemplateSection(),
[addFooterTemplateSection],
() => addFooterTemplateSection(locale),
[addFooterTemplateSection, locale],
);
const [isBlocksOpen, setIsBlocksOpen] = useState(true);
@@ -88,9 +88,9 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
}
}}
>
<option value="none">{m.backgroundVideoSourceOptionNone}</option>
<option value="url">{m.backgroundVideoSourceOptionUrl}</option>
<option value="upload">{m.backgroundVideoSourceOptionUpload}</option>
<option value="none">{m.backgroundImageSourceOptionNone}</option>
<option value="url">{m.backgroundImageSourceOptionUrl}</option>
<option value="upload">{m.backgroundImageSourceOptionUpload}</option>
</select>
</label>
@@ -229,9 +229,9 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
max={100}
step={1}
presets={[
{ id: "left", label: "왼쪽", value: 0 },
{ id: "center", label: "가운데", value: 50 },
{ id: "right", label: "오른쪽", value: 100 },
{ id: "left", label: m.backgroundImagePositionOptionLeft, value: 0 },
{ id: "center", label: m.backgroundImagePositionOptionCenter, value: 50 },
{ id: "right", label: m.backgroundImagePositionOptionRight, value: 100 },
]}
onChangeValue={(next) =>
updateBlock(selectedBlockId, {
@@ -253,9 +253,9 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
max={100}
step={1}
presets={[
{ id: "top", label: "위", value: 0 },
{ id: "center", label: "가운데", value: 50 },
{ id: "bottom", label: "아래", value: 100 },
{ id: "top", label: m.backgroundImagePositionOptionTop, value: 0 },
{ id: "center", label: m.backgroundImagePositionOptionCenter, value: 50 },
{ id: "bottom", label: m.backgroundImagePositionOptionBottom, value: 100 },
]}
onChangeValue={(next) =>
updateBlock(selectedBlockId, {
@@ -317,9 +317,9 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
}
}}
>
<option value="none"></option>
<option value="url">URL</option>
<option value="upload"> </option>
<option value="none">{m.backgroundVideoSourceOptionNone}</option>
<option value="url">{m.backgroundVideoSourceOptionUrl}</option>
<option value="upload">{m.backgroundVideoSourceOptionUpload}</option>
</select>
</label>
@@ -387,7 +387,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
)}
</div>
{/* 세로 패딩 슬라이더 (px) - 프리셋 + 자유 슬라이더 */}
{/* 세로 패딩 슬라이더 - 프리셋 + 자유 슬라이더 */}
<div className="mt-3 space-y-1">
<NumericPropertyControl
label={m.paddingYLabel}
@@ -397,11 +397,11 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
max={80}
step={2}
presets={[
{ id: "x-tight", label: "매우 좁게", value: 16 },
{ id: "tight", label: "좁게", value: 32 },
{ id: "normal", label: "보통", value: 48 },
{ id: "relaxed", label: "넓게", value: 64 },
{ id: "x-relaxed", label: "아주 넓게", value: 80 },
{ id: "x-tight", label: m.paddingYPresetExtraTight, value: 16 },
{ id: "tight", label: m.paddingYPresetTight, value: 32 },
{ id: "normal", label: m.paddingYPresetNormal, value: 48 },
{ id: "relaxed", label: m.paddingYPresetRelaxed, value: 64 },
{ id: "x-relaxed", label: m.paddingYPresetExtraRelaxed, value: 80 },
]}
onChangeValue={(next) => {
const safe = Number.isFinite(next) ? next : 48;
@@ -682,7 +682,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
});
})()}
{/* 최대 폭 슬라이더 (px) - 프리셋 + 자유 슬라이더 */}
{/* 최대 폭 슬라이더 - 프리셋 + 자유 슬라이더 */}
<NumericPropertyControl
label={m.maxWidthLabel}
unitLabel={m.maxWidthUnitLabel}
@@ -691,11 +691,11 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
max={1440}
step={16}
presets={[
{ id: "x-narrow", label: "매우 좁게", value: 640 },
{ id: "narrow", label: "좁게", value: 800 },
{ id: "normal", label: "보통", value: 960 },
{ id: "wide", label: "넓게", value: 1200 },
{ id: "x-wide", label: "아주 넓게", value: 1440 },
{ id: "x-narrow", label: m.maxWidthPresetExtraNarrow, value: 640 },
{ id: "narrow", label: m.maxWidthPresetNarrow, value: 800 },
{ id: "normal", label: m.maxWidthPresetNormal, value: 960 },
{ id: "wide", label: m.maxWidthPresetWide, value: 1200 },
{ id: "x-wide", label: m.maxWidthPresetExtraWide, value: 1440 },
]}
onChangeValue={(next) => {
const safe = Number.isFinite(next) ? next : 960;
@@ -703,7 +703,7 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
}}
/>
{/* 컬럼 간 간격 슬라이더 (px) - 프리셋 + 자유 슬라이더 */}
{/* 컬럼 간 간격 슬라이더 - 프리셋 + 자유 슬라이더 */}
<NumericPropertyControl
label={m.gapXLabel}
unitLabel={m.gapXUnitLabel}
@@ -712,12 +712,12 @@ export function SectionPropertiesPanel({ sectionProps, selectedBlockId, updateBl
max={64}
step={2}
presets={[
{ id: "zero", label: "0", value: 0 },
{ id: "tight", label: "좁게", value: 16 },
{ id: "normal", label: "보통", value: 24 },
{ id: "relaxed", label: "넓게", value: 32 },
{ id: "x-relaxed", label: "아주 넓게", value: 48 },
{ id: "max", label: "최대", value: 64 },
{ id: "zero", label: m.gapXPresetZero, value: 0 },
{ id: "tight", label: m.gapXPresetTight, value: 16 },
{ id: "normal", label: m.gapXPresetNormal, value: 24 },
{ id: "relaxed", label: m.gapXPresetRelaxed, value: 32 },
{ id: "x-relaxed", label: m.gapXPresetExtraRelaxed, value: 48 },
{ id: "max", label: m.gapXPresetMax, value: 64 },
]}
onChangeValue={(next) => {
const safe = Number.isFinite(next) ? next : 24;
+6 -8
View File
@@ -1,12 +1,14 @@
"use client";
import type { Block, SectionBlockProps, TextBlockProps, ButtonBlockProps } from "@/features/editor/state/editorStore";
import type { EditorPricingTemplateMessages } from "@/features/i18n/messages/editorTemplates";
export function createPricingTemplateBlocks(opts: {
sectionId: string;
createId: () => string;
messages: EditorPricingTemplateMessages;
}): { blocks: Block[]; lastSelectedId: string } {
const { sectionId, createId } = opts;
const { sectionId, createId, messages } = opts;
const columns: SectionBlockProps["columns"] = [
{ id: `${sectionId}_col_1`, span: 4 },
@@ -33,11 +35,7 @@ export function createPricingTemplateBlocks(opts: {
columnId: null,
};
const planDefinitions: Array<{ name: string; price: string; popular?: boolean }> = [
{ name: "Basic", price: "₩9,900/월" },
{ name: "Pro", price: "₩29,900/월", popular: true },
{ name: "Enterprise", price: "문의" },
];
const planDefinitions = messages.plans;
const pricingBlocks: Block[] = columns.flatMap((col, index) => {
const plan = planDefinitions[index] ?? planDefinitions[0];
@@ -96,7 +94,7 @@ export function createPricingTemplateBlocks(opts: {
// Features list (simple text for now)
const featuresId = createId();
const featuresProps: TextBlockProps = {
text: "• 모든 기본 기능\n• 이메일 지원\n• 1GB 스토리지",
text: messages.featuresText,
align: "center",
size: "sm",
color: "muted",
@@ -112,7 +110,7 @@ export function createPricingTemplateBlocks(opts: {
// Button
const buttonId = createId();
const buttonProps: ButtonBlockProps = {
label: isPopular ? "시작하기" : "선택하기",
label: isPopular ? messages.primaryButtonLabel : messages.secondaryButtonLabel,
href: "#",
align: "center",
size: "md",
@@ -1,12 +1,14 @@
"use client";
import type { Block, SectionBlockProps, TextBlockProps } from "@/features/editor/state/editorStore";
import type { EditorTestimonialsTemplateMessages } from "@/features/i18n/messages/editorTemplates";
export function createTestimonialsTemplateBlocks(opts: {
sectionId: string;
createId: () => string;
messages: EditorTestimonialsTemplateMessages;
}): { blocks: Block[]; lastSelectedId: string } {
const { sectionId, createId } = opts;
const { sectionId, createId, messages } = opts;
const columns: SectionBlockProps["columns"] = [
{ id: `${sectionId}_col_1`, span: 4 },
@@ -33,11 +35,7 @@ export function createTestimonialsTemplateBlocks(opts: {
columnId: null,
};
const testimonialDefinitions: Array<{ body: string; author: string }> = [
{ body: "이 서비스 덕분에 랜딩 페이지 제작 시간이 크게 줄었습니다.", author: "홍길동" },
{ body: "디자인을 잘 못해도 깔끔한 페이지를 만들 수 있어요.", author: "김영희" },
{ body: "팀 전체가 만족하는 빌더입니다.", author: "이철수" },
];
const testimonialDefinitions = messages.items;
const testimonialBlocks: Block[] = columns.flatMap((col, index) => {
const t = testimonialDefinitions[index] ?? testimonialDefinitions[0];
+7 -2
View File
@@ -1,7 +1,7 @@
import "../styles/globals.css";
import "../styles/builder.css";
import type { ReactNode } from "react";
import { headers } from "next/headers";
import { headers, cookies } from "next/headers";
import { LocaleProvider } from "@/features/i18n/LocaleProvider";
import { LocaleSwitcher } from "@/features/i18n/LocaleSwitcher";
import { resolveLocaleFromAcceptLanguage, DEFAULT_LOCALE } from "@/features/i18n/locale";
@@ -12,9 +12,14 @@ export const metadata = {
};
export default async function RootLayout({ children }: { children: ReactNode }) {
const cookieStore = await cookies();
const cookieLocale = cookieStore.get("pb-locale")?.value;
const headerList = await headers();
const acceptLanguage = headerList.get("accept-language");
const locale = resolveLocaleFromAcceptLanguage(acceptLanguage ?? undefined) ?? DEFAULT_LOCALE;
const localeFromCookie = cookieLocale === "en" || cookieLocale === "ko" ? cookieLocale : null;
const locale = (localeFromCookie ?? resolveLocaleFromAcceptLanguage(acceptLanguage ?? undefined)) ?? DEFAULT_LOCALE;
return (
<html lang={locale} suppressHydrationWarning>
+5 -6
View File
@@ -1,12 +1,14 @@
"use client";
import { useEffect, useState } from "react";
import { getPublicPageRendererMessages } from "@/features/i18n/messages/publicPageRenderer";
interface PublicProjectPageClientProps {
html: string;
}
export default function PublicProjectPageClient({ html }: PublicProjectPageClientProps) {
const m = getPublicPageRendererMessages(null);
const [toastMessage, setToastMessage] = useState<string | null>(null);
useEffect(() => {
@@ -58,21 +60,18 @@ export default function PublicProjectPageClient({ html }: PublicProjectPageClien
const ok = data && data.ok;
const message = data && data.message;
if (ok) {
const success =
message || (config && config.successMessage) || "성공적으로 전송되었습니다.";
const success = message || (config && config.successMessage) || m.submitSuccessDefault;
showToast(success);
try {
form.reset();
} catch {}
} else {
const errorMsg =
message || (config && config.errorMessage) || "전송 중 오류가 발생했습니다.";
const errorMsg = message || (config && config.errorMessage) || m.submitErrorDefault;
showToast(errorMsg);
}
})
.catch(() => {
const errorMsg =
(config && config.errorMessage) || "전송 중 오류가 발생했습니다.";
const errorMsg = (config && config.errorMessage) || m.submitErrorDefault;
showToast(errorMsg);
});
});
+29 -24
View File
@@ -4,6 +4,9 @@ import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter, useParams } from "next/navigation";
import { FolderKanban, LayoutDashboard, ListChecks, SunMoon } from "lucide-react";
import { useAppLocale } from "@/features/i18n/LocaleProvider";
import { getDashboardMessages } from "@/features/i18n/messages/dashboard";
import { getSubmissionsMessages } from "@/features/i18n/messages/submissions";
// 프로젝트별 폼 제출 내역을 조회해서 보여주는 페이지 컴포넌트.
// - URL 의 slug 파라미터를 기준으로 /api/projects/[slug]/submissions 에 요청을 보낸다.
@@ -25,6 +28,10 @@ export default function ProjectSubmissionsPage() {
const params = useParams() as { slug?: string } | null;
const slug = (params?.slug ?? "").toString();
const locale = useAppLocale();
const t = getDashboardMessages(locale);
const m = getSubmissionsMessages(locale);
const [submissions, setSubmissions] = useState<SubmissionItem[]>([]);
const [status, setStatus] = useState<PageStatus>("idle");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
@@ -48,21 +55,19 @@ export default function ProjectSubmissionsPage() {
if (res.status === 401) {
setStatus("error");
setErrorMessage("폼 제출 내역을 조회하려면 로그인이 필요합니다. 다시 로그인해 주세요.");
setErrorMessage(m.errorUnauthorized);
router.push("/login");
return;
}
if (res.status === 404) {
setStatus("error");
setErrorMessage(
"프로젝트를 찾을 수 없거나 접근 권한이 없습니다. 프로젝트 소유자 계정으로 다시 로그인해 주세요.",
);
setErrorMessage(m.errorGeneric);
return;
}
setStatus("error");
setErrorMessage("폼 제출 내역을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
setErrorMessage(m.errorGeneric);
return;
}
@@ -76,7 +81,7 @@ export default function ProjectSubmissionsPage() {
} catch {
if (!cancelled) {
setStatus("error");
setErrorMessage("폼 제출 내역을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
setErrorMessage(m.errorGeneric);
}
}
};
@@ -112,13 +117,13 @@ export default function ProjectSubmissionsPage() {
});
if (!res.ok) {
setErrorMessage("로그아웃 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
setErrorMessage(m.errorLogout);
return;
}
router.push("/login");
} catch {
setErrorMessage("로그아웃 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
setErrorMessage(m.errorLogout);
}
};
@@ -139,8 +144,8 @@ export default function ProjectSubmissionsPage() {
<main className="min-h-screen flex flex-col bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
<header className="border-b border-slate-200 px-6 py-4 flex items-center justify-between bg-white/80 backdrop-blur dark:border-slate-800 dark:bg-slate-950/80">
<div>
<h1 className="text-2xl font-bold tracking-tight"> </h1>
<p className="text-xs text-slate-500 dark:text-slate-400"> .</p>
<h1 className="text-2xl font-bold tracking-tight">{m.headerTitle}</h1>
<p className="text-xs text-slate-500 dark:text-slate-400">{m.headerDescription}</p>
<p className="mt-1 text-[11px] font-mono text-slate-500 dark:text-slate-400">{slug}</p>
</div>
<div className="flex items-center gap-3 text-sm">
@@ -150,21 +155,21 @@ export default function ProjectSubmissionsPage() {
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-sky-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:hover:text-sky-200"
>
<LayoutDashboard className="w-4 h-4" aria-hidden="true" />
<span></span>
<span>{t.navDashboard}</span>
</Link>
<Link
href="/projects"
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-sky-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:hover:text-sky-200"
>
<FolderKanban className="w-4 h-4" aria-hidden="true" />
<span> </span>
<span>{t.navProjects}</span>
</Link>
<Link
href="/projects/submissions"
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-emerald-700 hover:bg-emerald-50 hover:text-emerald-800 dark:text-emerald-200 dark:hover:bg-emerald-900/60 dark:hover:text-emerald-100"
>
<ListChecks className="w-4 h-4" aria-hidden="true" />
<span> </span>
<span>{t.navSubmissions}</span>
</Link>
</nav>
<button
@@ -173,7 +178,7 @@ export default function ProjectSubmissionsPage() {
onClick={handleToggleTheme}
>
<SunMoon className="w-4 h-4" aria-hidden="true" />
<span> </span>
<span>{t.themeToggleLabel}</span>
</button>
<div className="relative">
<button
@@ -183,7 +188,7 @@ export default function ProjectSubmissionsPage() {
setIsMenuOpen((prev) => !prev);
}}
>
{t.menuLabel}
</button>
{isMenuOpen && (
<div className="absolute right-0 mt-1 w-32 rounded border border-slate-200 bg-white shadow-lg z-20 dark:border-slate-700 dark:bg-slate-900/95">
@@ -195,7 +200,7 @@ export default function ProjectSubmissionsPage() {
void handleLogout();
}}
>
{t.logoutLabel}
</button>
</div>
)}
@@ -209,23 +214,23 @@ export default function ProjectSubmissionsPage() {
)}
{status === "loading" && (
<p className="text-xs text-slate-500 mb-3 dark:text-slate-400"> ...</p>
<p className="text-xs text-slate-500 mb-3 dark:text-slate-400">{m.loadingText}</p>
)}
{submissions.length === 0 && status === "idle" && !errorMessage && (
<p className="text-xs text-slate-500 dark:text-slate-400"> .</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{m.emptyText}</p>
)}
{submissions.length > 0 && (
<table className="w-full text-xs text-left border-collapse mt-2">
<thead>
<tr className="border-b border-slate-200 text-slate-600 dark:border-slate-800 dark:text-slate-400">
<th className="py-2 pr-4"> </th>
<th className="py-2 pr-4"></th>
<th className="py-2 pr-4"></th>
<th className="py-2 pr-4"></th>
<th className="py-2 pr-4"></th>
<th className="py-2 pr-4"> </th>
<th className="py-2 pr-4">{m.tableHeaderCreatedAt}</th>
<th className="py-2 pr-4">{m.tableHeaderName}</th>
<th className="py-2 pr-4">{m.tableHeaderEmail}</th>
<th className="py-2 pr-4">{m.tableHeaderPhone}</th>
<th className="py-2 pr-4">{m.tableHeaderBirthdate}</th>
<th className="py-2 pr-4">{m.tableHeaderOtherFields}</th>
</tr>
</thead>
<tbody>
@@ -84,7 +84,7 @@ export function NumericPropertyControl({
<PropertySliderField
label=""
ariaLabelSlider={`${label} 슬라이더`}
ariaLabelInput={`${label} 커스텀${unitLabel ? ` ${unitLabel}` : ""}`}
ariaLabelInput={`${label} 커스텀`}
value={Number.isFinite(value) ? value : 0}
min={min}
max={max}
@@ -847,7 +847,7 @@ export function PublicPageRenderer({ blocks, projectSlug }: PublicPageRendererPr
: [
{
id: `${block.id}_item_1`,
text: "리스트 아이템 1",
text: "List item 1",
children: [],
},
];
+87 -63
View File
@@ -10,7 +10,8 @@ import { createFaqTemplateBlocks } from "@/app/editor/templates/faqTemplate";
import { createPricingTemplateBlocks } from "@/app/editor/templates/pricingTemplate";
import { createTestimonialsTemplateBlocks } from "@/app/editor/templates/testimonialsTemplate";
import { getEditorTemplatesMessages } from "@/features/i18n/messages/editorTemplates";
import { DEFAULT_LOCALE } from "@/features/i18n/locale";
import { getEditorBlockDefaultsMessages } from "@/features/i18n/messages/editorBlockDefaults";
import { DEFAULT_LOCALE, type AppLocale } from "@/features/i18n/locale";
// 블록 타입 정의: 텍스트/버튼/이미지/비디오/섹션/구분선/리스트/폼 블록/폼 요소 블록
export type BlockType =
@@ -739,27 +740,27 @@ export interface EditorState {
future: Block[][];
projectConfig: ProjectConfig;
updateProjectConfig: (partial: Partial<ProjectConfig>) => void;
addTextBlock: () => void;
addButtonBlock: () => void;
addTextBlock: (locale?: AppLocale | null) => void;
addButtonBlock: (locale?: AppLocale | null) => void;
addImageBlock: () => void;
addVideoBlock: () => void;
addDividerBlock: () => void;
addListBlock: () => void;
addListBlock: (locale?: AppLocale | null) => void;
addSectionBlock: () => void;
addFormBlock: () => void;
addFormInputBlock: () => void;
addFormSelectBlock: () => void;
addFormCheckboxBlock: () => void;
addFormRadioBlock: () => void;
addHeroTemplateSection: () => void;
addFeaturesTemplateSection: () => void;
addCtaTemplateSection: () => void;
addFaqTemplateSection: () => void;
addPricingTemplateSection: () => void;
addTestimonialsTemplateSection: () => void;
addBlogTemplateSection: () => void;
addTeamTemplateSection: () => void;
addFooterTemplateSection: () => void;
addFormBlock: (locale?: AppLocale | null) => void;
addFormInputBlock: (locale?: AppLocale | null) => void;
addFormSelectBlock: (locale?: AppLocale | null) => void;
addFormCheckboxBlock: (locale?: AppLocale | null) => void;
addFormRadioBlock: (locale?: AppLocale | null) => void;
addHeroTemplateSection: (locale?: AppLocale | null) => void;
addFeaturesTemplateSection: (locale?: AppLocale | null) => void;
addCtaTemplateSection: (locale?: AppLocale | null) => void;
addFaqTemplateSection: (locale?: AppLocale | null) => void;
addPricingTemplateSection: (locale?: AppLocale | null) => void;
addTestimonialsTemplateSection: (locale?: AppLocale | null) => void;
addBlogTemplateSection: (locale?: AppLocale | null) => void;
addTeamTemplateSection: (locale?: AppLocale | null) => void;
addFooterTemplateSection: (locale?: AppLocale | null) => void;
selectListItem: (itemId: string | null) => void;
indentSelectedListItem: (blockId: string) => void;
outdentSelectedListItem: (blockId: string) => void;
@@ -819,6 +820,8 @@ const getNextFormFieldName = (blocks: Block[], prefix: string): string => {
return `${prefix}-${max + 1}`;
};
const isKoLocale = (locale?: AppLocale | null): boolean => (locale ?? DEFAULT_LOCALE) === "ko";
// 에디터 스토어 생성 함수 (테스트 및 앱에서 공유 사용)
// set/get 은 zustand 내부 구현에 의해 주입되므로, 여기서는 any 로 완화해 사용한다.
const createEditorState = (set: any, get: any): EditorState => {
@@ -837,7 +840,7 @@ const createEditorState = (set: any, get: any): EditorState => {
history: [],
future: [],
projectConfig: {
title: "새 페이지",
title: "New page",
slug: "my-landing",
canvasPreset: "full",
canvasBgColorHex: "#020617",
@@ -862,7 +865,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// 텍스트 블록 추가: 기본 텍스트와 함께 생성 후 선택 상태로 만든다
addTextBlock: () => {
addTextBlock: (locale?: AppLocale | null) => {
const id = createId();
const { selectedBlockId, blocks } = get();
@@ -883,11 +886,12 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const blockDefaults = getEditorBlockDefaultsMessages(locale);
const newBlock: Block = {
id,
type: "text",
props: {
text: "새 텍스트",
text: blockDefaults.text.defaultText,
align: "left",
size: "base",
},
@@ -903,7 +907,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// Hero 템플릿 섹션 추가: 섹션 1개와 기본 텍스트/버튼 블록들을 첫 컬럼에 배치한다
addHeroTemplateSection: () => {
addHeroTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -916,7 +920,7 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE);
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createHeroTemplateBlocks({
sectionId,
createId,
@@ -942,7 +946,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// Features 템플릿 섹션 추가: 3컬럼(4/4/4) 섹션과 각 컬럼의 제목/설명 텍스트를 생성한다
addFeaturesTemplateSection: () => {
addFeaturesTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -955,7 +959,7 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE);
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createFeaturesTemplateBlocks({
sectionId,
createId,
@@ -981,7 +985,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// Blog 템플릿 섹션 추가: 3컬럼 포스트 카드(제목/요약)를 생성한다
addBlogTemplateSection: () => {
addBlogTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -994,7 +998,7 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE);
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createBlogTemplateBlocks({
sectionId,
createId,
@@ -1020,7 +1024,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// Team 템플릿 섹션 추가: 3컬럼 팀 카드(이름/역할/소개)를 생성한다
addTeamTemplateSection: () => {
addTeamTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -1033,7 +1037,7 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE);
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createTeamTemplateBlocks({
sectionId,
createId,
@@ -1059,7 +1063,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// Footer 템플릿 섹션 추가: 링크/카피라이트 텍스트가 포함된 1컬럼 섹션을 생성한다
addFooterTemplateSection: () => {
addFooterTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -1072,7 +1076,7 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE);
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createFooterTemplateBlocks({
sectionId,
createId,
@@ -1098,7 +1102,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// CTA 템플릿 섹션 추가: 텍스트와 버튼이 포함된 1컬럼 섹션을 생성한다
addCtaTemplateSection: () => {
addCtaTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -1111,7 +1115,7 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE);
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createCtaTemplateBlocks({
sectionId,
createId,
@@ -1137,7 +1141,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// FAQ 템플릿 섹션 추가: 질문/답변 쌍이 수직으로 나열된 섹션을 생성한다
addFaqTemplateSection: () => {
addFaqTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -1150,7 +1154,7 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(DEFAULT_LOCALE);
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createFaqTemplateBlocks({
sectionId,
createId,
@@ -1176,7 +1180,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// Pricing 템플릿 섹션 추가: 3컬럼 요금제 카드(플랜 이름/가격/설명)를 생성한다
addPricingTemplateSection: () => {
addPricingTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -1189,9 +1193,11 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createPricingTemplateBlocks({
sectionId,
createId,
messages: tpl.pricing,
});
pushHistoryImpl(set, get);
@@ -1213,7 +1219,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// Testimonials 템플릿 섹션 추가: 3컬럼 후기 카드(본문/작성자)를 생성한다
addTestimonialsTemplateSection: () => {
addTestimonialsTemplateSection: (locale?: AppLocale | null) => {
const { selectedBlockId, blocks } = get();
let sectionId = createId();
let replacingSectionId: string | null = null;
@@ -1226,9 +1232,11 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const tpl = getEditorTemplatesMessages(locale);
const { blocks: templateBlocks, lastSelectedId } = createTestimonialsTemplateBlocks({
sectionId,
createId,
messages: tpl.testimonials,
});
pushHistoryImpl(set, get);
@@ -1276,7 +1284,7 @@ const createEditorState = (set: any, get: any): EditorState => {
type: "image",
props: {
src: "",
alt: "이미지 설명",
alt: "Image description",
align: "center",
widthMode: "auto",
borderRadius: "md",
@@ -1340,11 +1348,12 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// 폼 입력 블록 추가: 루트에 기본 label/formFieldName 과 함께 생성 후 선택 상태로 만든다
addFormInputBlock: () => {
addFormInputBlock: (locale?: AppLocale | null) => {
const id = createId();
const { blocks } = get();
const label = "입력 필드";
const blockDefaults = getEditorBlockDefaultsMessages(locale);
const label = blockDefaults.formInput.label;
const formFieldName = getNextFormFieldName(blocks, "text");
const newBlock: Block = {
@@ -1380,15 +1389,17 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// 폼 셀렉트 블록 추가: 루트에 기본 label/formFieldName/options 와 함께 생성 후 선택 상태로 만든다
addFormSelectBlock: () => {
addFormSelectBlock: (locale?: AppLocale | null) => {
const id = createId();
const { blocks } = get();
const label = "선택 필드";
const blockDefaults = getEditorBlockDefaultsMessages(locale);
const selectDefaults = blockDefaults.formSelect;
const label = selectDefaults.label;
const formFieldName = getNextFormFieldName(blocks, "select");
const options: FormSelectOption[] = [
{ label: "옵션 1", value: "option_1" },
{ label: "옵션 2", value: "option_2" },
{ label: selectDefaults.option1Label, value: "option_1" },
{ label: selectDefaults.option2Label, value: "option_2" },
];
const newBlock: Block = {
@@ -1422,15 +1433,17 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// 폼 체크박스 블록 추가: 루트에 기본 groupLabel/formFieldName/options 와 함께 생성 후 선택 상태로 만든다
addFormCheckboxBlock: () => {
addFormCheckboxBlock: (locale?: AppLocale | null) => {
const id = createId();
const { blocks } = get();
const groupLabel = "체크박스";
const blockDefaults = getEditorBlockDefaultsMessages(locale);
const checkboxDefaults = blockDefaults.formCheckbox;
const groupLabel = checkboxDefaults.groupLabel;
const formFieldName = getNextFormFieldName(blocks, "checkbox");
const options: FormCheckboxOption[] = [
{ label: "체크 1", value: "check_1" },
{ label: "체크 2", value: "check_2" },
{ label: checkboxDefaults.option1Label, value: "check_1" },
{ label: checkboxDefaults.option2Label, value: "check_2" },
];
const newBlock: Block = {
@@ -1465,15 +1478,17 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// 폼 라디오 그룹 블록 추가: 루트에 기본 groupLabel/formFieldName/options 와 함께 생성 후 선택 상태로 만든다
addFormRadioBlock: () => {
addFormRadioBlock: (locale?: AppLocale | null) => {
const id = createId();
const { blocks } = get();
const groupLabel = "라디오 그룹";
const blockDefaults = getEditorBlockDefaultsMessages(locale);
const radioDefaults = blockDefaults.formRadio;
const groupLabel = radioDefaults.groupLabel;
const formFieldName = getNextFormFieldName(blocks, "radio");
const options: FormRadioOption[] = [
{ label: "옵션 A", value: "option_a" },
{ label: "옵션 B", value: "option_b" },
{ label: radioDefaults.optionALabel, value: "option_a" },
{ label: radioDefaults.optionBLabel, value: "option_b" },
];
const newBlock: Block = {
@@ -1549,7 +1564,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// 리스트 블록 추가: 기본 항목/정렬과 함께 생성 후 선택 상태로 만든다
addListBlock: () => {
addListBlock: (locale?: AppLocale | null) => {
const id = createId();
const { selectedBlockId, blocks } = get();
@@ -1570,15 +1585,18 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
const blockDefaults = getEditorBlockDefaultsMessages(locale);
const itemText = blockDefaults.list.firstItemText;
const newBlock: Block = {
id,
type: "list",
props: {
items: ["리스트 아이템 1"],
items: [itemText],
itemsTree: [
{
id: `${id}_item_1`,
text: "리스트 아이템 1",
text: itemText,
children: [],
},
],
@@ -1624,28 +1642,31 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// 폼 블록 추가: 기본 contact 폼 설정과 함께 생성 후 선택 상태로 만든다
addFormBlock: () => {
addFormBlock: (locale?: AppLocale | null) => {
const id = createId();
const blockDefaults = getEditorBlockDefaultsMessages(locale);
const formDefaults = blockDefaults.form;
const fields: FormFieldConfig[] = [
{
id: `${id}_field_name`,
name: "name",
label: "이름",
label: formDefaults.nameLabel,
type: "text",
required: true,
},
{
id: `${id}_field_email`,
name: "email",
label: "이메일",
label: formDefaults.emailLabel,
type: "email",
required: true,
},
{
id: `${id}_field_message`,
name: "message",
label: "메시지",
label: formDefaults.messageLabel,
type: "textarea",
required: true,
},
@@ -1657,8 +1678,8 @@ const createEditorState = (set: any, get: any): EditorState => {
props: {
kind: "contact",
submitTarget: "internal",
successMessage: "성공적으로 전송되었습니다.",
errorMessage: "전송 중 오류가 발생했습니다.",
successMessage: formDefaults.successMessage,
errorMessage: formDefaults.errorMessage,
fields,
fieldIds: [],
submitButtonId: null,
@@ -1675,7 +1696,7 @@ const createEditorState = (set: any, get: any): EditorState => {
},
// 버튼 블록 추가: 기본 라벨/링크와 함께 생성 후 선택 상태로 만든다
addButtonBlock: () => {
addButtonBlock: (locale?: AppLocale | null) => {
const id = createId();
const { selectedBlockId, blocks } = get();
@@ -1695,11 +1716,14 @@ const createEditorState = (set: any, get: any): EditorState => {
}
}
}
const blockDefaults = getEditorBlockDefaultsMessages(locale);
const newBlock: Block = {
id,
type: "button",
props: {
label: "버튼",
label: blockDefaults.button.label,
href: "#",
align: "left",
size: "md",
+7
View File
@@ -10,6 +10,13 @@ export function LocaleSwitcher() {
const handleToggle = () => {
const nextLocale: AppLocale = locale === "en" ? "ko" : "en";
setLocale(nextLocale);
try {
const maxAgeSeconds = 60 * 60 * 24 * 365; // 1 year
document.cookie = `pb-locale=${nextLocale}; path=/; max-age=${maxAgeSeconds}`;
} catch {
// 쿠키 저장 실패는 무시 (로케일 상태는 여전히 컨텍스트에서 유지된다)
}
};
const labelText = locale === "en" ? "EN" : "KO";
+11
View File
@@ -46,6 +46,9 @@ export type EditorMessages = {
listItemToolbarMoveDownLabel: string;
listItemToolbarIndentLabel: string;
listItemToolbarOutdentLabel: string;
imageBlockEmptyPreviewPlaceholder: string;
columnAreaLabel: string;
};
const EDITOR_MESSAGES: Record<AppLocale, EditorMessages> = {
@@ -96,6 +99,10 @@ const EDITOR_MESSAGES: Record<AppLocale, EditorMessages> = {
listItemToolbarMoveDownLabel: "Move item down",
listItemToolbarIndentLabel: "Indent item",
listItemToolbarOutdentLabel: "Outdent item",
imageBlockEmptyPreviewPlaceholder:
"Enter an image URL or upload a file to see the preview here.",
columnAreaLabel: "Column area",
},
ko: {
headerTitle: "페이지 에디터",
@@ -144,6 +151,10 @@ const EDITOR_MESSAGES: Record<AppLocale, EditorMessages> = {
listItemToolbarMoveDownLabel: "아이템 아래로 이동",
listItemToolbarIndentLabel: "아이템 들여쓰기",
listItemToolbarOutdentLabel: "아이템 내어쓰기",
imageBlockEmptyPreviewPlaceholder:
"이미지 URL 을 입력하거나 파일을 업로드하면 여기에서 미리보기가 표시됩니다.",
columnAreaLabel: "컬럼 영역",
},
};
@@ -0,0 +1,121 @@
import type { AppLocale } from "../locale";
import { DEFAULT_LOCALE } from "../locale";
export type EditorBlockDefaultsMessages = {
text: {
defaultText: string;
};
list: {
firstItemText: string;
};
button: {
label: string;
};
form: {
nameLabel: string;
emailLabel: string;
messageLabel: string;
successMessage: string;
errorMessage: string;
};
formInput: {
label: string;
};
formSelect: {
label: string;
option1Label: string;
option2Label: string;
};
formCheckbox: {
groupLabel: string;
option1Label: string;
option2Label: string;
};
formRadio: {
groupLabel: string;
optionALabel: string;
optionBLabel: string;
};
};
const EDITOR_BLOCK_DEFAULTS_MESSAGES: Record<AppLocale, EditorBlockDefaultsMessages> = {
en: {
text: {
defaultText: "New text",
},
list: {
firstItemText: "List item 1",
},
button: {
label: "Button",
},
form: {
nameLabel: "Name",
emailLabel: "Email",
messageLabel: "Message",
successMessage: "Your message has been sent successfully.",
errorMessage: "An error occurred while submitting.",
},
formInput: {
label: "Input field",
},
formSelect: {
label: "Select field",
option1Label: "Option 1",
option2Label: "Option 2",
},
formCheckbox: {
groupLabel: "Checkbox group",
option1Label: "Check 1",
option2Label: "Check 2",
},
formRadio: {
groupLabel: "Radio group",
optionALabel: "Option A",
optionBLabel: "Option B",
},
},
ko: {
text: {
defaultText: "새 텍스트",
},
list: {
firstItemText: "리스트 아이템 1",
},
button: {
label: "버튼",
},
form: {
nameLabel: "이름",
emailLabel: "이메일",
messageLabel: "메시지",
successMessage: "성공적으로 전송되었습니다.",
errorMessage: "전송 중 오류가 발생했습니다.",
},
formInput: {
label: "입력 필드",
},
formSelect: {
label: "셀렉트 필드",
option1Label: "옵션 1",
option2Label: "옵션 2",
},
formCheckbox: {
groupLabel: "체크박스 그룹",
option1Label: "옵션 1",
option2Label: "옵션 2",
},
formRadio: {
groupLabel: "라디오 그룹",
optionALabel: "옵션 A",
optionBLabel: "옵션 B",
},
},
};
export function getEditorBlockDefaultsMessages(
locale: AppLocale | null | undefined,
): EditorBlockDefaultsMessages {
const key = (locale ?? DEFAULT_LOCALE) as AppLocale;
return EDITOR_BLOCK_DEFAULTS_MESSAGES[key] ?? EDITOR_BLOCK_DEFAULTS_MESSAGES[DEFAULT_LOCALE];
}
@@ -120,8 +120,8 @@ const EDITOR_BUTTON_PANEL_MESSAGES: Record<AppLocale, EditorButtonPanelMessages>
imagePlacementOptionTop: "Text top",
imagePlacementOptionBottom: "Text bottom",
paddingXLabel: "Horizontal padding (px)",
paddingYLabel: "Vertical padding (px)",
paddingXLabel: "Horizontal padding",
paddingYLabel: "Vertical padding",
linkLabel: "Button link",
linkAria: "Button link",
@@ -211,8 +211,8 @@ const EDITOR_BUTTON_PANEL_MESSAGES: Record<AppLocale, EditorButtonPanelMessages>
imagePlacementOptionTop: "텍스트 위쪽",
imagePlacementOptionBottom: "텍스트 아래쪽",
paddingXLabel: "가로 패딩 (px)",
paddingYLabel: "세로 패딩 (px)",
paddingXLabel: "가로 패딩",
paddingYLabel: "세로 패딩",
linkLabel: "버튼 링크",
linkAria: "버튼 링크",
@@ -57,9 +57,9 @@ const EDITOR_DIVIDER_PROPERTIES_PANEL_MESSAGES: Record<AppLocale, EditorDividerP
lengthModeAria: "Divider length mode",
lengthModeOptionAuto: "Fit content",
lengthModeOptionFull: "Full width",
lengthModeOptionFixed: "Fixed length (px)",
lengthModeOptionFixed: "Fixed length",
fixedLengthLabel: "Fixed length (px)",
fixedLengthLabel: "Fixed length",
fixedLengthUnitLabel: "(px)",
fixedLengthPresetShort: "Short",
fixedLengthPresetNormal: "Normal",
@@ -94,9 +94,9 @@ const EDITOR_DIVIDER_PROPERTIES_PANEL_MESSAGES: Record<AppLocale, EditorDividerP
lengthModeAria: "구분선 길이 모드",
lengthModeOptionAuto: "내용에 맞춤",
lengthModeOptionFull: "전체 폭",
lengthModeOptionFixed: "고정 길이 (px)",
lengthModeOptionFixed: "고정 길이",
fixedLengthLabel: "고정 길이 (px)",
fixedLengthLabel: "고정 길이",
fixedLengthUnitLabel: "(px)",
fixedLengthPresetShort: "짧게",
fixedLengthPresetNormal: "보통",
@@ -117,7 +117,7 @@ const EDITOR_FORM_CHECKBOX_PANEL_MESSAGES: Record<AppLocale, EditorFormCheckboxP
optionLayoutOptionStacked: "Vertical (default)",
optionLayoutOptionInline: "Horizontal (inline)",
labelGapLabel: "Label/field gap (px)",
labelGapLabel: "Label/field gap",
groupTitleLabel: "Group title",
@@ -148,13 +148,13 @@ const EDITOR_FORM_CHECKBOX_PANEL_MESSAGES: Record<AppLocale, EditorFormCheckboxP
styleSectionTitle: "Field style",
textSizeLabel: "Checkbox text size (px)",
textSizeLabel: "Checkbox text size",
textSizeUnitLabel: "(px)",
lineHeightLabel: "Checkbox line height (px)",
lineHeightLabel: "Checkbox line height",
lineHeightUnitLabel: "(px)",
letterSpacingLabel: "Checkbox letter spacing (px)",
letterSpacingLabel: "Checkbox letter spacing",
letterSpacingUnitLabel: "(px)",
widthModeLabel: "Field width",
@@ -165,13 +165,13 @@ const EDITOR_FORM_CHECKBOX_PANEL_MESSAGES: Record<AppLocale, EditorFormCheckboxP
fixedWidthLabel: "Field fixed width",
fixedWidthUnitLabel: "(px)",
paddingXLabel: "Checkbox horizontal padding (px)",
paddingXLabel: "Checkbox horizontal padding",
paddingXUnitLabel: "(px)",
paddingYLabel: "Checkbox vertical padding (px)",
paddingYLabel: "Checkbox vertical padding",
paddingYUnitLabel: "(px)",
optionGapLabel: "Checkbox option gap (px)",
optionGapLabel: "Checkbox option gap",
optionGapUnitLabel: "(px)",
textColorLabel: "Text color",
@@ -212,7 +212,7 @@ const EDITOR_FORM_CHECKBOX_PANEL_MESSAGES: Record<AppLocale, EditorFormCheckboxP
optionLayoutOptionStacked: "세로 (기본)",
optionLayoutOptionInline: "가로 (인라인)",
labelGapLabel: "라벨/필드 간격 (px)",
labelGapLabel: "라벨/필드 간격",
groupTitleLabel: "그룹 타이틀",
@@ -243,13 +243,13 @@ const EDITOR_FORM_CHECKBOX_PANEL_MESSAGES: Record<AppLocale, EditorFormCheckboxP
styleSectionTitle: "필드 스타일",
textSizeLabel: "체크박스 텍스트 크기 (px)",
textSizeLabel: "체크박스 텍스트 크기",
textSizeUnitLabel: "(px)",
lineHeightLabel: "체크박스 줄간격 (px)",
lineHeightLabel: "체크박스 줄간격",
lineHeightUnitLabel: "(px)",
letterSpacingLabel: "체크박스 자간 (px)",
letterSpacingLabel: "체크박스 자간",
letterSpacingUnitLabel: "(px)",
widthModeLabel: "필드 너비",
@@ -260,13 +260,13 @@ const EDITOR_FORM_CHECKBOX_PANEL_MESSAGES: Record<AppLocale, EditorFormCheckboxP
fixedWidthLabel: "필드 고정 너비",
fixedWidthUnitLabel: "(px)",
paddingXLabel: "체크박스 가로 패딩 (px)",
paddingXLabel: "체크박스 가로 패딩",
paddingXUnitLabel: "(px)",
paddingYLabel: "체크박스 세로 패딩 (px)",
paddingYLabel: "체크박스 세로 패딩",
paddingYUnitLabel: "(px)",
optionGapLabel: "체크박스 옵션 간격 (px)",
optionGapLabel: "체크박스 옵션 간격",
optionGapUnitLabel: "(px)",
textColorLabel: "텍스트 색상",
@@ -32,6 +32,10 @@ export type EditorFormControllerPanelMessages = {
formFixedWidthLabel: string;
formFixedWidthPresetNarrow: string;
formFixedWidthPresetNormal: string;
formFixedWidthPresetWide: string;
marginYLabel: string;
marginYPresetNone: string;
marginYPresetCompact: string;
@@ -52,6 +56,8 @@ export type EditorFormControllerPanelMessages = {
submitButtonLabel: string;
submitButtonAriaLabel: string;
submitButtonNoneOptionLabel: string;
sheetsGuideButtonLabel: string;
sheetsGuideHeading: string;
sheetsGuideCloseAriaLabel: string;
@@ -94,9 +100,13 @@ const EDITOR_FORM_CONTROLLER_PANEL_MESSAGES: Record<AppLocale, EditorFormControl
formWidthModeOptionFull: "Full width",
formWidthModeOptionFixed: "Fixed value",
formFixedWidthLabel: "Form fixed width (px)",
formFixedWidthLabel: "Form fixed width",
marginYLabel: "Form top/bottom margin (px)",
formFixedWidthPresetNarrow: "Narrow",
formFixedWidthPresetNormal: "Normal",
formFixedWidthPresetWide: "Wide",
marginYLabel: "Form top/bottom margin",
marginYPresetNone: "None",
marginYPresetCompact: "Compact",
marginYPresetNormal: "Normal",
@@ -116,6 +126,8 @@ const EDITOR_FORM_CONTROLLER_PANEL_MESSAGES: Record<AppLocale, EditorFormControl
submitButtonLabel: "Submit button",
submitButtonAriaLabel: "Submit button",
submitButtonNoneOptionLabel: "(None)",
sheetsGuideButtonLabel: "Google Sheets integration guide",
sheetsGuideHeading: "Google Sheets integration guide",
sheetsGuideCloseAriaLabel: "Close Google Sheets integration guide",
@@ -160,9 +172,13 @@ const EDITOR_FORM_CONTROLLER_PANEL_MESSAGES: Record<AppLocale, EditorFormControl
formWidthModeOptionFull: "전체 폭",
formWidthModeOptionFixed: "고정 값",
formFixedWidthLabel: "폼 고정 너비 (px)",
formFixedWidthLabel: "폼 고정 너비",
marginYLabel: "폼 위/아래 여백 (px)",
formFixedWidthPresetNarrow: "좁게",
formFixedWidthPresetNormal: "보통",
formFixedWidthPresetWide: "넓게",
marginYLabel: "폼 위/아래 여백",
marginYPresetNone: "없음",
marginYPresetCompact: "좁게",
marginYPresetNormal: "보통",
@@ -182,6 +198,8 @@ const EDITOR_FORM_CONTROLLER_PANEL_MESSAGES: Record<AppLocale, EditorFormControl
submitButtonLabel: "Submit 버튼",
submitButtonAriaLabel: "Submit 버튼",
submitButtonNoneOptionLabel: "선택 안 함",
sheetsGuideButtonLabel: "Google Sheets 연동 가이드",
sheetsGuideHeading: "Google Sheets 연동 가이드",
sheetsGuideCloseAriaLabel: "Google Sheets 연동 가이드 닫기",
@@ -22,6 +22,12 @@ export type EditorFormInputPanelMessages = {
labelGapLabel: string;
labelGapUnitLabel: string;
// Presets for label gap
labelGapPresetTight: string;
labelGapPresetNormal: string;
labelGapPresetRelaxed: string;
labelGapPresetExtra: string;
labelImageUrlLabel: string;
labelImageAltLabel: string;
@@ -44,9 +50,19 @@ export type EditorFormInputPanelMessages = {
lineHeightLabel: string;
lineHeightUnitLabel: string;
// Presets for line height
lineHeightPresetTight: string;
lineHeightPresetNormal: string;
lineHeightPresetLoose: string;
letterSpacingLabel: string;
letterSpacingUnitLabel: string;
// Presets for letter spacing
letterSpacingPresetTight: string;
letterSpacingPresetNormal: string;
letterSpacingPresetWide: string;
textAlignLabel: string;
textAlignOptionLeft: string;
textAlignOptionCenter: string;
@@ -61,12 +77,31 @@ export type EditorFormInputPanelMessages = {
fixedWidthLabel: string;
fixedWidthUnitLabel: string;
// Presets for fixed width
fixedWidthPresetSmall: string;
fixedWidthPresetMedium: string;
fixedWidthPresetLarge: string;
paddingXLabel: string;
paddingXUnitLabel: string;
// Presets for horizontal padding
paddingXPresetXs: string;
paddingXPresetSm: string;
paddingXPresetMd: string;
paddingXPresetLg: string;
paddingXPresetXl: string;
paddingYLabel: string;
paddingYUnitLabel: string;
// Presets for vertical padding
paddingYPresetXs: string;
paddingYPresetSm: string;
paddingYPresetMd: string;
paddingYPresetLg: string;
paddingYPresetXl: string;
textColorLabel: string;
textColorPickerAria: string;
textColorHexAria: string;
@@ -106,9 +141,14 @@ const EDITOR_FORM_INPUT_PANEL_MESSAGES: Record<AppLocale, EditorFormInputPanelMe
layoutLabel: "Layout",
layoutOptionStacked: "Vertical (default)",
layoutOptionInline: "Horizontal (inline)",
labelGapLabel: "Label/field gap (px)",
labelGapLabel: "Label/field gap",
labelGapUnitLabel: "(px)",
labelGapPresetTight: "Tight",
labelGapPresetNormal: "Normal",
labelGapPresetRelaxed: "Relaxed",
labelGapPresetExtra: "Extra",
labelImageUrlLabel: "Label image URL",
labelImageAltLabel: "Label image alt text",
@@ -125,15 +165,23 @@ const EDITOR_FORM_INPUT_PANEL_MESSAGES: Record<AppLocale, EditorFormInputPanelMe
requiredNoticeText: "Required state is configured in the form controller.",
textSizeLabel: "Field text size (px)",
textSizeLabel: "Field text size",
textSizeUnitLabel: "(px)",
lineHeightLabel: "Field line height (px)",
lineHeightLabel: "Field line height",
lineHeightUnitLabel: "(px)",
letterSpacingLabel: "Field letter spacing (px)",
lineHeightPresetTight: "Tight",
lineHeightPresetNormal: "Normal",
lineHeightPresetLoose: "Loose",
letterSpacingLabel: "Field letter spacing",
letterSpacingUnitLabel: "(px)",
letterSpacingPresetTight: "Tight",
letterSpacingPresetNormal: "Normal",
letterSpacingPresetWide: "Wide",
textAlignLabel: "Text alignment",
textAlignOptionLeft: "Left",
textAlignOptionCenter: "Center",
@@ -148,12 +196,28 @@ const EDITOR_FORM_INPUT_PANEL_MESSAGES: Record<AppLocale, EditorFormInputPanelMe
fixedWidthLabel: "Field fixed width",
fixedWidthUnitLabel: "(px)",
paddingXLabel: "Field horizontal padding (px)",
fixedWidthPresetSmall: "Small",
fixedWidthPresetMedium: "Medium",
fixedWidthPresetLarge: "Large",
paddingXLabel: "Field horizontal padding",
paddingXUnitLabel: "(px)",
paddingYLabel: "Field vertical padding (px)",
paddingXPresetXs: "Extra thin",
paddingXPresetSm: "Thin",
paddingXPresetMd: "Normal",
paddingXPresetLg: "Wide",
paddingXPresetXl: "Extra wide",
paddingYLabel: "Field vertical padding",
paddingYUnitLabel: "(px)",
paddingYPresetXs: "Extra thin",
paddingYPresetSm: "Thin",
paddingYPresetMd: "Normal",
paddingYPresetLg: "Wide",
paddingYPresetXl: "Extra wide",
textColorLabel: "Field text color",
textColorPickerAria: "Field text color picker",
textColorHexAria: "Field text color HEX",
@@ -191,9 +255,14 @@ const EDITOR_FORM_INPUT_PANEL_MESSAGES: Record<AppLocale, EditorFormInputPanelMe
layoutLabel: "레이아웃",
layoutOptionStacked: "세로 (기본)",
layoutOptionInline: "가로 (인라인)",
labelGapLabel: "라벨/필드 간격 (px)",
labelGapLabel: "라벨/필드 간격",
labelGapUnitLabel: "(px)",
labelGapPresetTight: "타이트",
labelGapPresetNormal: "보통",
labelGapPresetRelaxed: "느슨",
labelGapPresetExtra: "넓게",
labelImageUrlLabel: "라벨 이미지 URL",
labelImageAltLabel: "라벨 이미지 대체 텍스트",
@@ -210,15 +279,23 @@ const EDITOR_FORM_INPUT_PANEL_MESSAGES: Record<AppLocale, EditorFormInputPanelMe
requiredNoticeText: "필수 여부는 폼 컨트롤러에서 설정합니다.",
textSizeLabel: "필드 텍스트 크기 (px)",
textSizeLabel: "필드 텍스트 크기",
textSizeUnitLabel: "(px)",
lineHeightLabel: "필드 줄간격 (px)",
lineHeightLabel: "필드 줄간격",
lineHeightUnitLabel: "(px)",
letterSpacingLabel: "필드 자간 (px)",
lineHeightPresetTight: "타이트",
lineHeightPresetNormal: "보통",
lineHeightPresetLoose: "루즈",
letterSpacingLabel: "필드 자간",
letterSpacingUnitLabel: "(px)",
letterSpacingPresetTight: "좁게",
letterSpacingPresetNormal: "보통",
letterSpacingPresetWide: "넓게",
textAlignLabel: "텍스트 정렬",
textAlignOptionLeft: "왼쪽",
textAlignOptionCenter: "가운데",
@@ -233,12 +310,28 @@ const EDITOR_FORM_INPUT_PANEL_MESSAGES: Record<AppLocale, EditorFormInputPanelMe
fixedWidthLabel: "필드 고정 너비",
fixedWidthUnitLabel: "(px)",
paddingXLabel: "필드 가로 패딩 (px)",
fixedWidthPresetSmall: "작게",
fixedWidthPresetMedium: "보통",
fixedWidthPresetLarge: "넓게",
paddingXLabel: "필드 가로 패딩",
paddingXUnitLabel: "(px)",
paddingYLabel: "필드 세로 패딩 (px)",
paddingXPresetXs: "아주 얇게",
paddingXPresetSm: "얇게",
paddingXPresetMd: "보통",
paddingXPresetLg: "넓게",
paddingXPresetXl: "아주 넓게",
paddingYLabel: "필드 세로 패딩",
paddingYUnitLabel: "(px)",
paddingYPresetXs: "아주 얇게",
paddingYPresetSm: "얇게",
paddingYPresetMd: "보통",
paddingYPresetLg: "넓게",
paddingYPresetXl: "아주 넓게",
textColorLabel: "필드 텍스트 색상",
textColorPickerAria: "필드 텍스트 색상 피커",
textColorHexAria: "필드 텍스트 색상 HEX",
@@ -4,6 +4,50 @@ import { DEFAULT_LOCALE } from "../locale";
export type EditorFormRadioPanelMessages = {
sectionTitle: string;
// Group title & option configuration
groupTitleTypeLabel: string;
groupTitleTypeOptionText: string;
groupTitleTypeOptionImage: string;
groupTitleDisplayLabel: string;
groupTitleDisplayOptionVisible: string;
groupTitleDisplayOptionHidden: string;
layoutLabel: string;
layoutOptionStacked: string;
layoutOptionInline: string;
optionLayoutLabel: string;
optionLayoutOptionStacked: string;
optionLayoutOptionInline: string;
labelGapLabel: string;
groupTitleLabel: string;
submitKeyLabel: string;
optionsLabel: string;
addOptionButtonLabel: string;
optionLabelPlaceholder: string;
optionValuePlaceholder: string;
optionImageSourceLabel: string;
optionImageSourceOptionUrl: string;
optionImageSourceOptionUpload: string;
optionImageUrlPlaceholder: string;
optionImageUploadLabel: string;
optionImageUploadAria: string;
groupTitleImageSourceLabel: string;
groupTitleImageSourceOptionUrl: string;
groupTitleImageSourceOptionUpload: string;
groupTitleImageUrlLabel: string;
groupTitleImageUploadLabel: string;
groupTitleImageUploadAria: string;
requiredNoticeText: string;
styleSectionTitle: string;
@@ -53,29 +97,72 @@ const EDITOR_FORM_RADIO_PANEL_MESSAGES: Record<AppLocale, EditorFormRadioPanelMe
en: {
sectionTitle: "Radio field",
groupTitleTypeLabel: "Group title type",
groupTitleTypeOptionText: "Text",
groupTitleTypeOptionImage: "Image",
groupTitleDisplayLabel: "Group title display mode",
groupTitleDisplayOptionVisible: "Visible (default)",
groupTitleDisplayOptionHidden: "Hidden",
layoutLabel: "Group title layout",
layoutOptionStacked: "Vertical (default)",
layoutOptionInline: "Horizontal (inline)",
optionLayoutLabel: "Option layout",
optionLayoutOptionStacked: "Vertical (default)",
optionLayoutOptionInline: "Horizontal (inline)",
labelGapLabel: "Label/field gap",
groupTitleLabel: "Group title",
submitKeyLabel: "Submit key",
optionsLabel: "Options",
addOptionButtonLabel: "Add option",
optionLabelPlaceholder: "Label",
optionValuePlaceholder: "Value (value)",
optionImageSourceLabel: "Option image source",
optionImageSourceOptionUrl: "URL",
optionImageSourceOptionUpload: "File upload",
optionImageUrlPlaceholder: "Option image URL (optional)",
optionImageUploadLabel: "Option image file upload",
optionImageUploadAria: "Option image file upload",
groupTitleImageSourceLabel: "Group title image source",
groupTitleImageSourceOptionUrl: "URL",
groupTitleImageSourceOptionUpload: "File upload",
groupTitleImageUrlLabel: "Group title image URL",
groupTitleImageUploadLabel: "Group title image file upload",
groupTitleImageUploadAria: "Group title image file upload",
requiredNoticeText: "Required state is configured in the form controller.",
styleSectionTitle: "Field style",
textSizeLabel: "Radio text size (px)",
textSizeLabel: "Radio text size",
textSizeUnitLabel: "(px)",
lineHeightLabel: "Radio line height (px)",
lineHeightLabel: "Radio line height",
lineHeightUnitLabel: "(px)",
letterSpacingLabel: "Radio letter spacing (px)",
letterSpacingLabel: "Radio letter spacing",
letterSpacingUnitLabel: "(px)",
fixedWidthLabel: "Field fixed width",
fixedWidthUnitLabel: "(px)",
paddingXLabel: "Radio horizontal padding (px)",
paddingXLabel: "Radio horizontal padding",
paddingXUnitLabel: "(px)",
paddingYLabel: "Radio vertical padding (px)",
paddingYLabel: "Radio vertical padding",
paddingYUnitLabel: "(px)",
optionGapLabel: "Radio option gap (px)",
optionGapLabel: "Radio option gap",
optionGapUnitLabel: "(px)",
textColorLabel: "Text color",
@@ -100,29 +187,72 @@ const EDITOR_FORM_RADIO_PANEL_MESSAGES: Record<AppLocale, EditorFormRadioPanelMe
ko: {
sectionTitle: "라디오 필드",
groupTitleTypeLabel: "그룹 타이틀 타입",
groupTitleTypeOptionText: "텍스트",
groupTitleTypeOptionImage: "이미지",
groupTitleDisplayLabel: "그룹 타이틀 표시 방식",
groupTitleDisplayOptionVisible: "표시 (기본)",
groupTitleDisplayOptionHidden: "숨김",
layoutLabel: "그룹 타이틀 레이아웃",
layoutOptionStacked: "세로 (기본)",
layoutOptionInline: "가로 (인라인)",
optionLayoutLabel: "옵션 레이아웃",
optionLayoutOptionStacked: "세로 (기본)",
optionLayoutOptionInline: "가로 (인라인)",
labelGapLabel: "라벨/필드 간격",
groupTitleLabel: "그룹 타이틀",
submitKeyLabel: "전송 키",
optionsLabel: "옵션",
addOptionButtonLabel: "옵션 추가",
optionLabelPlaceholder: "라벨",
optionValuePlaceholder: "값(value)",
optionImageSourceLabel: "옵션 이미지 소스",
optionImageSourceOptionUrl: "URL",
optionImageSourceOptionUpload: "파일 업로드",
optionImageUrlPlaceholder: "옵션 이미지 URL (선택)",
optionImageUploadLabel: "옵션 이미지 파일 업로드",
optionImageUploadAria: "옵션 이미지 파일 업로드",
groupTitleImageSourceLabel: "그룹 타이틀 이미지 소스",
groupTitleImageSourceOptionUrl: "URL",
groupTitleImageSourceOptionUpload: "파일 업로드",
groupTitleImageUrlLabel: "그룹 타이틀 이미지 URL",
groupTitleImageUploadLabel: "그룹 타이틀 이미지 파일 업로드",
groupTitleImageUploadAria: "그룹 타이틀 이미지 파일 업로드",
requiredNoticeText: "필수 여부는 폼 컨트롤러에서 설정합니다.",
styleSectionTitle: "필드 스타일",
textSizeLabel: "라디오 텍스트 크기 (px)",
textSizeLabel: "라디오 텍스트 크기",
textSizeUnitLabel: "(px)",
lineHeightLabel: "라디오 줄간격 (px)",
lineHeightLabel: "라디오 줄간격",
lineHeightUnitLabel: "(px)",
letterSpacingLabel: "라디오 자간 (px)",
letterSpacingLabel: "라디오 자간",
letterSpacingUnitLabel: "(px)",
fixedWidthLabel: "필드 고정 너비",
fixedWidthUnitLabel: "(px)",
paddingXLabel: "라디오 가로 패딩 (px)",
paddingXLabel: "라디오 가로 패딩",
paddingXUnitLabel: "(px)",
paddingYLabel: "라디오 세로 패딩 (px)",
paddingYLabel: "라디오 세로 패딩",
paddingYUnitLabel: "(px)",
optionGapLabel: "라디오 옵션 간격 (px)",
optionGapLabel: "라디오 옵션 간격",
optionGapUnitLabel: "(px)",
textColorLabel: "텍스트 색상",
@@ -89,7 +89,7 @@ const EDITOR_FORM_SELECT_PANEL_MESSAGES: Record<AppLocale, EditorFormSelectPanel
layoutOptionStacked: "Vertical (default)",
layoutOptionInline: "Horizontal (inline)",
labelGapLabel: "Label/field gap (px)",
labelGapLabel: "Label/field gap",
fieldLabelLabel: "Field label",
submitKeyLabel: "Submit key",
@@ -103,13 +103,13 @@ const EDITOR_FORM_SELECT_PANEL_MESSAGES: Record<AppLocale, EditorFormSelectPanel
styleSectionTitle: "Field style",
textSizeLabel: "Select text size (px)",
textSizeLabel: "Select text size",
textSizeUnitLabel: "(px)",
lineHeightLabel: "Select line height (px)",
lineHeightLabel: "Select line height",
lineHeightUnitLabel: "(px)",
letterSpacingLabel: "Select letter spacing (px)",
letterSpacingLabel: "Select letter spacing",
letterSpacingUnitLabel: "(px)",
widthModeLabel: "Field width",
@@ -120,10 +120,10 @@ const EDITOR_FORM_SELECT_PANEL_MESSAGES: Record<AppLocale, EditorFormSelectPanel
fixedWidthLabel: "Field fixed width",
fixedWidthUnitLabel: "(px)",
paddingXLabel: "Select horizontal padding (px)",
paddingXLabel: "Select horizontal padding",
paddingXUnitLabel: "(px)",
paddingYLabel: "Select vertical padding (px)",
paddingYLabel: "Select vertical padding",
paddingYUnitLabel: "(px)",
textColorLabel: "Field text color",
@@ -160,7 +160,7 @@ const EDITOR_FORM_SELECT_PANEL_MESSAGES: Record<AppLocale, EditorFormSelectPanel
layoutOptionStacked: "세로 (기본)",
layoutOptionInline: "가로 (인라인)",
labelGapLabel: "라벨/필드 간격 (px)",
labelGapLabel: "라벨/필드 간격",
fieldLabelLabel: "필드 라벨",
submitKeyLabel: "전송 키",
@@ -174,13 +174,13 @@ const EDITOR_FORM_SELECT_PANEL_MESSAGES: Record<AppLocale, EditorFormSelectPanel
styleSectionTitle: "필드 스타일",
textSizeLabel: "셀렉트 텍스트 크기 (px)",
textSizeLabel: "셀렉트 텍스트 크기",
textSizeUnitLabel: "(px)",
lineHeightLabel: "셀렉트 줄간격 (px)",
lineHeightLabel: "셀렉트 줄간격",
lineHeightUnitLabel: "(px)",
letterSpacingLabel: "셀렉트 자간 (px)",
letterSpacingLabel: "셀렉트 자간",
letterSpacingUnitLabel: "(px)",
widthModeLabel: "필드 너비",
@@ -191,10 +191,10 @@ const EDITOR_FORM_SELECT_PANEL_MESSAGES: Record<AppLocale, EditorFormSelectPanel
fixedWidthLabel: "필드 고정 너비",
fixedWidthUnitLabel: "(px)",
paddingXLabel: "셀렉트 가로 패딩 (px)",
paddingXLabel: "셀렉트 가로 패딩",
paddingXUnitLabel: "(px)",
paddingYLabel: "셀렉트 세로 패딩 (px)",
paddingYLabel: "셀렉트 세로 패딩",
paddingYUnitLabel: "(px)",
textColorLabel: "필드 텍스트 색상",
@@ -78,9 +78,9 @@ const EDITOR_IMAGE_PANEL_MESSAGES: Record<AppLocale, EditorImagePanelMessages> =
widthModeLabel: "Width mode",
widthModeAria: "Image width mode",
widthModeOptionAuto: "Fit to content",
widthModeOptionFixed: "Fixed width (px)",
widthModeOptionFixed: "Fixed width",
fixedWidthLabel: "Fixed width (px)",
fixedWidthLabel: "Fixed width",
fixedWidthUnitLabel: "(px)",
fixedWidthPresetSmall: "Small",
fixedWidthPresetMedium: "Medium",
@@ -123,9 +123,9 @@ const EDITOR_IMAGE_PANEL_MESSAGES: Record<AppLocale, EditorImagePanelMessages> =
widthModeLabel: "너비 모드",
widthModeAria: "이미지 너비 모드",
widthModeOptionAuto: "내용에 맞춤",
widthModeOptionFixed: "고정 너비 (px)",
widthModeOptionFixed: "고정 너비",
fixedWidthLabel: "고정 너비 (px)",
fixedWidthLabel: "고정 너비",
fixedWidthUnitLabel: "(px)",
fixedWidthPresetSmall: "작게",
fixedWidthPresetMedium: "보통",
@@ -64,7 +64,7 @@ const EDITOR_LIST_PANEL_MESSAGES: Record<AppLocale, EditorListPanelMessages> = {
styleSectionTitle: "List style",
fontSizeLabel: "Font size (px)",
fontSizeLabel: "Font size",
fontSizeUnitLabel: "(px)",
fontSizePresetSmall: "Small",
fontSizePresetMedium: "Medium",
@@ -95,7 +95,7 @@ const EDITOR_LIST_PANEL_MESSAGES: Record<AppLocale, EditorListPanelMessages> = {
bulletStyleOptionUpperRoman: "Upper roman (I.)",
bulletStyleOptionNone: "None",
gapLabel: "Item gap (px)",
gapLabel: "Item gap",
gapUnitLabel: "(px)",
gapPresetTight: "Tight",
gapPresetNormal: "Normal",
@@ -113,7 +113,7 @@ const EDITOR_LIST_PANEL_MESSAGES: Record<AppLocale, EditorListPanelMessages> = {
styleSectionTitle: "리스트 스타일",
fontSizeLabel: "글자 크기 (px)",
fontSizeLabel: "글자 크기",
fontSizeUnitLabel: "(px)",
fontSizePresetSmall: "작게",
fontSizePresetMedium: "보통",
@@ -144,7 +144,7 @@ const EDITOR_LIST_PANEL_MESSAGES: Record<AppLocale, EditorListPanelMessages> = {
bulletStyleOptionUpperRoman: "로마 숫자 대문자 (I.)",
bulletStyleOptionNone: "없음",
gapLabel: "아이템 간 여백 (px)",
gapLabel: "아이템 간 여백",
gapUnitLabel: "(px)",
gapPresetTight: "좁게",
gapPresetNormal: "보통",
@@ -117,12 +117,12 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"The label that appears on the button. For example: 'Get started now', 'Contact us'. Short, action-oriented phrases usually work best.",
},
{
label: "Horizontal padding (px)",
label: "Horizontal padding",
description:
"Controls the left and right inner spacing of the button. Larger values create a wider button that stands out more.",
},
{
label: "Vertical padding (px)",
label: "Vertical padding",
description:
"Controls the top and bottom inner spacing. Increasing this value makes the button taller and more prominent.",
},
@@ -210,7 +210,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"Controls whether the image is aligned to the left, center, or right inside the container.",
},
{
label: "Width mode / fixed width (px)",
label: "Width mode / fixed width",
description:
"Choose between auto width (fit content) and a fixed width in px. For thumbnail grids, fixed widths often create a more stable layout.",
},
@@ -238,7 +238,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"Align the whole list to the left, center, or right. Checklists are typically left-aligned, while short feature summaries often use center alignment.",
},
{
label: "Font size (px)",
label: "Font size",
description:
"Controls the font size of list text. Slightly smaller than the main body copy can make it feel like supporting information.",
},
@@ -263,7 +263,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"Choose bullet shapes (●/○/■) or numbered styles (1., a., i.). Number/letter/Roman styles automatically become ordered lists.",
},
{
label: "Item gap (px)",
label: "Item gap",
description:
"Controls the vertical space between list items. For longer descriptions, use a larger gap for better readability.",
},
@@ -286,7 +286,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"Controls line thickness. A thin line is good for subtle separators, while a normal thickness clearly divides sections.",
},
{
label: "Length mode / fixed length (px)",
label: "Length mode / fixed length",
description:
"Choose whether the divider spans the full width, auto width, or a fixed length in px.",
},
@@ -362,7 +362,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"Controls whether the video is left, center, or right aligned inside its container.",
},
{
label: "Width mode / fixed width (px)",
label: "Width mode / fixed width",
description:
"Choose auto width, full width, or a fixed width in px for the video.",
},
@@ -405,12 +405,12 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"Add extra key/value pairs that are always included in the submission. Use one `key=value` per line. Example: `source=landing`.",
},
{
label: "Form width mode / fixed width (px)",
label: "Form width mode / fixed width",
description:
"Choose automatic width, full width, or a fixed px width. Contact forms often work well around 480640px.",
},
{
label: "Form vertical margin (px)",
label: "Form vertical margin",
description:
"Controls the vertical space above and below the form block.",
},
@@ -480,7 +480,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
{
label: "Layout / label-field gap",
description:
"Choose stacked or inline layout for label and field, and adjust the gap between them (px) in inline mode.",
"Choose stacked or inline layout for label and field, and adjust the gap between them in inline mode.",
},
{
label: "Width / fixed width",
@@ -719,12 +719,12 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"버튼 위에 표시될 문구입니다. 예: '지금 시작하기', '문의 남기기'. 짧고 행동을 유도하는 문장을 사용하는 것이 좋습니다.",
},
{
label: "가로 패딩 (px)",
label: "가로 패딩",
description:
"버튼 양 옆의 여백을 조절합니다. 값이 클수록 버튼이 가로로 넓어져 더 강조됩니다.",
},
{
label: "세로 패딩 (px)",
label: "세로 패딩",
description:
"버튼 위·아래 여백을 조절합니다. 값이 클수록 버튼 높이가 높아져 더 눈에 띕니다.",
},
@@ -812,7 +812,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"이미지가 포함된 영역 안에서 왼쪽/가운데/오른쪽 중 어디에 위치할지 결정합니다.",
},
{
label: "너비 모드 / 고정 너비 (px)",
label: "너비 모드 / 고정 너비",
description:
"이미지를 내용 크기에 맞출지(auto) 또는 px 단위로 고정된 너비를 사용할지 선택합니다. 썸네일 그리드에서는 고정 너비를 사용하는 편이 레이아웃이 안정적입니다.",
},
@@ -840,7 +840,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"리스트 전체를 왼쪽/가운데/오른쪽 중 어디에 정렬할지 결정합니다. 체크리스트는 왼쪽 정렬, 특징 요약은 가운데 정렬이 자주 사용됩니다.",
},
{
label: "글자 크기 (px)",
label: "글자 크기",
description:
"리스트 텍스트의 폰트 크기를 조절합니다. 본문보다 약간 작게 설정하면 보조 정보 느낌을 줄 수 있습니다.",
},
@@ -865,7 +865,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"불릿(●/○/■) 또는 번호(1., a., i.) 형식을 선택합니다. 숫자/알파벳/로마 숫자 스타일을 선택하면 자동으로 순서형 리스트(ordered)가 됩니다.",
},
{
label: "아이템 간 여백 (px)",
label: "아이템 간 여백",
description:
"각 리스트 항목 사이의 세로 간격입니다. 설명이 긴 항목이 많다면 여백을 넉넉히 두어 가독성을 확보하세요.",
},
@@ -888,7 +888,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"선의 두께입니다. '얇게'는 가벼운 보조 구분선, '보통'은 섹션 경계를 확실히 나눌 때 사용합니다.",
},
{
label: "길이 모드 / 고정 길이 (px)",
label: "길이 모드 / 고정 길이",
description:
"구분선을 전체 폭으로 쓸지, 내용에 맞출지, 또는 px 단위의 고정 길이를 사용할지 선택합니다.",
},
@@ -964,7 +964,7 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"비디오를 컨테이너 안에서 왼쪽/가운데/오른쪽 중 어디에 위치시킬지 결정합니다.",
},
{
label: "비디오 너비 모드 / 고정 너비 (px)",
label: "비디오 너비 모드 / 고정 너비",
description:
"비디오를 내용 너비에 맞출지, 가로 전체를 채울지, 특정 px 너비로 고정할지 선택합니다.",
},
@@ -1007,12 +1007,12 @@ const EDITOR_PROPERTIES_SIDEBAR_MESSAGES: Record<AppLocale, EditorPropertiesSide
"`key=value` 형식으로 줄바꿈해 작성하면, 모든 폼 제출에 공통으로 포함될 추가 파라미터를 지정할 수 있습니다. 예: `source=landing`.",
},
{
label: "폼 너비 모드 / 폼 고정 너비 (px)",
label: "폼 너비 모드 / 폼 고정 너비",
description:
"폼 전체의 가로 폭을 자동/전체 폭/고정 px 값 중에서 선택합니다. 문의 폼은 보통 480~640px 정도의 고정 폭이 읽기 좋습니다.",
},
{
label: "폼 위/아래 여백 (px)",
label: "폼 위/아래 여백",
description:
"폼 블록 위·아래에 들어가는 공백입니다. 페이지 내 다른 섹션과의 간격을 조절할 때 사용합니다.",
},
@@ -172,7 +172,7 @@ const EDITOR_SECTION_PANEL_MESSAGES: Record<AppLocale, EditorSectionPanelMessage
columnLayoutOptionThreeEqual: "3 columns (1/3 - 1/3 - 1/3)",
columnLayoutOptionCustom: "Custom",
maxWidthLabel: "Max width (px)",
maxWidthLabel: "Max width",
maxWidthUnitLabel: "(px)",
maxWidthPresetExtraNarrow: "Very narrow",
maxWidthPresetNarrow: "Narrow",
@@ -180,7 +180,7 @@ const EDITOR_SECTION_PANEL_MESSAGES: Record<AppLocale, EditorSectionPanelMessage
maxWidthPresetWide: "Wide",
maxWidthPresetExtraWide: "Very wide",
gapXLabel: "Column gap (px)",
gapXLabel: "Column gap",
gapXUnitLabel: "(px)",
gapXPresetZero: "0",
gapXPresetTight: "Tight",
@@ -268,7 +268,7 @@ const EDITOR_SECTION_PANEL_MESSAGES: Record<AppLocale, EditorSectionPanelMessage
columnLayoutOptionThreeEqual: "3열 (1/3 - 1/3 - 1/3)",
columnLayoutOptionCustom: "직접 조정",
maxWidthLabel: "최대 폭 (px)",
maxWidthLabel: "최대 폭",
maxWidthUnitLabel: "(px)",
maxWidthPresetExtraNarrow: "매우 좁게",
maxWidthPresetNarrow: "좁게",
@@ -276,7 +276,7 @@ const EDITOR_SECTION_PANEL_MESSAGES: Record<AppLocale, EditorSectionPanelMessage
maxWidthPresetWide: "넓게",
maxWidthPresetExtraWide: "아주 넓게",
gapXLabel: "컬럼 간 간격 (px)",
gapXLabel: "컬럼 간 간격",
gapXUnitLabel: "(px)",
gapXPresetZero: "0",
gapXPresetTight: "좁게",
@@ -69,7 +69,7 @@ const EDITOR_TEXT_PANEL_MESSAGES: Record<AppLocale, EditorTextPanelMessages> = {
letterSpacingLabel: "Letter spacing",
letterSpacingPresetAria: "Letter spacing preset",
letterSpacingSliderAria: "Letter spacing slider",
letterSpacingInputAria: "Letter spacing custom (px)",
letterSpacingInputAria: "Letter spacing custom",
letterSpacingPresetTighter: "Very tight",
letterSpacingPresetTight: "Tight",
letterSpacingPresetNormal: "Normal",
@@ -118,7 +118,7 @@ const EDITOR_TEXT_PANEL_MESSAGES: Record<AppLocale, EditorTextPanelMessages> = {
letterSpacingLabel: "글자 간격",
letterSpacingPresetAria: "글자 간격 프리셋",
letterSpacingSliderAria: "글자 간격 슬라이더",
letterSpacingInputAria: "글자 간격 커스텀 (px)",
letterSpacingInputAria: "글자 간격 커스텀",
letterSpacingPresetTighter: "아주 좁게",
letterSpacingPresetTight: "좁게",
letterSpacingPresetNormal: "보통",
@@ -111,9 +111,9 @@ const EDITOR_VIDEO_PANEL_MESSAGES: Record<AppLocale, EditorVideoPanelMessages> =
widthModeAria: "Video width mode",
widthModeOptionAuto: "Fit to content",
widthModeOptionFull: "Full width",
widthModeOptionFixed: "Fixed width (px)",
widthModeOptionFixed: "Fixed width",
fixedWidthLabel: "Fixed width (px)",
fixedWidthLabel: "Fixed width",
fixedWidthUnitLabel: "(px)",
fixedWidthPresetSmall: "Small",
fixedWidthPresetMedium: "Medium",
@@ -179,9 +179,9 @@ const EDITOR_VIDEO_PANEL_MESSAGES: Record<AppLocale, EditorVideoPanelMessages> =
widthModeAria: "비디오 너비 모드",
widthModeOptionAuto: "내용에 맞춤",
widthModeOptionFull: "가로 전체",
widthModeOptionFixed: "고정 너비 (px)",
widthModeOptionFixed: "고정 너비",
fixedWidthLabel: "고정 너비 (px)",
fixedWidthLabel: "고정 너비",
fixedWidthUnitLabel: "(px)",
fixedWidthPresetSmall: "작게",
fixedWidthPresetMedium: "보통",