이미지 파일 업로드 기능
This commit is contained in:
@@ -356,6 +356,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
const optionTextStyle: CSSProperties = {};
|
||||
const groupTextStyle: CSSProperties = {};
|
||||
const textSizeClass = props.fontSizeCustom && props.fontSizeCustom.trim() !== "" ? "" : "text-xs";
|
||||
const groupLabelMode = props.groupLabelMode ?? "text";
|
||||
|
||||
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
||||
groupStyle.width = pxToEm(props.widthPx);
|
||||
@@ -425,7 +426,17 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
.join(" ")}
|
||||
style={groupStyle}
|
||||
>
|
||||
<span style={groupTextStyle}>{props.groupLabel}</span>
|
||||
{groupLabelMode === "image" && props.groupLabelImageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={props.groupLabelImageUrl}
|
||||
alt={props.groupLabel || "폼 그룹 타이틀"}
|
||||
className="inline-block max-w-full h-auto"
|
||||
style={groupTextStyle}
|
||||
/>
|
||||
) : (
|
||||
<span style={groupTextStyle}>{props.groupLabel}</span>
|
||||
)}
|
||||
<div className="flex flex-col" data-testid="preview-form-checkbox-options" style={optionsStyle}>
|
||||
{props.options.map((opt) => (
|
||||
<label key={opt.value} className="inline-flex items-center gap-1">
|
||||
@@ -433,9 +444,19 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-slate-700 bg-slate-900 text-sky-500"
|
||||
/>
|
||||
<span data-testid="preview-form-checkbox-option" style={optionTextStyle}>
|
||||
{opt.label}
|
||||
</span>
|
||||
{opt.labelImageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={opt.labelImageUrl}
|
||||
alt={opt.label || props.groupLabel || "체크박스 옵션"}
|
||||
className="inline-block max-w-full h-auto"
|
||||
style={optionTextStyle}
|
||||
/>
|
||||
) : (
|
||||
<span data-testid="preview-form-checkbox-option" style={optionTextStyle}>
|
||||
{opt.label}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -451,6 +472,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
const optionTextStyle: CSSProperties = {};
|
||||
const groupTextStyle: CSSProperties = {};
|
||||
const textSizeClass = props.fontSizeCustom && props.fontSizeCustom.trim() !== "" ? "" : "text-xs";
|
||||
const groupLabelMode = props.groupLabelMode ?? "text";
|
||||
|
||||
if (widthMode === "fixed" && typeof props.widthPx === "number" && props.widthPx > 0) {
|
||||
groupStyle.width = pxToEm(props.widthPx);
|
||||
@@ -520,7 +542,17 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
.join(" ")}
|
||||
style={groupStyle}
|
||||
>
|
||||
<span style={groupTextStyle}>{props.groupLabel}</span>
|
||||
{groupLabelMode === "image" && props.groupLabelImageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={props.groupLabelImageUrl}
|
||||
alt={props.groupLabel || "폼 그룹 타이틀"}
|
||||
className="inline-block max-w-full h-auto"
|
||||
style={groupTextStyle}
|
||||
/>
|
||||
) : (
|
||||
<span style={groupTextStyle}>{props.groupLabel}</span>
|
||||
)}
|
||||
<div className="flex flex-col" data-testid="preview-form-radio-options" style={optionsStyle}>
|
||||
{props.options.map((opt) => (
|
||||
<label key={opt.value} className="inline-flex items-center gap-1">
|
||||
@@ -529,9 +561,19 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
className="h-4 w-4 border-slate-700 bg-slate-900 text-sky-500"
|
||||
name={props.formFieldName}
|
||||
/>
|
||||
<span data-testid="preview-form-radio-option" style={optionTextStyle}>
|
||||
{opt.label}
|
||||
</span>
|
||||
{opt.labelImageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={opt.labelImageUrl}
|
||||
alt={opt.label || props.groupLabel || "라디오 옵션"}
|
||||
className="inline-block max-w-full h-auto"
|
||||
style={optionTextStyle}
|
||||
/>
|
||||
) : (
|
||||
<span data-testid="preview-form-radio-option" style={optionTextStyle}>
|
||||
{opt.label}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -952,7 +994,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
<img
|
||||
src={field.groupLabelImageUrl}
|
||||
alt={field.label}
|
||||
className="h-4 w-auto"
|
||||
className="max-h-10 w-auto max-w-[200px]"
|
||||
/>
|
||||
) : (
|
||||
<span>{field.label}</span>
|
||||
@@ -986,7 +1028,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
<img
|
||||
src={field.groupLabelImageUrl}
|
||||
alt={field.label}
|
||||
className="h-4 w-auto"
|
||||
className="max-h-10 w-auto max-w-[200px] md:max-w-[300px] lg:max-w-[400px]"
|
||||
/>
|
||||
) : (
|
||||
<span>{field.label}</span>
|
||||
@@ -1005,7 +1047,7 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
<img
|
||||
src={opt.labelImageUrl}
|
||||
alt={opt.label || field.label || "라디오 옵션"}
|
||||
className="h-4 w-auto"
|
||||
className="h-5 w-auto max-w-[120px]"
|
||||
/>
|
||||
) : (
|
||||
<span>{opt.label}</span>
|
||||
@@ -1066,7 +1108,20 @@ export function PublicPageRenderer({ blocks }: PublicPageRendererProps) {
|
||||
}
|
||||
|
||||
const radiusToken = props.borderRadius ?? "md";
|
||||
const radiusPx = radiusToken === "none" ? 0 : radiusToken === "sm" ? 2 : radiusToken === "lg" ? 6 : radiusToken === "full" ? 9999 : 4;
|
||||
const fallbackRadiusPx =
|
||||
radiusToken === "none"
|
||||
? 0
|
||||
: radiusToken === "sm"
|
||||
? 4
|
||||
: radiusToken === "lg"
|
||||
? 16
|
||||
: radiusToken === "full"
|
||||
? 9999
|
||||
: 8;
|
||||
const radiusPx =
|
||||
typeof (props as any).borderRadiusPx === "number" && (props as any).borderRadiusPx >= 0
|
||||
? (props as any).borderRadiusPx
|
||||
: fallbackRadiusPx;
|
||||
imageStyle.borderRadius = radiusPx === 9999 ? "9999px" : pxToEm(radiusPx);
|
||||
|
||||
return (
|
||||
|
||||
@@ -125,6 +125,12 @@ export interface ImageBlockProps {
|
||||
widthPx?: number;
|
||||
// 모서리 둥글기
|
||||
borderRadius?: "none" | "sm" | "md" | "lg" | "full";
|
||||
// 모서리 둥글기 px 값 (이미지에 한해서는 연속적인 px 단위 조정을 지원한다)
|
||||
borderRadiusPx?: number;
|
||||
// 이미지 소스 타입: 업로드된 에셋 또는 외부 URL
|
||||
sourceType?: "asset" | "externalUrl";
|
||||
// 업로드된 에셋인 경우 Asset.id 를 저장해둔다.
|
||||
assetId?: string | null;
|
||||
}
|
||||
|
||||
// 구분선 블록 속성
|
||||
@@ -553,6 +559,8 @@ export interface FormCheckboxBlockProps extends FormFieldStyleProps {
|
||||
// 그룹 타이틀 자체도 텍스트/이미지 모드를 가질 수 있도록 분리한다.
|
||||
groupLabelMode?: FormLabelMode;
|
||||
groupLabelImageUrl?: string;
|
||||
groupLabelImageSource?: "url" | "upload";
|
||||
optionImageSource?: "url" | "upload";
|
||||
}
|
||||
|
||||
// 폼 라디오 그룹 옵션/블록 속성
|
||||
@@ -571,6 +579,8 @@ export interface FormRadioBlockProps extends FormFieldStyleProps {
|
||||
// 라디오 그룹 타이틀의 텍스트/이미지 모드
|
||||
groupLabelMode?: FormLabelMode;
|
||||
groupLabelImageUrl?: string;
|
||||
groupLabelImageSource?: "url" | "upload";
|
||||
optionImageSource?: "url" | "upload";
|
||||
}
|
||||
|
||||
// 폼 필드 설정
|
||||
@@ -611,6 +621,19 @@ export interface FormBlockProps {
|
||||
marginYPx?: number;
|
||||
}
|
||||
|
||||
export type CanvasPreset = "mobile" | "tablet" | "desktop" | "full" | "custom";
|
||||
|
||||
export interface ProjectConfig {
|
||||
title: string;
|
||||
slug: string;
|
||||
canvasPreset: CanvasPreset;
|
||||
canvasWidthPx?: number;
|
||||
canvasBgColorHex?: string;
|
||||
bodyBgColorHex?: string;
|
||||
headHtml?: string;
|
||||
trackingScript?: string;
|
||||
}
|
||||
|
||||
// 공통 블록 모델
|
||||
export interface Block {
|
||||
id: string;
|
||||
@@ -639,6 +662,8 @@ export interface EditorState {
|
||||
selectedListItemId?: string | null;
|
||||
history: Block[][];
|
||||
future: Block[][];
|
||||
projectConfig: ProjectConfig;
|
||||
updateProjectConfig: (partial: Partial<ProjectConfig>) => void;
|
||||
addTextBlock: () => void;
|
||||
addButtonBlock: () => void;
|
||||
addImageBlock: () => void;
|
||||
@@ -716,6 +741,24 @@ const createEditorState = (set: any, get: any): EditorState => ({
|
||||
selectedListItemId: null,
|
||||
history: [],
|
||||
future: [],
|
||||
projectConfig: {
|
||||
title: "새 페이지",
|
||||
slug: "my-landing",
|
||||
canvasPreset: "full",
|
||||
canvasBgColorHex: "#020617",
|
||||
bodyBgColorHex: "#020617",
|
||||
headHtml: "",
|
||||
trackingScript: "",
|
||||
},
|
||||
|
||||
updateProjectConfig: (partial: Partial<ProjectConfig>) => {
|
||||
set((state: EditorState) => ({
|
||||
projectConfig: {
|
||||
...state.projectConfig,
|
||||
...partial,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
// 텍스트 블록 추가: 기본 텍스트와 함께 생성 후 선택 상태로 만든다
|
||||
addTextBlock: () => {
|
||||
|
||||
Reference in New Issue
Block a user