From c20e393a1a930741147069a082f1af1ddc49ee67 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 24 Dec 2025 10:58:41 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=9D=B8?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=ED=8E=B8=EC=A7=91=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/designer/CanvasComponent.tsx | 91 ++++++++++++++- .../report/designer/ReportDesignerCanvas.tsx | 9 ++ frontend/contexts/ReportDesignerContext.tsx | 110 ++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index 1bd6db73..da440abc 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -193,6 +193,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) { // 복제 시 원본 컴포넌트들의 위치 저장 (상대적 위치 유지용) const originalPositionsRef = useRef>(new Map()); + // 인라인 편집 상태 + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(""); + const textareaRef = useRef(null); + const isSelected = selectedComponentId === component.id; const isMultiSelected = selectedComponentIds.includes(component.id); const isLocked = component.locked === true; @@ -290,15 +295,76 @@ export function CanvasComponent({ component }: CanvasComponentProps) { }); }; - // 더블 클릭 핸들러 (텍스트 컴포넌트만) + // 더블 클릭 핸들러 (텍스트 컴포넌트: 인라인 편집 모드 진입) const handleDoubleClick = (e: React.MouseEvent) => { if (component.type !== "text" && component.type !== "label") return; + if (isLocked) return; // 잠긴 컴포넌트는 편집 불가 + e.stopPropagation(); - fitTextToContent(); + + // 인라인 편집 모드 진입 + setEditValue(component.defaultValue || ""); + setIsEditing(true); + }; + + // 인라인 편집 시작 시 textarea에 포커스 + useEffect(() => { + if (isEditing && textareaRef.current) { + textareaRef.current.focus(); + textareaRef.current.select(); + } + }, [isEditing]); + + // 선택 해제 시 편집 모드 종료를 위한 ref + const editValueRef = useRef(editValue); + const isEditingRef = useRef(isEditing); + editValueRef.current = editValue; + isEditingRef.current = isEditing; + + // 선택 해제 시 편집 모드 종료 (저장 후 종료) + useEffect(() => { + if (!isSelected && !isMultiSelected && isEditingRef.current) { + // 현재 편집 값으로 저장 + if (editValueRef.current !== component.defaultValue) { + updateComponent(component.id, { defaultValue: editValueRef.current }); + } + setIsEditing(false); + } + }, [isSelected, isMultiSelected, component.id, component.defaultValue, updateComponent]); + + // 인라인 편집 저장 + const handleEditSave = () => { + if (!isEditing) return; + + updateComponent(component.id, { + defaultValue: editValue, + }); + setIsEditing(false); + }; + + // 인라인 편집 취소 + const handleEditCancel = () => { + setIsEditing(false); + setEditValue(""); + }; + + // 인라인 편집 키보드 핸들러 + const handleEditKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + handleEditCancel(); + } else if (e.key === "Enter" && !e.shiftKey) { + // Enter: 저장 (Shift+Enter는 줄바꿈) + e.preventDefault(); + handleEditSave(); + } }; // 드래그 시작 const handleMouseDown = (e: React.MouseEvent) => { + // 편집 모드에서는 드래그 비활성화 + if (isEditing) return; + if ((e.target as HTMLElement).classList.contains("resize-handle")) { return; } @@ -636,6 +702,27 @@ export function CanvasComponent({ component }: CanvasComponentProps) { switch (component.type) { case "text": case "label": + // 인라인 편집 모드 + if (isEditing) { + return ( +