feat: Add BOM tree view and BOM item editor components
- Introduced new components for BOM tree view and BOM item editor, enhancing the data management capabilities within the application. - Updated the ComponentsPanel to include these new components with appropriate descriptions and default sizes. - Integrated the BOM item editor into the V2PropertiesPanel for seamless editing of BOM items. - Adjusted the SplitLineComponent to improve the handling of canvas split positions, ensuring better user experience during component interactions.
This commit is contained in:
@@ -1089,98 +1089,106 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||
const labelOffset = hasVisibleLabel ? (labelFontSize + labelMarginBottom + 2) : 0;
|
||||
|
||||
// 캔버스 분할선에 따른 X 위치 조정 (너비는 변경하지 않음 - 내부 컴포넌트 깨짐 방지)
|
||||
const calculateCanvasSplitX = (): number => {
|
||||
const calculateCanvasSplitX = (): { x: number; w: number } => {
|
||||
const compType = (component as any).componentType || "";
|
||||
const isSplitLine = type === "component" && compType === "v2-split-line";
|
||||
const origX = position?.x || 0;
|
||||
const defaultW = size?.width || 200;
|
||||
|
||||
if (isSplitLine) return origX;
|
||||
|
||||
// DEBUG: 스플릿 스토어 상태 확인 (첫 컴포넌트만)
|
||||
if (canvasSplit.active && origX > 0 && origX < 50) {
|
||||
console.log("[SplitDebug]", {
|
||||
compId: component.id,
|
||||
compType,
|
||||
type,
|
||||
active: canvasSplit.active,
|
||||
scopeId: canvasSplit.scopeId,
|
||||
myScopeId: myScopeIdRef.current,
|
||||
canvasWidth: canvasSplit.canvasWidth,
|
||||
initialX: canvasSplit.initialDividerX,
|
||||
currentX: canvasSplit.currentDividerX,
|
||||
origX,
|
||||
});
|
||||
}
|
||||
if (isSplitLine) return { x: origX, w: defaultW };
|
||||
|
||||
if (!canvasSplit.active || canvasSplit.canvasWidth <= 0 || !canvasSplit.scopeId) {
|
||||
return origX;
|
||||
return { x: origX, w: defaultW };
|
||||
}
|
||||
|
||||
if (myScopeIdRef.current === null) {
|
||||
const el = document.getElementById(`interactive-${component.id}`);
|
||||
const container = el?.closest("[data-screen-runtime]");
|
||||
myScopeIdRef.current = container?.getAttribute("data-split-scope") || "__none__";
|
||||
console.log("[SplitDebug] scope resolved:", { compId: component.id, elFound: !!el, containerFound: !!container, myScopeId: myScopeIdRef.current, storeScopeId: canvasSplit.scopeId });
|
||||
}
|
||||
if (myScopeIdRef.current !== canvasSplit.scopeId) {
|
||||
return origX;
|
||||
return { x: origX, w: defaultW };
|
||||
}
|
||||
|
||||
const { initialDividerX, currentDividerX, canvasWidth } = canvasSplit;
|
||||
const delta = currentDividerX - initialDividerX;
|
||||
if (Math.abs(delta) < 1) return origX;
|
||||
if (Math.abs(delta) < 1) return { x: origX, w: defaultW };
|
||||
|
||||
const origW = size?.width || 200;
|
||||
const origW = defaultW;
|
||||
if (canvasSplitSideRef.current === null) {
|
||||
const componentCenterX = origX + (origW / 2);
|
||||
canvasSplitSideRef.current = componentCenterX < initialDividerX ? "left" : "right";
|
||||
}
|
||||
|
||||
let newX = origX;
|
||||
// 영역별 비례 스케일링: 스플릿선이 벽 역할 → 절대 넘어가지 않음
|
||||
let newX: number;
|
||||
let newW: number;
|
||||
const GAP = 4; // 스플릿선과의 최소 간격
|
||||
|
||||
if (canvasSplitSideRef.current === "left") {
|
||||
if (initialDividerX > 0) {
|
||||
newX = origX * (currentDividerX / initialDividerX);
|
||||
// 왼쪽 영역: [0, currentDividerX - GAP]
|
||||
const initialZoneWidth = initialDividerX;
|
||||
const currentZoneWidth = Math.max(20, currentDividerX - GAP);
|
||||
const scale = initialZoneWidth > 0 ? currentZoneWidth / initialZoneWidth : 1;
|
||||
newX = origX * scale;
|
||||
newW = origW * scale;
|
||||
// 안전 클램핑: 왼쪽 영역을 절대 넘지 않음
|
||||
if (newX + newW > currentDividerX - GAP) {
|
||||
newW = currentDividerX - GAP - newX;
|
||||
}
|
||||
} else {
|
||||
// 오른쪽 영역: [currentDividerX + GAP, canvasWidth]
|
||||
const initialRightWidth = canvasWidth - initialDividerX;
|
||||
const currentRightWidth = canvasWidth - currentDividerX;
|
||||
if (initialRightWidth > 0) {
|
||||
const posRatio = (origX - initialDividerX) / initialRightWidth;
|
||||
newX = currentDividerX + posRatio * currentRightWidth;
|
||||
}
|
||||
const currentRightWidth = Math.max(20, canvasWidth - currentDividerX - GAP);
|
||||
const scale = initialRightWidth > 0 ? currentRightWidth / initialRightWidth : 1;
|
||||
const rightOffset = origX - initialDividerX;
|
||||
newX = currentDividerX + GAP + rightOffset * scale;
|
||||
newW = origW * scale;
|
||||
// 안전 클램핑: 오른쪽 영역을 절대 넘지 않음
|
||||
if (newX < currentDividerX + GAP) newX = currentDividerX + GAP;
|
||||
if (newX + newW > canvasWidth) newW = canvasWidth - newX;
|
||||
}
|
||||
|
||||
// 캔버스 범위 내로 클램핑
|
||||
return Math.max(0, Math.min(newX, canvasWidth - 10));
|
||||
newX = Math.max(0, newX);
|
||||
newW = Math.max(20, newW);
|
||||
|
||||
return { x: newX, w: newW };
|
||||
};
|
||||
|
||||
const adjustedX = calculateCanvasSplitX();
|
||||
const splitResult = calculateCanvasSplitX();
|
||||
const adjustedX = splitResult.x;
|
||||
const adjustedW = splitResult.w;
|
||||
const origW = size?.width || 200;
|
||||
const isSplitActive = canvasSplit.active && canvasSplit.scopeId && myScopeIdRef.current === canvasSplit.scopeId;
|
||||
|
||||
// styleWithoutSize에서 left/top 제거 (캔버스 분할 조정값 덮어쓰기 방지)
|
||||
const { left: _styleLeft, top: _styleTop, ...safeStyleWithoutSize } = styleWithoutSize as any;
|
||||
|
||||
const componentStyle = {
|
||||
position: "absolute" as const,
|
||||
...safeStyleWithoutSize,
|
||||
// left/top은 반드시 마지막에 (styleWithoutSize가 덮어쓰지 못하게)
|
||||
left: adjustedX,
|
||||
top: position?.y || 0,
|
||||
zIndex: position?.z || 1,
|
||||
...styleWithoutSize,
|
||||
width: size?.width || 200,
|
||||
width: isSplitActive ? adjustedW : (size?.width || 200),
|
||||
height: isTableSearchWidget ? "auto" : size?.height || 10,
|
||||
minHeight: isTableSearchWidget ? "48px" : undefined,
|
||||
overflow: labelOffset > 0 ? "visible" : undefined,
|
||||
// GPU 가속: 드래그 중 will-change 활성화, 끝나면 해제
|
||||
willChange: canvasSplit.isDragging && isSplitActive ? "left" as const : undefined,
|
||||
overflow: (isSplitActive && adjustedW < origW) ? "hidden" : (labelOffset > 0 ? "visible" : undefined),
|
||||
willChange: canvasSplit.isDragging && isSplitActive ? "left, width" as const : undefined,
|
||||
transition: isSplitActive
|
||||
? (canvasSplit.isDragging ? "none" : "left 0.15s ease-out")
|
||||
? (canvasSplit.isDragging ? "none" : "left 0.15s ease-out, width 0.15s ease-out")
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id={`interactive-${component.id}`} className="absolute" style={componentStyle}>
|
||||
{/* 위젯 렌더링 (라벨은 V2Input 내부에서 absolute로 표시됨) */}
|
||||
{renderInteractiveWidget(component)}
|
||||
{renderInteractiveWidget(
|
||||
isSplitActive && adjustedW !== origW
|
||||
? { ...component, size: { ...(component as any).size, width: adjustedW } }
|
||||
: component
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 팝업 화면 렌더링 */}
|
||||
|
||||
Reference in New Issue
Block a user