Files
landing-builder/components/wysiwyg/PropsPanel.tsx
T
jaybe 1c607f6331
Auto PR / open-pr (push) Successful in 20s
Auto Label PR / add-automerge-label (pull_request) Successful in 7s
CI / test (pull_request) Successful in 1m34s
feat: WYSIWYG 빌더 1차 기능 완성 및 테스트 추가
2025-11-17 10:04:01 +09:00

116 lines
3.6 KiB
TypeScript

import React, { useEffect, useState } from 'react'
import type { WObject } from '@/lib/wysiwyg/schema'
export type PropsPanelProps = {
object: WObject
onChange?: (next: WObject) => void
}
function num(v: string, fallback: number) {
const n = Number(v)
return Number.isFinite(n) ? n : fallback
}
function isImage(o: WObject): o is Extract<WObject, { type: 'image' }> {
return o.type === 'image'
}
function isText(o: WObject): o is Extract<WObject, { type: 'text' }> {
return o.type === 'text'
}
function isButton(o: WObject): o is Extract<WObject, { type: 'button' }> {
return o.type === 'button'
}
export function PropsPanel({ object, onChange }: PropsPanelProps) {
const [model, setModel] = useState<WObject>(object)
useEffect(() => {
setModel(object)
}, [object])
const emit = (next: WObject) => {
setModel(next)
onChange?.(next)
}
const update = (patch: Partial<Pick<WObject, 'x' | 'y' | 'width' | 'height' | 'rotate'>>): void => {
const next: WObject = { ...model, ...patch }
emit(next)
}
const updateImageProps = (patch: Partial<{ src: string; alt?: string }>): void => {
if (!isImage(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
const updateTextProps = (patch: Partial<{ text: string }>): void => {
if (!isText(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
const updateButtonProps = (patch: Partial<{ label: string }>): void => {
if (!isButton(model)) return
const next: WObject = { ...model, props: { ...model.props, ...patch } }
emit(next)
}
return (
<div aria-label="Properties Panel">
<div>
<label>
X
<input aria-label="X" type="number" value={model.x} onChange={(e) => update({ x: num(e.target.value, model.x) })} />
</label>
<label>
Y
<input aria-label="Y" type="number" value={model.y} onChange={(e) => update({ y: num(e.target.value, model.y) })} />
</label>
<label>
Width
<input aria-label="Width" type="number" value={model.width} onChange={(e) => update({ width: num(e.target.value, model.width) })} />
</label>
<label>
Height
<input aria-label="Height" type="number" value={model.height} onChange={(e) => update({ height: num(e.target.value, model.height) })} />
</label>
<label>
Rotate
<input aria-label="Rotate" type="number" value={model.rotate} onChange={(e) => update({ rotate: num(e.target.value, model.rotate) })} />
</label>
</div>
{isText(model) && (
<div>
<label>
Text
<input aria-label="Text" type="text" value={model.props.text} onChange={(e) => updateTextProps({ text: e.target.value })} />
</label>
</div>
)}
{isButton(model) && (
<div>
<label>
Label
<input aria-label="Label" type="text" value={model.props.label} onChange={(e) => updateButtonProps({ label: e.target.value })} />
</label>
</div>
)}
{isImage(model) && (
<div>
<label>
Src
<input aria-label="Src" type="text" value={model.props.src} onChange={(e) => updateImageProps({ src: e.target.value })} />
</label>
<label>
Alt
<input aria-label="Alt" type="text" value={model.props.alt || ''} onChange={(e) => updateImageProps({ alt: e.target.value })} />
</label>
</div>
)}
</div>
)
}