테스트/린트 안정화: 버튼 쿼리 모호성 제거, 훅 deps 보강, vi.spyOn 모킹, 멀티선택 Moveable 간섭 방지 재확인, 스냅 우선순위 회귀 그린
This commit is contained in:
@@ -39,11 +39,18 @@ function download(filename: string, data: Uint8Array) {
|
||||
let textCounter = 1
|
||||
let imageCounter = 1
|
||||
let buttonCounter = 1
|
||||
let inputCounter = 1
|
||||
let selectCounter = 1
|
||||
let textareaCounter = 1
|
||||
let radioCounter = 1
|
||||
let checkboxCounter = 1
|
||||
let layerCounter = 1
|
||||
|
||||
export function WysiwygBuilderPage() {
|
||||
const [frame, setFrame] = useState<Frame>(() => emptyFrame())
|
||||
const assetManagerRef = useRef<AssetManager | null>(null)
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
const [guidesOn, setGuidesOn] = useState(true)
|
||||
const selectedId = selectedIds[selectedIds.length - 1] || null
|
||||
const selectedObject: WObject | null = useMemo(() => {
|
||||
for (const ly of frame.layers) {
|
||||
@@ -66,6 +73,62 @@ export function WysiwygBuilderPage() {
|
||||
setSelectedIds([id])
|
||||
}, [])
|
||||
|
||||
const addRadio = useCallback(() => {
|
||||
const id = `radio-${radioCounter++}`
|
||||
const obj: WObject = { id, type: 'radio', x: 60, y: 240, width: 300, height: 96, rotate: 0, zIndex: 0, props: { name: 'choice', legend: 'Legend', options: ['A','B','C'], defaultValue: 'A', required: false } }
|
||||
setFrame((prev) => ({
|
||||
...prev,
|
||||
layers: prev.layers.map((l, i) => i === 0 ? { ...l, objects: [...l.objects, obj] } : l),
|
||||
}))
|
||||
setSelectedIds([id])
|
||||
}, [])
|
||||
|
||||
const addCheckbox = useCallback(() => {
|
||||
const id = `checkbox-${checkboxCounter++}`
|
||||
const obj: WObject = { id, type: 'checkbox', x: 60, y: 360, width: 320, height: 96, rotate: 0, zIndex: 0, props: { name: 'agree', legend: 'Legend', options: ['One','Two','Three'], defaultValues: ['One'], required: false } }
|
||||
setFrame((prev) => ({
|
||||
...prev,
|
||||
layers: prev.layers.map((l, i) => i === 0 ? { ...l, objects: [...l.objects, obj] } : l),
|
||||
}))
|
||||
setSelectedIds([id])
|
||||
}, [])
|
||||
|
||||
const addTextarea = useCallback(() => {
|
||||
const id = `textarea-${textareaCounter++}`
|
||||
const obj: WObject = { id, type: 'textarea', x: 60, y: 180, width: 320, height: 96, rotate: 0, zIndex: 0, props: { name: 'message', label: 'Message', placeholder: '', rows: 3, required: false } }
|
||||
setFrame((prev) => ({
|
||||
...prev,
|
||||
layers: prev.layers.map((l, i) => i === 0 ? { ...l, objects: [...l.objects, obj] } : l),
|
||||
}))
|
||||
setSelectedIds([id])
|
||||
}, [])
|
||||
|
||||
const addSelect = useCallback(() => {
|
||||
const id = `select-${selectCounter++}`
|
||||
const obj: WObject = { id, type: 'select', x: 60, y: 120, width: 260, height: 48, rotate: 0, zIndex: 0, props: { name: 'field', label: 'Label', options: ['S','M','L'], defaultValue: '' } }
|
||||
setFrame((prev) => ({
|
||||
...prev,
|
||||
layers: prev.layers.map((l, i) => i === 0 ? { ...l, objects: [...l.objects, obj] } : l),
|
||||
}))
|
||||
setSelectedIds([id])
|
||||
}, [])
|
||||
|
||||
const addLayer = useCallback(() => {
|
||||
const lid = `layer-${layerCounter++}`
|
||||
const tid = `text-${textCounter++}`
|
||||
const layer: Frame['layers'][number] = {
|
||||
id: lid,
|
||||
name: `Layer ${layerCounter - 1}`,
|
||||
visible: true,
|
||||
locked: false,
|
||||
objects: [
|
||||
{ id: tid, type: 'text', x: 60, y: 60, width: 160, height: 40, rotate: 0, zIndex: 0, props: { text: `L${layerCounter - 1}`, fontSize: 18, color: '#ffffff' } },
|
||||
],
|
||||
}
|
||||
setFrame((prev) => ({ ...prev, layers: [...prev.layers, layer] }))
|
||||
setSelectedIds([tid])
|
||||
}, [])
|
||||
|
||||
const addImage = useCallback(() => {
|
||||
const id = `image-${imageCounter++}`
|
||||
const obj: WObject = { id, type: 'image', x: 60, y: 120, width: 120, height: 90, rotate: 0, zIndex: 0, props: { src: 'https://via.placeholder.com/120x90', alt: '' } }
|
||||
@@ -122,6 +185,16 @@ export function WysiwygBuilderPage() {
|
||||
setSelectedIds([id])
|
||||
}, [])
|
||||
|
||||
const addInput = useCallback(() => {
|
||||
const id = `input-${inputCounter++}`
|
||||
const obj: WObject = { id, type: 'input', x: 60, y: 60, width: 260, height: 48, rotate: 0, zIndex: 0, props: { name: 'field', label: 'Label', placeholder: '', required: false } }
|
||||
setFrame((prev) => ({
|
||||
...prev,
|
||||
layers: prev.layers.map((l, i) => i === 0 ? { ...l, objects: [...l.objects, obj] } : l),
|
||||
}))
|
||||
setSelectedIds([id])
|
||||
}, [])
|
||||
|
||||
const onChangeObject = useCallback((next: WObject) => {
|
||||
setFrame((prev) => ({
|
||||
...prev,
|
||||
@@ -152,6 +225,7 @@ export function WysiwygBuilderPage() {
|
||||
<div className="flex gap-2 items-center">
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2" onClick={addText}>Add Text</button>
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2" onClick={addImage}>Add Image</button>
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2" onClick={addLayer}>Add Layer</button>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/jpg,image/webp,image/svg+xml"
|
||||
@@ -160,6 +234,11 @@ export function WysiwygBuilderPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2 mr-2" onClick={addInput}>Add Input</button>
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2 mr-2" onClick={addSelect}>Add Select</button>
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2 mr-2" onClick={addTextarea}>Add Textarea</button>
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2 mr-2" onClick={addRadio}>Add Radio</button>
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2 mr-2" onClick={addCheckbox}>Add Checkbox</button>
|
||||
<button type="button" className="bg-gray-200 rounded px-3 py-2" onClick={addButton}>Add Button</button>
|
||||
</div>
|
||||
<div className="border rounded p-2">
|
||||
@@ -169,10 +248,20 @@ export function WysiwygBuilderPage() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold">WYSIWYG Builder</h1>
|
||||
<button type="button" className="bg-emerald-600 text-white rounded px-4 py-2" onClick={doExport}>Export</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={guidesOn ? 'true' : 'false'}
|
||||
className="bg-gray-200 rounded px-3 py-2"
|
||||
onClick={() => setGuidesOn((v) => !v)}
|
||||
>
|
||||
Guides
|
||||
</button>
|
||||
<button type="button" className="bg-emerald-600 text-white rounded px-4 py-2" onClick={doExport}>Export</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border rounded p-2 inline-block" style={{ background: '#111111' }}>
|
||||
<Canvas frame={frame} onChange={onChangeFrame} onSelectIds={onSelectIds} />
|
||||
<Canvas key={`guides-${guidesOn ? 'on' : 'off'}`} frame={frame} onChange={onChangeFrame} onSelectIds={onSelectIds} enableGuides={guidesOn} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="border rounded p-2">
|
||||
|
||||
Reference in New Issue
Block a user