From aa283d11dac7406aaac9e9c3021fa11fca3305ee Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 24 Dec 2025 09:48:37 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=8D=94=EB=B8=94=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=8B=9C=20=EC=BB=A8=ED=85=90=EC=B8=A0=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/designer/CanvasComponent.tsx | 130 ++++++++++++++---- 1 file changed, 100 insertions(+), 30 deletions(-) diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index ccc3aa8a..8f7c1db2 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -190,6 +190,105 @@ export function CanvasComponent({ component }: CanvasComponentProps) { const isLocked = component.locked === true; const isGrouped = !!component.groupId; + // 표시할 값 결정 + const getDisplayValue = (): string => { + // 쿼리와 필드가 연결되어 있으면 실제 데이터 조회 + if (component.queryId && component.fieldName) { + const queryResult = getQueryResult(component.queryId); + + // 실행 결과가 있으면 첫 번째 행의 해당 필드 값 표시 + if (queryResult && queryResult.rows.length > 0) { + const firstRow = queryResult.rows[0]; + const value = firstRow[component.fieldName]; + + // 값이 있으면 문자열로 변환하여 반환 + if (value !== null && value !== undefined) { + return String(value); + } + } + + // 실행 결과가 없거나 값이 없으면 필드명 표시 + return `{${component.fieldName}}`; + } + + // 기본값이 있으면 기본값 표시 + if (component.defaultValue) { + return component.defaultValue; + } + + // 둘 다 없으면 타입에 따라 기본 텍스트 + return component.type === "text" ? "텍스트 입력" : "레이블 텍스트"; + }; + + // 텍스트 컴포넌트: 더블 클릭 시 컨텐츠에 맞게 크기 조절 + const fitTextToContent = () => { + if (isLocked) return; + if (component.type !== "text" && component.type !== "label") return; + + const minWidth = 50; + const minHeight = 30; + + // 여백을 px로 변환 + const marginRightPx = margins.right * MM_TO_PX; + const marginBottomPx = margins.bottom * MM_TO_PX; + const canvasWidthPx = canvasWidth * MM_TO_PX; + const canvasHeightPx = canvasHeight * MM_TO_PX; + + // 최대 크기 (여백 고려) + const maxWidth = canvasWidthPx - marginRightPx - component.x; + const maxHeight = canvasHeightPx - marginBottomPx - component.y; + + const displayValue = getDisplayValue(); + const fontSize = component.fontSize || 14; + + // 줄바꿈으로 분리하여 각 줄의 너비 측정 + const lines = displayValue.split("\n"); + let maxLineWidth = 0; + + lines.forEach((line) => { + const measureEl = document.createElement("span"); + measureEl.style.position = "absolute"; + measureEl.style.visibility = "hidden"; + measureEl.style.whiteSpace = "nowrap"; + measureEl.style.fontSize = `${fontSize}px`; + measureEl.style.fontWeight = component.fontWeight || "normal"; + measureEl.style.fontFamily = "system-ui, -apple-system, sans-serif"; + measureEl.textContent = line || " "; // 빈 줄은 공백으로 + document.body.appendChild(measureEl); + + const lineWidth = measureEl.getBoundingClientRect().width; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + document.body.removeChild(measureEl); + }); + + // 컴포넌트 padding (p-2 = 8px * 2) + 여유분 + const horizontalPadding = 24; + const verticalPadding = 20; + + // 줄 높이 계산 (font-size * line-height 약 1.5) + const lineHeight = fontSize * 1.5; + const totalHeight = lines.length * lineHeight; + + const finalWidth = Math.min(maxLineWidth + horizontalPadding, maxWidth); + const finalHeight = Math.min(totalHeight + verticalPadding, maxHeight); + + const newWidth = Math.max(minWidth, finalWidth); + const newHeight = Math.max(minHeight, finalHeight); + + // 크기 업데이트 + updateComponent(component.id, { + width: snapValueToGrid(newWidth), + height: snapValueToGrid(newHeight), + }); + }; + + // 더블 클릭 핸들러 (텍스트 컴포넌트만) + const handleDoubleClick = (e: React.MouseEvent) => { + if (component.type !== "text" && component.type !== "label") return; + e.stopPropagation(); + fitTextToContent(); + }; + // 드래그 시작 const handleMouseDown = (e: React.MouseEvent) => { if ((e.target as HTMLElement).classList.contains("resize-handle")) { @@ -405,36 +504,6 @@ export function CanvasComponent({ component }: CanvasComponentProps) { canvasHeight, ]); - // 표시할 값 결정 - const getDisplayValue = (): string => { - // 쿼리와 필드가 연결되어 있으면 실제 데이터 조회 - if (component.queryId && component.fieldName) { - const queryResult = getQueryResult(component.queryId); - - // 실행 결과가 있으면 첫 번째 행의 해당 필드 값 표시 - if (queryResult && queryResult.rows.length > 0) { - const firstRow = queryResult.rows[0]; - const value = firstRow[component.fieldName]; - - // 값이 있으면 문자열로 변환하여 반환 - if (value !== null && value !== undefined) { - return String(value); - } - } - - // 실행 결과가 없거나 값이 없으면 필드명 표시 - return `{${component.fieldName}}`; - } - - // 기본값이 있으면 기본값 표시 - if (component.defaultValue) { - return component.defaultValue; - } - - // 둘 다 없으면 타입에 따라 기본 텍스트 - return component.type === "text" ? "텍스트 입력" : "레이블 텍스트"; - }; - // 컴포넌트 타입별 렌더링 const renderContent = () => { const displayValue = getDisplayValue(); @@ -1182,6 +1251,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) { : "1px solid #e5e7eb", }} onMouseDown={handleMouseDown} + onDoubleClick={handleDoubleClick} > {renderContent()}