뷰어에서 자재 클릭시 보기 구현

This commit is contained in:
dohyeons 2025-10-17 16:23:33 +09:00
parent 184d687f0f
commit 4a4700ea23
7 changed files with 122 additions and 59 deletions

View File

@ -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,

View File

@ -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 {

View File

@ -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;

View File

@ -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칸 너비 보장

View File

@ -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;

View File

@ -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) {

View File

@ -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>
)}