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 (