오류 수정
This commit is contained in:
@@ -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, {
|
||||
|
||||
@@ -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
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user