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:
DDD1542
2026-02-24 10:49:23 +09:00
parent 5ec689101e
commit 27853a9447
13 changed files with 2258 additions and 522 deletions

View File

@@ -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>
{/* 팝업 화면 렌더링 */}