텍스트 인라인 편집 기능 추가

This commit is contained in:
dohyeons
2025-12-24 10:58:41 +09:00
parent f300b637d1
commit c20e393a1a
3 changed files with 208 additions and 2 deletions

View File

@@ -193,6 +193,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
// 복제 시 원본 컴포넌트들의 위치 저장 (상대적 위치 유지용)
const originalPositionsRef = useRef<Map<string, { x: number; y: number }>>(new Map());
// 인라인 편집 상태
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(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 (
<textarea
ref={textareaRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={handleEditSave}
onKeyDown={handleEditKeyDown}
className="h-full w-full resize-none border-none bg-transparent p-0 outline-none focus:ring-2 focus:ring-blue-500"
style={{
fontSize: `${component.fontSize}px`,
color: component.fontColor,
fontWeight: component.fontWeight,
textAlign: component.textAlign as "left" | "center" | "right",
lineHeight: "1.5",
}}
/>
);
}
return (
<div
className="h-full w-full"

View File

@@ -214,6 +214,7 @@ export function ReportDesignerCanvas() {
duplicateComponents,
copyStyles,
pasteStyles,
fitSelectedToContent,
undo,
redo,
showRuler,
@@ -646,6 +647,13 @@ export function ReportDesignerCanvas() {
return;
}
// Ctrl+Shift+F (또는 Cmd+Shift+F): 텍스트 크기 자동 맞춤
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "f") {
e.preventDefault();
fitSelectedToContent();
return;
}
// Ctrl+C (또는 Cmd+C): 복사
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "c") {
e.preventDefault();
@@ -699,6 +707,7 @@ export function ReportDesignerCanvas() {
duplicateComponents,
copyStyles,
pasteStyles,
fitSelectedToContent,
undo,
redo,
]);