diff --git a/resources/views/esign/fields.blade.php b/resources/views/esign/fields.blade.php index 31a2e0a1..65c893ab 100644 --- a/resources/views/esign/fields.blade.php +++ b/resources/views/esign/fields.blade.php @@ -156,9 +156,10 @@ className={`w-full relative rounded border-2 transition-all ${p === currentPage }; // ─── FieldOverlay (interact.js) ─── +// 기본: 점선 + 드래그=이동 / 더블클릭: 실선 + 핸들=크기조절 / ESC: 모드해제 const FieldOverlay = ({ field, index, selected, signers, containerRef, onSelect, onUpdate, onDragEnd, gridEnabled }) => { const fieldRef = useRef(null); - // Ref로 최신 field/callbacks 추적 (interact.js 재초기화 방지) + const [resizeMode, setResizeMode] = useState(false); const latestRef = useRef({ field, index, onUpdate, onDragEnd }); latestRef.current = { field, index, onUpdate, onDragEnd }; @@ -167,7 +168,18 @@ className={`w-full relative rounded border-2 transition-all ${p === currentPage const signerName = signers[signerIdx]?.name || '?'; const typeInfo = FIELD_TYPES.find(t => t.value === field.field_type) || FIELD_TYPES[0]; - // interact.js는 gridEnabled 변경 시에만 재초기화 + // 선택 해제 시 리사이즈 모드 자동 해제 + useEffect(() => { if (!selected) setResizeMode(false); }, [selected]); + + // ESC 키로 리사이즈 모드 해제 + useEffect(() => { + if (!resizeMode) return; + const h = (e) => { if (e.key === 'Escape') { e.stopPropagation(); setResizeMode(false); } }; + window.addEventListener('keydown', h); + return () => window.removeEventListener('keydown', h); + }, [resizeMode]); + + // interact.js: resizeMode에 따라 드래그 또는 리사이즈만 활성화 useEffect(() => { const el = fieldRef.current; if (!el) return; @@ -182,87 +194,103 @@ className={`w-full relative rounded border-2 transition-all ${p === currentPage })); } - const interactable = interact(el) - .draggable({ - inertia: false, - modifiers, - listeners: { - move(event) { - const { field: f, index: idx, onUpdate: update } = latestRef.current; - const rect = containerRef.current.getBoundingClientRect(); - const dx = (event.dx / rect.width) * 100; - const dy = (event.dy / rect.height) * 100; - const newX = Math.max(0, Math.min(100 - f.width, f.position_x + dx)); - const newY = Math.max(0, Math.min(100 - f.height, f.position_y + dy)); - update(idx, { position_x: round2(newX), position_y: round2(newY) }); + const interactable = interact(el); + + if (resizeMode) { + interactable + .draggable({ enabled: false }) + .resizable({ + edges: { left: true, right: true, top: true, bottom: true }, + modifiers: [ + interact.modifiers.restrictEdges({ outer: 'parent' }), + interact.modifiers.restrictSize({ min: { width: 30, height: 20 } }), + ], + listeners: { + move(event) { + const { field: f, index: idx, onUpdate: update } = latestRef.current; + const rect = containerRef.current.getBoundingClientRect(); + const newX = f.position_x + (event.deltaRect.left / rect.width) * 100; + const newY = f.position_y + (event.deltaRect.top / rect.height) * 100; + const newW = (event.rect.width / rect.width) * 100; + const newH = (event.rect.height / rect.height) * 100; + update(idx, { + position_x: round2(Math.max(0, newX)), + position_y: round2(Math.max(0, newY)), + width: round2(Math.max(2, newW)), + height: round2(Math.max(1, newH)), + }); + }, + end() { latestRef.current.onDragEnd(); }, }, - end() { latestRef.current.onDragEnd(); }, - }, - }) - .resizable({ - edges: { left: true, right: true, top: true, bottom: true }, - modifiers: [ - interact.modifiers.restrictEdges({ outer: 'parent' }), - interact.modifiers.restrictSize({ min: { width: 30, height: 20 } }), - ], - listeners: { - move(event) { - const { field: f, index: idx, onUpdate: update } = latestRef.current; - const rect = containerRef.current.getBoundingClientRect(); - const newX = f.position_x + (event.deltaRect.left / rect.width) * 100; - const newY = f.position_y + (event.deltaRect.top / rect.height) * 100; - const newW = (event.rect.width / rect.width) * 100; - const newH = (event.rect.height / rect.height) * 100; - update(idx, { - position_x: round2(Math.max(0, newX)), - position_y: round2(Math.max(0, newY)), - width: round2(Math.max(2, newW)), - height: round2(Math.max(1, newH)), - }); + }); + } else { + interactable + .resizable({ enabled: false }) + .draggable({ + inertia: false, + modifiers, + listeners: { + move(event) { + const { field: f, index: idx, onUpdate: update } = latestRef.current; + const rect = containerRef.current.getBoundingClientRect(); + const dx = (event.dx / rect.width) * 100; + const dy = (event.dy / rect.height) * 100; + const newX = Math.max(0, Math.min(100 - f.width, f.position_x + dx)); + const newY = Math.max(0, Math.min(100 - f.height, f.position_y + dy)); + update(idx, { position_x: round2(newX), position_y: round2(newY) }); + }, + end() { latestRef.current.onDragEnd(); }, }, - end() { latestRef.current.onDragEnd(); }, - }, - }); + }); + } return () => interactable.unset(); - }, [gridEnabled]); + }, [gridEnabled, resizeMode]); return (
{ e.stopPropagation(); onSelect(index); }} + onDoubleClick={(e) => { e.stopPropagation(); onSelect(index); setResizeMode(prev => !prev); }} style={{ position: 'absolute', left: `${field.position_x}%`, top: `${field.position_y}%`, width: `${field.width}%`, height: `${field.height}%`, - border: selected ? `2px solid ${color}` : `2px dashed ${color}`, - backgroundColor: selected ? `${color}30` : `${color}15`, - cursor: 'move', borderRadius: '4px', + border: resizeMode ? `2px solid ${color}` : `2px dashed ${color}`, + backgroundColor: resizeMode ? `${color}30` : (selected ? `${color}20` : `${color}10`), + cursor: resizeMode ? 'default' : 'move', + borderRadius: '4px', zIndex: selected ? 20 : 10, touchAction: 'none', - boxShadow: selected ? `0 0 0 1px ${color}, 0 2px 8px rgba(0,0,0,0.15)` : 'none', + boxShadow: resizeMode ? `0 0 0 2px ${color}, 0 2px 12px rgba(0,0,0,0.2)` : (selected ? `0 0 0 1px ${color}, 0 2px 8px rgba(0,0,0,0.15)` : 'none'), }} className="flex items-center justify-center select-none group transition-shadow">
{typeInfo.icon} {field.field_variable ? (field.field_value || `{{${field.field_variable}}}`) : (field.field_label || signerName)}
- {/* 리사이즈 핸들 (선택 시) */} - {selected && <> + {/* 리사이즈 모드 안내 툴팁 */} + {resizeMode && ( +
+ 크기 조절 · ESC 해제 +
+ )} + {/* 리사이즈 핸들 (리사이즈 모드에서만 표시) */} + {resizeMode && <> {['nw','n','ne','w','e','sw','s','se'].map(dir => { const styles = { - nw: { top: -4, left: -4, cursor: 'nw-resize' }, - n: { top: -4, left: '50%', transform: 'translateX(-50%)', cursor: 'n-resize' }, - ne: { top: -4, right: -4, cursor: 'ne-resize' }, - w: { top: '50%', left: -4, transform: 'translateY(-50%)', cursor: 'w-resize' }, - e: { top: '50%', right: -4, transform: 'translateY(-50%)', cursor: 'e-resize' }, - sw: { bottom: -4, left: -4, cursor: 'sw-resize' }, - s: { bottom: -4, left: '50%', transform: 'translateX(-50%)', cursor: 's-resize' }, - se: { bottom: -4, right: -4, cursor: 'se-resize' }, + nw: { top: -5, left: -5, cursor: 'nw-resize' }, + n: { top: -5, left: '50%', transform: 'translateX(-50%)', cursor: 'n-resize' }, + ne: { top: -5, right: -5, cursor: 'ne-resize' }, + w: { top: '50%', left: -5, transform: 'translateY(-50%)', cursor: 'w-resize' }, + e: { top: '50%', right: -5, transform: 'translateY(-50%)', cursor: 'e-resize' }, + sw: { bottom: -5, left: -5, cursor: 'sw-resize' }, + s: { bottom: -5, left: '50%', transform: 'translateX(-50%)', cursor: 's-resize' }, + se: { bottom: -5, right: -5, cursor: 'se-resize' }, }; return (
diff --git a/resources/views/esign/template-fields.blade.php b/resources/views/esign/template-fields.blade.php index 4170e526..36f7138d 100644 --- a/resources/views/esign/template-fields.blade.php +++ b/resources/views/esign/template-fields.blade.php @@ -130,8 +130,10 @@ className={`w-full relative rounded border-2 transition-all ${p === currentPage }; // ─── FieldOverlay (interact.js) ─── +// 기본: 점선 + 드래그=이동 / 더블클릭: 실선 + 핸들=크기조절 / ESC: 모드해제 const FieldOverlay = ({ field, index, selected, signerCount, containerRef, onSelect, onUpdate, onDragEnd, gridEnabled }) => { const fieldRef = useRef(null); + const [resizeMode, setResizeMode] = useState(false); const latestRef = useRef({ field, index, onUpdate, onDragEnd }); latestRef.current = { field, index, onUpdate, onDragEnd }; @@ -140,6 +142,18 @@ className={`w-full relative rounded border-2 transition-all ${p === currentPage const signerName = SIGNER_LABELS[signerIdx] || `서명자 ${field.signer_order}`; const typeInfo = FIELD_TYPES.find(t => t.value === field.field_type) || FIELD_TYPES[0]; + // 선택 해제 시 리사이즈 모드 자동 해제 + useEffect(() => { if (!selected) setResizeMode(false); }, [selected]); + + // ESC 키로 리사이즈 모드 해제 + useEffect(() => { + if (!resizeMode) return; + const h = (e) => { if (e.key === 'Escape') { e.stopPropagation(); setResizeMode(false); } }; + window.addEventListener('keydown', h); + return () => window.removeEventListener('keydown', h); + }, [resizeMode]); + + // interact.js: resizeMode에 따라 드래그 또는 리사이즈만 활성화 useEffect(() => { const el = fieldRef.current; if (!el) return; @@ -154,86 +168,103 @@ className={`w-full relative rounded border-2 transition-all ${p === currentPage })); } - const interactable = interact(el) - .draggable({ - inertia: false, - modifiers, - listeners: { - move(event) { - const { field: f, index: idx, onUpdate: update } = latestRef.current; - const rect = containerRef.current.getBoundingClientRect(); - const dx = (event.dx / rect.width) * 100; - const dy = (event.dy / rect.height) * 100; - const newX = Math.max(0, Math.min(100 - f.width, f.position_x + dx)); - const newY = Math.max(0, Math.min(100 - f.height, f.position_y + dy)); - update(idx, { position_x: round2(newX), position_y: round2(newY) }); + const interactable = interact(el); + + if (resizeMode) { + interactable + .draggable({ enabled: false }) + .resizable({ + edges: { left: true, right: true, top: true, bottom: true }, + modifiers: [ + interact.modifiers.restrictEdges({ outer: 'parent' }), + interact.modifiers.restrictSize({ min: { width: 30, height: 20 } }), + ], + listeners: { + move(event) { + const { field: f, index: idx, onUpdate: update } = latestRef.current; + const rect = containerRef.current.getBoundingClientRect(); + const newX = f.position_x + (event.deltaRect.left / rect.width) * 100; + const newY = f.position_y + (event.deltaRect.top / rect.height) * 100; + const newW = (event.rect.width / rect.width) * 100; + const newH = (event.rect.height / rect.height) * 100; + update(idx, { + position_x: round2(Math.max(0, newX)), + position_y: round2(Math.max(0, newY)), + width: round2(Math.max(2, newW)), + height: round2(Math.max(1, newH)), + }); + }, + end() { latestRef.current.onDragEnd(); }, }, - end() { latestRef.current.onDragEnd(); }, - }, - }) - .resizable({ - edges: { left: true, right: true, top: true, bottom: true }, - modifiers: [ - interact.modifiers.restrictEdges({ outer: 'parent' }), - interact.modifiers.restrictSize({ min: { width: 30, height: 20 } }), - ], - listeners: { - move(event) { - const { field: f, index: idx, onUpdate: update } = latestRef.current; - const rect = containerRef.current.getBoundingClientRect(); - const newX = f.position_x + (event.deltaRect.left / rect.width) * 100; - const newY = f.position_y + (event.deltaRect.top / rect.height) * 100; - const newW = (event.rect.width / rect.width) * 100; - const newH = (event.rect.height / rect.height) * 100; - update(idx, { - position_x: round2(Math.max(0, newX)), - position_y: round2(Math.max(0, newY)), - width: round2(Math.max(2, newW)), - height: round2(Math.max(1, newH)), - }); + }); + } else { + interactable + .resizable({ enabled: false }) + .draggable({ + inertia: false, + modifiers, + listeners: { + move(event) { + const { field: f, index: idx, onUpdate: update } = latestRef.current; + const rect = containerRef.current.getBoundingClientRect(); + const dx = (event.dx / rect.width) * 100; + const dy = (event.dy / rect.height) * 100; + const newX = Math.max(0, Math.min(100 - f.width, f.position_x + dx)); + const newY = Math.max(0, Math.min(100 - f.height, f.position_y + dy)); + update(idx, { position_x: round2(newX), position_y: round2(newY) }); + }, + end() { latestRef.current.onDragEnd(); }, }, - end() { latestRef.current.onDragEnd(); }, - }, - }); + }); + } return () => interactable.unset(); - }, [gridEnabled]); + }, [gridEnabled, resizeMode]); return (
{ e.stopPropagation(); onSelect(index); }} + onDoubleClick={(e) => { e.stopPropagation(); onSelect(index); setResizeMode(prev => !prev); }} style={{ position: 'absolute', left: `${field.position_x}%`, top: `${field.position_y}%`, width: `${field.width}%`, height: `${field.height}%`, - border: selected ? `2px solid ${color}` : `2px dashed ${color}`, - backgroundColor: selected ? `${color}30` : `${color}15`, - cursor: 'move', borderRadius: '4px', + border: resizeMode ? `2px solid ${color}` : `2px dashed ${color}`, + backgroundColor: resizeMode ? `${color}30` : (selected ? `${color}20` : `${color}10`), + cursor: resizeMode ? 'default' : 'move', + borderRadius: '4px', zIndex: selected ? 20 : 10, touchAction: 'none', - boxShadow: selected ? `0 0 0 1px ${color}, 0 2px 8px rgba(0,0,0,0.15)` : 'none', + boxShadow: resizeMode ? `0 0 0 2px ${color}, 0 2px 12px rgba(0,0,0,0.2)` : (selected ? `0 0 0 1px ${color}, 0 2px 8px rgba(0,0,0,0.15)` : 'none'), }} className="flex items-center justify-center select-none group transition-shadow">
{typeInfo.icon} {field.field_variable ? `{{${field.field_variable}}}` : (field.field_label || signerName)}
- {selected && <> + {/* 리사이즈 모드 안내 툴팁 */} + {resizeMode && ( +
+ 크기 조절 · ESC 해제 +
+ )} + {/* 리사이즈 핸들 (리사이즈 모드에서만 표시) */} + {resizeMode && <> {['nw','n','ne','w','e','sw','s','se'].map(dir => { const styles = { - nw: { top: -4, left: -4, cursor: 'nw-resize' }, - n: { top: -4, left: '50%', transform: 'translateX(-50%)', cursor: 'n-resize' }, - ne: { top: -4, right: -4, cursor: 'ne-resize' }, - w: { top: '50%', left: -4, transform: 'translateY(-50%)', cursor: 'w-resize' }, - e: { top: '50%', right: -4, transform: 'translateY(-50%)', cursor: 'e-resize' }, - sw: { bottom: -4, left: -4, cursor: 'sw-resize' }, - s: { bottom: -4, left: '50%', transform: 'translateX(-50%)', cursor: 's-resize' }, - se: { bottom: -4, right: -4, cursor: 'se-resize' }, + nw: { top: -5, left: -5, cursor: 'nw-resize' }, + n: { top: -5, left: '50%', transform: 'translateX(-50%)', cursor: 'n-resize' }, + ne: { top: -5, right: -5, cursor: 'ne-resize' }, + w: { top: '50%', left: -5, transform: 'translateY(-50%)', cursor: 'w-resize' }, + e: { top: '50%', right: -5, transform: 'translateY(-50%)', cursor: 'e-resize' }, + sw: { bottom: -5, left: -5, cursor: 'sw-resize' }, + s: { bottom: -5, left: '50%', transform: 'translateX(-50%)', cursor: 's-resize' }, + se: { bottom: -5, right: -5, cursor: 'se-resize' }, }; return (