feat: WYSIWYG 빌더 1차 기능 완성 및 테스트 추가
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
"use client"
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Moveable from 'react-moveable'
|
||||
import type { Frame, WObject } from '@/lib/wysiwyg/schema'
|
||||
import { snapAngle as snapAngleUtil } from '@/lib/wysiwyg/snap'
|
||||
@@ -24,10 +25,13 @@ if (typeof document !== 'undefined') {
|
||||
export type CanvasProps = {
|
||||
frame: Frame
|
||||
onChange?: (next: Frame) => void
|
||||
onSelectIds?: (ids: string[]) => void
|
||||
}
|
||||
|
||||
export function Canvas({ frame, onChange }: CanvasProps) {
|
||||
export function Canvas({ frame, onChange, onSelectIds }: CanvasProps) {
|
||||
const [model, setModel] = useState<Frame>(frame)
|
||||
// 외부 프레임 변경(예: 빌더에서 PropsPanel 수정/객체 추가)이 들어오면 동기화
|
||||
useEffect(() => { setModel(frame) }, [frame])
|
||||
const initialId = model.layers[0]?.objects[0]?.id ?? null
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>(initialId ? [initialId] : [])
|
||||
const selectedId = selectedIds[selectedIds.length - 1] ?? null
|
||||
@@ -117,10 +121,14 @@ export function Canvas({ frame, onChange }: CanvasProps) {
|
||||
setSelectedIds((prev) => {
|
||||
const exists = prev.includes(o.id)
|
||||
if (exists) return prev.filter((id) => id !== o.id)
|
||||
return [...prev, o.id]
|
||||
const next = [...prev, o.id]
|
||||
try { onSelectIds?.(next) } catch {}
|
||||
return next
|
||||
})
|
||||
} else {
|
||||
setSelectedIds([o.id])
|
||||
const next = [o.id]
|
||||
setSelectedIds(next)
|
||||
try { onSelectIds?.(next) } catch {}
|
||||
}
|
||||
draggingRef.current = { id: o.id, startX: e.clientX, startY: e.clientY, origX: o.x, origY: o.y }
|
||||
setShowGuides(true)
|
||||
@@ -150,6 +158,7 @@ export function Canvas({ frame, onChange }: CanvasProps) {
|
||||
}
|
||||
}
|
||||
setSelectedIds(hits)
|
||||
try { onSelectIds?.(hits) } catch {}
|
||||
return
|
||||
}
|
||||
const rot = rotatingRef.current
|
||||
@@ -259,6 +268,7 @@ export function Canvas({ frame, onChange }: CanvasProps) {
|
||||
if ((e.target as HTMLElement).dataset.testid === 'wysiwyg-canvas') {
|
||||
setSelectedIds([])
|
||||
setMarquee({ active: true, x0: e.clientX, y0: e.clientY, x1: e.clientX, y1: e.clientY })
|
||||
try { onSelectIds?.([]) } catch {}
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMoveCanvas}
|
||||
@@ -280,8 +290,32 @@ export function Canvas({ frame, onChange }: CanvasProps) {
|
||||
aria-label={`object ${o.id}`}
|
||||
onMouseDown={(e) => onMouseDownObj(e, o)}
|
||||
ref={selectedId === o.id ? (el) => { selectedElRef.current = el } : undefined}
|
||||
style={{ position: 'absolute', left: o.x, top: o.y, width: o.width, height: o.height, transform: `rotate(${o.rotate}deg)`, outline: selectedIds.includes(o.id)? '2px solid #0ea5e9':'none' }}
|
||||
style={{ position: 'absolute', left: o.x, top: o.y, width: o.width, height: o.height, transform: `rotate(${o.rotate}deg)`, outline: selectedIds.includes(o.id)? '2px solid #0ea5e9':'none', border: '1px solid rgba(148,163,184,0.6)', background: 'rgba(15,23,42,0.8)', color: '#f9fafb', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 4, boxSizing: 'border-box' }}
|
||||
>
|
||||
{/* 실제 오브젝트 콘텐츠 렌더링 */}
|
||||
{o.type === 'text' && (
|
||||
<p style={{ margin: 0, fontSize: o.props.fontSize, color: o.props.color }}>{o.props.text}</p>
|
||||
)}
|
||||
{o.type === 'image' && (
|
||||
<img src={o.props.src} alt={o.props.alt || ''} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }} />
|
||||
)}
|
||||
{o.type === 'button' && (
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
borderRadius: 9999,
|
||||
border: 'none',
|
||||
padding: '4px 12px',
|
||||
fontSize: 12,
|
||||
cursor: 'pointer',
|
||||
background: o.props.background ?? '#2563eb',
|
||||
color: o.props.color ?? '#ffffff',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{o.props.label}
|
||||
</button>
|
||||
)}
|
||||
{selectedIds.includes(o.id) && (
|
||||
<div
|
||||
data-testid={`resize-handle-${o.id}`}
|
||||
@@ -302,7 +336,6 @@ export function Canvas({ frame, onChange }: CanvasProps) {
|
||||
<Moveable
|
||||
target={selectedId ? `[data-testid="obj-${selectedId}"]` : undefined}
|
||||
draggable
|
||||
resizable
|
||||
rotatable
|
||||
snappable
|
||||
snapGridWidth={8}
|
||||
@@ -449,22 +482,6 @@ export function Canvas({ frame, onChange }: CanvasProps) {
|
||||
// 드래그 종료 시 가이드 숨김
|
||||
setShowGuides(false)
|
||||
}}
|
||||
onResize={({ width, height }) => {
|
||||
if (!selected) return
|
||||
const nw = Math.max(8, snap8(typeof width === 'number' ? width : 0))
|
||||
const nh = Math.max(8, snap8(typeof height === 'number' ? height : 0))
|
||||
if (selectedIds.length > 1) {
|
||||
const dw = nw - selected.width
|
||||
const dh = nh - selected.height
|
||||
updateObjects(selectedIds, (o) => ({
|
||||
...o,
|
||||
width: Math.max(8, o.width + dw),
|
||||
height: Math.max(8, o.height + dh),
|
||||
}))
|
||||
} else {
|
||||
updateObject(selected.id, (o) => ({ ...o, width: nw, height: nh }))
|
||||
}
|
||||
}}
|
||||
onRotate={({ rotate, inputEvent }) => {
|
||||
if (!selected) return
|
||||
const fine = isShiftEvent(inputEvent) && !!inputEvent.shiftKey
|
||||
|
||||
Reference in New Issue
Block a user