뷰어에서 자재 클릭시 보기 구현
This commit is contained in:
parent
184d687f0f
commit
4a4700ea23
|
|
@ -61,8 +61,9 @@ export class DashboardService {
|
|||
id, dashboard_id, element_type, element_subtype,
|
||||
position_x, position_y, width, height,
|
||||
title, custom_title, show_header, content, data_source_config, chart_config,
|
||||
list_config, yard_config,
|
||||
display_order, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
`,
|
||||
[
|
||||
elementId,
|
||||
|
|
@ -79,6 +80,8 @@ export class DashboardService {
|
|||
element.content || null,
|
||||
JSON.stringify(element.dataSource || {}),
|
||||
JSON.stringify(element.chartConfig || {}),
|
||||
JSON.stringify(element.listConfig || null),
|
||||
JSON.stringify(element.yardConfig || null),
|
||||
i,
|
||||
now,
|
||||
now,
|
||||
|
|
@ -342,6 +345,16 @@ export class DashboardService {
|
|||
content: row.content,
|
||||
dataSource: JSON.parse(row.data_source_config || "{}"),
|
||||
chartConfig: JSON.parse(row.chart_config || "{}"),
|
||||
listConfig: row.list_config
|
||||
? typeof row.list_config === "string"
|
||||
? JSON.parse(row.list_config)
|
||||
: row.list_config
|
||||
: undefined,
|
||||
yardConfig: row.yard_config
|
||||
? typeof row.yard_config === "string"
|
||||
? JSON.parse(row.yard_config)
|
||||
: row.yard_config
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -465,8 +478,9 @@ export class DashboardService {
|
|||
id, dashboard_id, element_type, element_subtype,
|
||||
position_x, position_y, width, height,
|
||||
title, custom_title, show_header, content, data_source_config, chart_config,
|
||||
list_config, yard_config,
|
||||
display_order, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
`,
|
||||
[
|
||||
elementId,
|
||||
|
|
@ -483,6 +497,8 @@ export class DashboardService {
|
|||
element.content || null,
|
||||
JSON.stringify(element.dataSource || {}),
|
||||
JSON.stringify(element.chartConfig || {}),
|
||||
JSON.stringify(element.listConfig || null),
|
||||
JSON.stringify(element.yardConfig || null),
|
||||
i,
|
||||
now,
|
||||
now,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@ export interface DashboardElement {
|
|||
title?: string;
|
||||
showLegend?: boolean;
|
||||
};
|
||||
listConfig?: {
|
||||
columns?: any[];
|
||||
pagination?: any;
|
||||
viewMode?: string;
|
||||
cardColumns?: number;
|
||||
};
|
||||
yardConfig?: {
|
||||
layoutId: number;
|
||||
layoutName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ export function CanvasElement({
|
|||
if (isResizing && tempPosition && tempSize) {
|
||||
// tempPosition과 tempSize는 이미 리사이즈 중에 마그네틱 스냅 적용됨
|
||||
// 다시 스냅하지 않고 그대로 사용!
|
||||
let finalX = tempPosition.x;
|
||||
const finalX = tempPosition.x;
|
||||
const finalY = tempPosition.y;
|
||||
let finalWidth = tempSize.width;
|
||||
const finalHeight = tempSize.height;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
// 충돌 방지 기능이 포함된 업데이트 핸들러
|
||||
const handleUpdateWithCollisionDetection = useCallback(
|
||||
(id: string, updates: Partial<DashboardElement>) => {
|
||||
// position이나 size가 아닌 다른 속성 업데이트는 충돌 감지 없이 바로 처리
|
||||
if (!updates.position && !updates.size) {
|
||||
onUpdateElement(id, updates);
|
||||
return;
|
||||
}
|
||||
|
||||
// 업데이트할 요소 찾기
|
||||
const elementIndex = elements.findIndex((el) => el.id === id);
|
||||
if (elementIndex === -1) {
|
||||
|
|
@ -58,9 +64,38 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
return;
|
||||
}
|
||||
|
||||
// position이나 size와 다른 속성이 함께 있으면 분리해서 처리
|
||||
const positionSizeUpdates: any = {};
|
||||
const otherUpdates: any = {};
|
||||
|
||||
Object.keys(updates).forEach((key) => {
|
||||
if (key === "position" || key === "size") {
|
||||
positionSizeUpdates[key] = (updates as any)[key];
|
||||
} else {
|
||||
otherUpdates[key] = (updates as any)[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 다른 속성들은 먼저 바로 업데이트
|
||||
if (Object.keys(otherUpdates).length > 0) {
|
||||
onUpdateElement(id, otherUpdates);
|
||||
}
|
||||
|
||||
// position/size가 없으면 여기서 종료
|
||||
if (Object.keys(positionSizeUpdates).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 임시로 업데이트된 요소 배열 생성
|
||||
const updatedElements = elements.map((el) =>
|
||||
el.id === id ? { ...el, ...updates, position: updates.position || el.position, size: updates.size || el.size } : el
|
||||
el.id === id
|
||||
? {
|
||||
...el,
|
||||
...positionSizeUpdates,
|
||||
position: positionSizeUpdates.position || el.position,
|
||||
size: positionSizeUpdates.size || el.size,
|
||||
}
|
||||
: el,
|
||||
);
|
||||
|
||||
// 서브 그리드 크기 계산 (cellSize / 3)
|
||||
|
|
@ -85,7 +120,7 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
}
|
||||
});
|
||||
},
|
||||
[elements, onUpdateElement, cellSize, canvasWidth]
|
||||
[elements, onUpdateElement, cellSize, canvasWidth],
|
||||
);
|
||||
|
||||
// 드래그 오버 처리
|
||||
|
|
@ -124,20 +159,17 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
const subGridSize = Math.floor(cellSize / 3);
|
||||
const gridSize = cellSize + 5; // GAP 포함한 실제 그리드 크기
|
||||
const magneticThreshold = 15;
|
||||
|
||||
|
||||
// X 좌표 스냅
|
||||
const nearestGridX = Math.round(rawX / gridSize) * gridSize;
|
||||
const distToGridX = Math.abs(rawX - nearestGridX);
|
||||
let snappedX = distToGridX <= magneticThreshold
|
||||
? nearestGridX
|
||||
: Math.round(rawX / subGridSize) * subGridSize;
|
||||
|
||||
let snappedX = distToGridX <= magneticThreshold ? nearestGridX : Math.round(rawX / subGridSize) * subGridSize;
|
||||
|
||||
// Y 좌표 스냅
|
||||
const nearestGridY = Math.round(rawY / gridSize) * gridSize;
|
||||
const distToGridY = Math.abs(rawY - nearestGridY);
|
||||
const snappedY = distToGridY <= magneticThreshold
|
||||
? nearestGridY
|
||||
: Math.round(rawY / subGridSize) * subGridSize;
|
||||
const snappedY =
|
||||
distToGridY <= magneticThreshold ? nearestGridY : Math.round(rawY / subGridSize) * subGridSize;
|
||||
|
||||
// X 좌표가 캔버스 너비를 벗어나지 않도록 제한
|
||||
const maxX = canvasWidth - cellSize * 2; // 최소 2칸 너비 보장
|
||||
|
|
|
|||
|
|
@ -355,6 +355,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
dataSource: el.dataSource,
|
||||
chartConfig: el.chartConfig,
|
||||
listConfig: el.listConfig,
|
||||
yardConfig: el.yardConfig,
|
||||
}));
|
||||
|
||||
let savedDashboard;
|
||||
|
|
|
|||
|
|
@ -130,7 +130,14 @@ function MaterialBox({
|
|||
|
||||
const handlePointerDown = (e: any) => {
|
||||
e.stopPropagation();
|
||||
if (isSelected && onDrag && meshRef.current) {
|
||||
|
||||
// 뷰어 모드(onDrag 없음)에서는 클릭만 처리
|
||||
if (!onDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 편집 모드에서 선택되었고 드래그 가능한 경우
|
||||
if (isSelected && meshRef.current) {
|
||||
// 드래그 시작 시점의 자재 위치 저장 (숫자로 변환)
|
||||
dragStartPos.current = {
|
||||
x: Number(placement.position_x),
|
||||
|
|
@ -161,11 +168,17 @@ function MaterialBox({
|
|||
e.stopPropagation();
|
||||
e.nativeEvent?.stopPropagation();
|
||||
e.nativeEvent?.stopImmediatePropagation();
|
||||
console.log("3D Box clicked:", placement.material_name);
|
||||
onClick();
|
||||
}}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerOver={() => {
|
||||
gl.domElement.style.cursor = isSelected ? "grab" : "pointer";
|
||||
// 뷰어 모드(onDrag 없음)에서는 기본 커서, 편집 모드에서는 grab 커서
|
||||
if (onDrag) {
|
||||
gl.domElement.style.cursor = isSelected ? "grab" : "pointer";
|
||||
} else {
|
||||
gl.domElement.style.cursor = "pointer";
|
||||
}
|
||||
}}
|
||||
onPointerOut={() => {
|
||||
if (!isDragging) {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,17 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 선택 변경 로그
|
||||
const handlePlacementClick = (placement: YardPlacement | null) => {
|
||||
console.log("Yard3DViewer - Placement clicked:", placement?.material_name);
|
||||
setSelectedPlacement(placement);
|
||||
};
|
||||
|
||||
// 선택 상태 변경 감지
|
||||
useEffect(() => {
|
||||
console.log("selectedPlacement changed:", selectedPlacement?.material_name);
|
||||
}, [selectedPlacement]);
|
||||
|
||||
// 배치 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadPlacements = async () => {
|
||||
|
|
@ -91,62 +102,42 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
<div className="relative h-full w-full">
|
||||
{/* 3D 캔버스 */}
|
||||
<div className="flex-1">
|
||||
<Yard3DCanvas
|
||||
placements={placements}
|
||||
selectedPlacementId={selectedPlacement?.id || null}
|
||||
onPlacementClick={setSelectedPlacement}
|
||||
/>
|
||||
</div>
|
||||
<Yard3DCanvas
|
||||
placements={placements}
|
||||
selectedPlacementId={selectedPlacement?.id || null}
|
||||
onPlacementClick={handlePlacementClick}
|
||||
/>
|
||||
|
||||
{/* 선택된 자재 정보 패널 (우측) */}
|
||||
{/* 선택된 자재 정보 패널 (오버레이) */}
|
||||
{selectedPlacement && (
|
||||
<div className="w-80 border-l bg-white p-4">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700">자재 정보</h3>
|
||||
<div className="absolute top-4 right-4 z-50 w-64 rounded-lg border border-gray-300 bg-white p-4 shadow-xl">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-800">자재 정보</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log("Close button clicked");
|
||||
setSelectedPlacement(null);
|
||||
}}
|
||||
className="rounded-full p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">자재 코드</label>
|
||||
<div className="mt-1 text-sm font-medium">{selectedPlacement.material_code}</div>
|
||||
<label className="text-xs font-medium text-gray-500">자재명</label>
|
||||
<div className="mt-1 text-sm font-semibold text-gray-900">{selectedPlacement.material_name}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">자재명</label>
|
||||
<div className="mt-1 text-sm font-medium">{selectedPlacement.material_name}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">수량</label>
|
||||
<div className="mt-1 text-sm">
|
||||
<label className="text-xs font-medium text-gray-500">수량</label>
|
||||
<div className="mt-1 text-sm font-semibold text-gray-900">
|
||||
{selectedPlacement.quantity} {selectedPlacement.unit}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">위치 (X, Y, Z)</label>
|
||||
<div className="mt-1 text-sm">
|
||||
({selectedPlacement.position_x.toFixed(1)}, {selectedPlacement.position_y.toFixed(1)},{" "}
|
||||
{selectedPlacement.position_z.toFixed(1)})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">크기 (W × H × D)</label>
|
||||
<div className="mt-1 text-sm">
|
||||
{selectedPlacement.size_x} × {selectedPlacement.size_y} × {selectedPlacement.size_z}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedPlacement.memo && (
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">메모</label>
|
||||
<div className="mt-1 text-sm text-gray-700">{selectedPlacement.memo}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue