텍스트 스타일 및 프리뷰 렌더링 정리
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
import type { Block, ButtonBlockProps, FormBlockProps } from "@/features/editor/state/editorStore";
|
||||
|
||||
interface FormControllerPanelProps {
|
||||
block: Block; // type === "form"
|
||||
blocks: Block[];
|
||||
selectedBlockId: string | null;
|
||||
updateBlock: (id: string, partial: any) => void;
|
||||
}
|
||||
|
||||
export function FormControllerPanel({ block, blocks, selectedBlockId, updateBlock }: FormControllerPanelProps) {
|
||||
if (!selectedBlockId || block.id !== selectedBlockId) return null;
|
||||
|
||||
const formProps = block.props as FormBlockProps;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 text-xs">
|
||||
<p className="text-slate-400">
|
||||
이 폼은 퍼블릭 페이지에서 <code className="font-mono text-[11px]">/api/forms/submit</code>
|
||||
엔드포인트로 먼저 전송된 뒤, 설정에 따라 내부 처리 또는 Webhook 으로 전달됩니다.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-slate-400">전송 대상</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
value={formProps.submitTarget ?? "internal"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
submitTarget: e.target.value as FormBlockProps["submitTarget"],
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="internal">서버 처리 전용 (Webhook 없이 사용)</option>
|
||||
<option value="webhook">외부 Webhook / Google Sheets 로 전달</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{formProps.submitTarget === "webhook" && (
|
||||
<div className="space-y-2">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-slate-400">Webhook / Google Sheets URL</span>
|
||||
<input
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
|
||||
placeholder="예: https://script.google.com/macros/s/.../exec"
|
||||
value={formProps.destinationUrl ?? ""}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
destinationUrl: e.target.value,
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between gap-2">
|
||||
<span className="text-slate-400">전송 포맷</span>
|
||||
<select
|
||||
className="w-40 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
value={formProps.payloadFormat ?? "form"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
payloadFormat: e.target.value as any,
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="form">폼 데이터 (x-www-form-urlencoded)</option>
|
||||
<option value="json">JSON (application/json)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex items-center justify-between gap-2">
|
||||
<span className="text-slate-400">HTTP 메서드</span>
|
||||
<select
|
||||
className="w-28 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
value={formProps.method ?? "POST"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
method: e.target.value as FormBlockProps["method"],
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-slate-400">Authorization 토큰 (옵션)</span>
|
||||
<input
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
|
||||
placeholder="예: Bearer xxxxxx"
|
||||
value={formProps.headers?.Authorization ?? ""}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
headers: {
|
||||
...(formProps.headers ?? {}),
|
||||
Authorization: e.target.value,
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-slate-400">추가 파라미터 (key=value 형식, 줄바꿈으로 구분)</span>
|
||||
<textarea
|
||||
className="w-full h-16 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] font-mono outline-none focus:border-sky-500"
|
||||
placeholder={"예:\nsource=landing\nformId=contact-hero"}
|
||||
value={Object.entries(formProps.extraParams ?? {})
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join("\n")}
|
||||
onChange={(e) => {
|
||||
const lines = e.target.value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||
const next: Record<string, string> = {};
|
||||
for (const line of lines) {
|
||||
const idx = line.indexOf("=");
|
||||
if (idx > 0) {
|
||||
const key = line.slice(0, idx).trim();
|
||||
const value = line.slice(idx + 1).trim();
|
||||
if (key) next[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
updateBlock(selectedBlockId, {
|
||||
extraParams: next,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 border-t border-slate-800 pt-3 mt-4">
|
||||
<h3 className="text-[11px] font-semibold text-slate-200">폼 컨트롤러</h3>
|
||||
|
||||
<fieldset className="space-y-2" aria-label="폼 필드 매핑">
|
||||
<legend className="text-[11px] text-slate-400 mb-1">폼 필드 매핑</legend>
|
||||
{blocks
|
||||
.filter((b) =>
|
||||
b.type === "formInput" ||
|
||||
b.type === "formSelect" ||
|
||||
b.type === "formCheckbox" ||
|
||||
b.type === "formRadio",
|
||||
)
|
||||
.map((fieldBlock) => {
|
||||
const fieldId = fieldBlock.id;
|
||||
const fieldLabel = (fieldBlock.props as any).label ??
|
||||
(fieldBlock.props as any).groupLabel ??
|
||||
(fieldBlock.props as any).formFieldName ??
|
||||
fieldId;
|
||||
|
||||
const checked = (formProps.fieldIds ?? []).includes(fieldId);
|
||||
|
||||
return (
|
||||
<label
|
||||
key={fieldId}
|
||||
className="flex items-center gap-2 text-[11px] text-slate-200"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3 rounded border-slate-600 bg-slate-950"
|
||||
checked={checked}
|
||||
onChange={(e) => {
|
||||
const current = formProps.fieldIds ?? [];
|
||||
const next = e.target.checked
|
||||
? [...current, fieldId]
|
||||
: current.filter((id) => id !== fieldId);
|
||||
|
||||
updateBlock(selectedBlockId, {
|
||||
fieldIds: next,
|
||||
} as any);
|
||||
}}
|
||||
/>
|
||||
<span>{fieldLabel}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</fieldset>
|
||||
|
||||
<div className="space-y-1">
|
||||
<span className="text-[11px] text-slate-400">Submit 버튼</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[11px] outline-none focus:border-sky-500"
|
||||
aria-label="Submit 버튼"
|
||||
value={formProps.submitButtonId ?? ""}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
submitButtonId: e.target.value || null,
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="">선택 안 함</option>
|
||||
{blocks
|
||||
.filter((b) => b.type === "button")
|
||||
.map((buttonBlock) => {
|
||||
const btnProps = buttonBlock.props as ButtonBlockProps;
|
||||
return (
|
||||
<option key={buttonBlock.id} value={buttonBlock.id}>
|
||||
{btnProps.label || buttonBlock.id}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user