feat: WYSIWYG 빌더 1차 기능 완성 및 테스트 추가
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

This commit is contained in:
2025-11-17 10:04:01 +09:00
parent 9ced249015
commit 1c607f6331
13 changed files with 660 additions and 28 deletions
+39 -22
View File
@@ -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