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

This commit is contained in:
2025-12-10 15:56:51 +09:00
parent 73e9bc6a1c
commit f71207aeb5
127 changed files with 7346 additions and 2079 deletions
+50 -46
View File
@@ -4,6 +4,8 @@ import { useState } from "react";
import type { Block, ButtonBlockProps, FormBlockProps } from "@/features/editor/state/editorStore";
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
import { computeFormControllerPublicTokens } from "@/features/editor/utils/formHelpers";
import { useAppLocale } from "@/features/i18n/LocaleProvider";
import { getEditorFormControllerPanelMessages } from "@/features/i18n/messages/editorFormControllerPanel";
interface FormControllerPanelProps {
block: Block; // type === "form"
@@ -18,6 +20,8 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
const [showSheetsGuide, setShowSheetsGuide] = useState(false);
const formProps = block.props as FormBlockProps;
const locale = useAppLocale();
const m = getEditorFormControllerPanelMessages(locale);
const sheetsScriptExample = (() => {
const tokens = computeFormControllerPublicTokens(block, blocks);
@@ -71,13 +75,14 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
return (
<div className="space-y-4 text-xs">
<p className="text-slate-400">
<code className="font-mono text-[11px]">/api/forms/submit</code>
, Webhook .
{m.introTextPrefix}
<code className="font-mono text-[11px]">/api/forms/submit</code>
{m.introTextSuffix}
</p>
<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.submitTargetLabel}</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={formProps.submitTarget ?? "internal"}
@@ -87,18 +92,18 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
} as any)
}
>
<option value="internal"> (Webhook )</option>
<option value="webhook"> Webhook / Google Sheets </option>
<option value="internal">{m.submitTargetOptionInternal}</option>
<option value="webhook">{m.submitTargetOptionWebhook}</option>
</select>
</label>
{formProps.submitTarget === "webhook" && (
<div className="space-y-3">
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400">Webhook URL (: Google Apps Script URL)</span>
<span className="text-slate-500 dark:text-slate-400">{m.webhookUrlLabel}</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"
placeholder="예: https://script.google.com/macros/s/.../exec"
placeholder={m.webhookUrlPlaceholder}
value={formProps.destinationUrl ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -108,7 +113,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
/>
</label>
<label className="flex items-center justify-between gap-2">
<span className="text-slate-500 dark:text-slate-400"> </span>
<span className="text-slate-500 dark:text-slate-400">{m.payloadFormatLabel}</span>
<select
className="w-40 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={formProps.payloadFormat ?? "form"}
@@ -118,12 +123,12 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
} as any)
}
>
<option value="form"> (x-www-form-urlencoded)</option>
<option value="json">JSON (application/json)</option>
<option value="form">{m.payloadFormatOptionForm}</option>
<option value="json">{m.payloadFormatOptionJson}</option>
</select>
</label>
<label className="flex items-center justify-between gap-2">
<span className="text-slate-500 dark:text-slate-400">HTTP </span>
<span className="text-slate-500 dark:text-slate-400">{m.httpMethodLabel}</span>
<select
className="w-28 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={formProps.method ?? "POST"}
@@ -138,10 +143,10 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400">Authorization ()</span>
<span className="text-slate-500 dark:text-slate-400">{m.authTokenLabel}</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"
placeholder="예: Bearer xxxxxx"
placeholder={m.authTokenPlaceholder}
value={formProps.headers?.Authorization ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -154,10 +159,10 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-slate-500 dark:text-slate-400"> (key=value , )</span>
<span className="text-slate-500 dark:text-slate-400">{m.extraParamsLabel}</span>
<textarea
className="w-full h-16 rounded border border-slate-300 bg-white px-2 py-1 text-[11px] font-mono text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
placeholder={"예:\nsource=landing\nformId=contact-hero"}
placeholder={m.extraParamsPlaceholder}
value={Object.entries(formProps.extraParams ?? {})
.map(([k, v]) => `${k}=${v}`)
.join("\n")}
@@ -186,7 +191,7 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
className="inline-flex items-center gap-1 rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
onClick={() => setShowSheetsGuide(true)}
>
Google Sheets
{m.sheetsGuideButtonLabel}
</button>
</div>
</div>
@@ -194,9 +199,9 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</div>
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">{m.layoutSectionTitle}</h3>
<label className="flex flex-col gap-1 text-xs text-slate-500 dark:text-slate-400">
<span> </span>
<span>{m.formWidthModeLabel}</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={formProps.formWidthMode ?? "auto"}
@@ -206,14 +211,14 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
} as any)
}
>
<option value="auto"></option>
<option value="full"> </option>
<option value="fixed"> </option>
<option value="auto">{m.formWidthModeOptionAuto}</option>
<option value="full">{m.formWidthModeOptionFull}</option>
<option value="fixed">{m.formWidthModeOptionFixed}</option>
</select>
</label>
{(formProps.formWidthMode ?? "auto") === "fixed" && (
<NumericPropertyControl
label="폼 고정 너비 (px)"
label={m.formFixedWidthLabel}
unitLabel="(px)"
value={typeof formProps.formWidthPx === "number" ? formProps.formWidthPx : 480}
min={160}
@@ -232,17 +237,17 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
/>
)}
<NumericPropertyControl
label="폼 위/아래 여백 (px)"
label={m.marginYLabel}
unitLabel="(px)"
value={typeof formProps.marginYPx === "number" ? formProps.marginYPx : 24}
min={0}
max={160}
step={4}
presets={[
{ id: "none", label: "없음", value: 0 },
{ id: "compact", label: "좁게", value: 16 },
{ id: "normal", label: "보통", value: 24 },
{ id: "spacious", label: "넓게", value: 40 },
{ id: "none", label: m.marginYPresetNone, value: 0 },
{ id: "compact", label: m.marginYPresetCompact, value: 16 },
{ id: "normal", label: m.marginYPresetNormal, value: 24 },
{ id: "spacious", label: m.marginYPresetSpacious, value: 40 },
]}
onChangeValue={(v) =>
updateBlock(selectedBlockId, {
@@ -253,12 +258,12 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</div>
<div className="space-y-2 border-t border-slate-800 pt-3 mt-4">
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">{m.messagesSectionTitle}</h3>
<label className="flex flex-col gap-1 text-xs text-slate-500 dark:text-slate-400">
<span> </span>
<span>{m.successMessageLabel}</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"
placeholder="예: 성공적으로 전송되었습니다."
placeholder={m.successMessagePlaceholder}
value={formProps.successMessage ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -268,10 +273,10 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
/>
</label>
<label className="flex flex-col gap-1 text-xs text-slate-500 dark:text-slate-400">
<span> </span>
<span>{m.errorMessageLabel}</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"
placeholder="예: 전송 중 오류가 발생했습니다."
placeholder={m.errorMessagePlaceholder}
value={formProps.errorMessage ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -283,10 +288,10 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</div>
<div className="space-y-3 border-t border-slate-800 pt-3 mt-4">
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200"> </h3>
<h3 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">{m.controllerSectionTitle}</h3>
<fieldset className="space-y-2" aria-label="폼 필드 매핑">
<legend className="text-[11px] text-slate-400 mb-1"> </legend>
<fieldset className="space-y-2" aria-label={m.fieldMappingAriaLabel}>
<legend className="text-[11px] text-slate-400 mb-1">{m.fieldMappingLegend}</legend>
{blocks
.filter((b) =>
b.type === "formInput" ||
@@ -355,10 +360,10 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
</fieldset>
<div className="space-y-1">
<span className="text-[11px] text-slate-400">Submit </span>
<span className="text-[11px] text-slate-400">{m.submitButtonLabel}</span>
<select
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
aria-label="Submit 버튼"
aria-label={m.submitButtonAriaLabel}
value={formProps.submitButtonId ?? ""}
onChange={(e) =>
updateBlock(selectedBlockId, {
@@ -393,27 +398,26 @@ export function FormControllerPanel({ block, blocks, selectedBlockId, updateBloc
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<div className="w-full max-w-xl rounded border border-slate-200 bg-white px-4 py-3 text-xs text-slate-900 shadow-lg dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">
<div className="mb-2 flex items-center justify-between">
<h4 className="text-[11px] font-semibold">Google Sheets </h4>
<h4 className="text-[11px] font-semibold">{m.sheetsGuideHeading}</h4>
<button
type="button"
className="text-[11px] text-slate-400 hover:text-slate-100"
onClick={() => setShowSheetsGuide(false)}
aria-label="Google Sheets 연동 가이드 닫기"
aria-label={m.sheetsGuideCloseAriaLabel}
>
×
</button>
</div>
<p className="text-[11px] text-slate-500 dark:text-slate-400 leading-relaxed">
, Google Apps Script URL .
.
{m.sheetsGuideIntro}
</p>
<ol className="list-decimal list-inside space-y-1 text-[11px] text-slate-400 mt-2">
<li>Google Sheets , 1 name, email, message .</li>
<li> "확장 프로그램 → 앱스 스크립트" .</li>
<li>"배포 → 새 배포" , URL(/exec) Webhook URL .</li>
<li>{m.sheetsGuideStep1}</li>
<li>{m.sheetsGuideStep2}</li>
<li>{m.sheetsGuideStep3}</li>
</ol>
<div className="mt-2 space-y-1">
<span className="text-[11px] text-slate-500 dark:text-slate-400"> Apps Script </span>
<span className="text-[11px] text-slate-500 dark:text-slate-400">{m.sheetsGuideExampleCodeLabel}</span>
<textarea
className="w-full h-40 rounded border border-slate-300 bg-white px-2 py-1 text-[11px] font-mono text-slate-900 outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
readOnly