WYSIWYG: Moveable 최소 통합(드/리/회 props + 8px 스냅 그리드) 및 스모크 테스트 추가
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { Canvas } from '@/components/wysiwyg/Canvas'
|
||||
import type { Frame } from '@/lib/wysiwyg/schema'
|
||||
|
||||
vi.mock('react-moveable', () => {
|
||||
const Mock = (props: Record<string, unknown>) => <div data-testid="moveable" data-props={JSON.stringify({
|
||||
draggable: props.draggable,
|
||||
resizable: props.resizable,
|
||||
rotatable: props.rotatable,
|
||||
snappable: props.snappable,
|
||||
snapGridWidth: props.snapGridWidth,
|
||||
snapGridHeight: props.snapGridHeight,
|
||||
})} />
|
||||
return { default: Mock }
|
||||
})
|
||||
|
||||
const frame: Frame = {
|
||||
id: 'frame1',
|
||||
width: 800,
|
||||
height: 600,
|
||||
background: '#ffffff',
|
||||
layers: [
|
||||
{
|
||||
id: 'layer1',
|
||||
name: 'Layer 1',
|
||||
visible: true,
|
||||
locked: false,
|
||||
objects: [
|
||||
{ id: 'obj1', type: 'text', x: 10, y: 12, width: 100, height: 40, rotate: 0, zIndex: 0, props: { text: 'Hello', fontSize: 16, color: '#000000' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('Canvas + Moveable integration (smoke)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders Moveable with draggable/resizable/rotatable and 8px grid', () => {
|
||||
render(<Canvas frame={frame} />)
|
||||
|
||||
const mv = screen.getByTestId('moveable')
|
||||
expect(mv).toBeInTheDocument()
|
||||
|
||||
const props = JSON.parse(mv.getAttribute('data-props') || '{}')
|
||||
expect(props.draggable).toBe(true)
|
||||
expect(props.resizable).toBe(true)
|
||||
expect(props.rotatable).toBe(true)
|
||||
expect(props.snappable).toBe(true)
|
||||
expect(props.snapGridWidth).toBe(8)
|
||||
expect(props.snapGridHeight).toBe(8)
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user