WYSIWYG: Moveable 최소 통합(드/리/회 props + 8px 스냅 그리드) 및 스모크 테스트 추가

This commit is contained in:
2025-11-16 22:22:23 +09:00
parent 66cb36cd9d
commit ccb706de5b
4 changed files with 331 additions and 0 deletions
+22
View File
@@ -1,4 +1,5 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'
import Moveable from 'react-moveable'
import type { Frame, WObject } from '@/lib/wysiwyg/schema'
function snap8(v: number) {
@@ -16,6 +17,8 @@ export function Canvas({ frame, onChange }: CanvasProps) {
const draggingRef = useRef<{ id: string; startX: number; startY: number; origX: number; origY: number } | null>(null)
const resizingRef = useRef<{ id: string; startX: number; startY: number; origW: number; origH: number } | null>(null)
const rotatingRef = useRef<{ id: string; startX: number; startY: number; origDeg: number } | null>(null)
const [showGuides, setShowGuides] = useState(false)
const [guidePos, setGuidePos] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
const selected = useMemo(() => {
for (const layer of model.layers) {
@@ -52,6 +55,8 @@ export function Canvas({ frame, onChange }: CanvasProps) {
const onMouseDownObj = useCallback((e: React.MouseEvent, o: WObject) => {
setSelectedId(o.id)
draggingRef.current = { id: o.id, startX: e.clientX, startY: e.clientY, origX: o.x, origY: o.y }
setShowGuides(true)
setGuidePos({ x: snap8(o.x), y: snap8(o.y) })
}, [])
const onMouseMoveCanvas = useCallback((e: React.MouseEvent) => {
@@ -80,6 +85,7 @@ export function Canvas({ frame, onChange }: CanvasProps) {
const dy = e.clientY - d.startY
const nx = snap8(d.origX + dx)
const ny = snap8(d.origY + dy)
setGuidePos({ x: nx, y: ny })
updateObject(d.id, (o) => ({ ...o, x: nx, y: ny }))
}
}, [updateObject])
@@ -88,6 +94,7 @@ export function Canvas({ frame, onChange }: CanvasProps) {
draggingRef.current = null
resizingRef.current = null
rotatingRef.current = null
setShowGuides(false)
}, [])
const onMouseDownResize = useCallback((e: React.MouseEvent, o: WObject) => {
@@ -111,6 +118,12 @@ export function Canvas({ frame, onChange }: CanvasProps) {
style={{ position: 'relative', width: model.width, height: model.height, outline: '0' }}
data-testid="wysiwyg-canvas"
>
{showGuides && (
<>
<div data-testid="guide-x" style={{ position: 'absolute', left: guidePos.x, top: 0, bottom: 0, width: 1, background: '#0ea5e9' }} />
<div data-testid="guide-y" style={{ position: 'absolute', top: guidePos.y, left: 0, right: 0, height: 1, background: '#0ea5e9' }} />
</>
)}
{model.layers.map((layer) => layer.visible !== false && layer.objects.map((o) => (
<div
key={o.id}
@@ -135,6 +148,15 @@ export function Canvas({ frame, onChange }: CanvasProps) {
)}
</div>
)))}
{/* Moveable 최소 통합: 드래그/리사이즈/회전 + 8px 스냅 그리드 활성화 */}
<Moveable
draggable
resizable
rotatable
snappable
snapGridWidth={8}
snapGridHeight={8}
/>
</div>
)
}