도구:
{[
{ type: "area" as ToolType, label: "영역", icon: Grid3x3, color: "text-blue-500" },
{ type: "location-bed" as ToolType, label: "베드", icon: Package, color: "text-emerald-500" },
{ type: "location-stp" as ToolType, label: "정차", icon: Move, color: "text-orange-500" },
// { type: "crane-gantry" as ToolType, label: "겐트리", icon: Combine, color: "text-green-500" },
{ type: "crane-mobile" as ToolType, label: "크레인", icon: Truck, color: "text-yellow-500" },
{ type: "rack" as ToolType, label: "랙", icon: Box, color: "text-purple-500" },
].map((tool) => {
const Icon = tool.icon;
return (
handleToolDragStart(tool.type)}
className="bg-background hover:bg-accent flex cursor-grab items-center gap-1 rounded-md border px-3 py-2 transition-colors active:cursor-grabbing"
title={`${tool.label} 드래그하여 배치`}
>
{tool.label}
);
})}
{/* 좌측: 외부 DB 선택 + 객체 목록 */}
{/* 스크롤 영역 */}
{/* 외부 DB 선택 */}
{/* 테이블 매핑 선택 */}
{selectedDbConnection && (
{loadingTables ? (
) : (
<>
{/* 창고 컬럼 매핑 */}
{selectedTables.warehouse && tableColumns.warehouse && (
)}
>
)}
)}
{/* 창고 선택 */}
{selectedDbConnection && selectedTables.warehouse && (
{loadingWarehouses ? (
) : (
)}
)}
{/* Area 목록 */}
{selectedDbConnection && selectedWarehouse && (
사용 가능한 Area
{loadingAreas && }
{availableAreas.length === 0 ? (
Area가 없습니다
) : (
{availableAreas.map((area) => (
{
// Area 정보를 임시 저장
setDraggedTool("area");
setDraggedAreaData(area);
}}
onDragEnd={() => {
setDraggedAreaData(null);
}}
className="bg-background hover:bg-accent cursor-grab rounded-lg border p-3 transition-all active:cursor-grabbing"
>
{area.AREANAME}
{area.AREAKEY}
))}
)}
)}
{/* Location 목록 (Area 클릭 시 표시) */}
{availableLocations.length > 0 && (
사용 가능한 Location
{loadingLocations && }
{availableLocations.map((location) => {
// Location 타입에 따라 ObjectType 결정
let locationType: ToolType = "location-bed";
if (location.LOCTYPE === "STP") {
locationType = "location-stp";
} else if (location.LOCTYPE === "TMP") {
locationType = "location-temp";
} else if (location.LOCTYPE === "DES") {
locationType = "location-dest";
}
return (
{
// Location 정보를 임시 저장
setDraggedTool(locationType);
setDraggedLocationData(location);
}}
onDragEnd={() => {
setDraggedLocationData(null);
}}
className="bg-background hover:bg-accent cursor-grab rounded-lg border p-3 transition-all active:cursor-grabbing"
>
{location.LOCANAME || location.LOCAKEY}
{location.LOCAKEY}
{location.LOCTYPE}
);
})}
)}
{/* 배치된 객체 목록 */}
배치된 객체 ({placedObjects.length})
{placedObjects.length === 0 ? (
상단 도구를 드래그하여 배치하세요
) : (
{placedObjects.map((obj) => (
handleObjectClick(obj.id)}
className={`cursor-pointer rounded-lg border p-3 transition-all ${
selectedObject?.id === obj.id ? "border-primary bg-primary/10" : "hover:border-primary/50"
}`}
>
위치: ({obj.position.x.toFixed(1)}, {obj.position.z.toFixed(1)})
{obj.areaKey &&
Area: {obj.areaKey}
}
))}
)}
{/* 중앙: 3D 캔버스 */}
e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
const rawX = ((e.clientX - rect.left) / rect.width - 0.5) * 100;
const rawZ = ((e.clientY - rect.top) / rect.height - 0.5) * 100;
// 그리드 크기 (5 단위)
const gridSize = 5;
// 그리드에 스냅
// Area(20x20)는 그리드 교차점에, 다른 객체(5x5)는 타일 중앙에
let snappedX = Math.round(rawX / gridSize) * gridSize;
let snappedZ = Math.round(rawZ / gridSize) * gridSize;
// 5x5 객체는 타일 중앙으로 오프셋 (Area는 제외)
if (draggedTool !== "area") {
snappedX += gridSize / 2;
snappedZ += gridSize / 2;
}
handleCanvasDrop(snappedX, snappedZ);
}}
>
{isLoading ? (
) : (
handleObjectClick(placement?.id || null)}
onPlacementDrag={(id, position) => handleObjectMove(id, position.x, position.z, position.y)}
focusOnPlacementId={null}
onCollisionDetected={() => {}}
/>
)}
{/* 우측: 객체 속성 편집 or 자재 목록 */}
{showMaterialPanel && selectedObject ? (
/* 자재 목록 패널 */
자재 목록
{selectedObject.name} ({selectedObject.locaKey})
{loadingMaterials ? (
) : materials.length === 0 ? (
자재가 없습니다
) : (
{materials.map((material, index) => (
{material.STKKEY}
층: {material.LOLAYER} | Area: {material.AREAKEY}
{material.STKWIDT && (
폭: {material.STKWIDT}
)}
{material.STKLENG && (
길이: {material.STKLENG}
)}
{material.STKHEIG && (
높이: {material.STKHEIG}
)}
{material.STKWEIG && (
무게: {material.STKWEIG}
)}
{material.STKRMKS && (
{material.STKRMKS}
)}
))}
)}
) : selectedObject ? (
객체 속성
{/* 이름 */}
handleObjectUpdate({ name: e.target.value })}
className="mt-1.5 h-9 text-sm"
/>
{/* 위치 */}
{/* 크기 */}
{/* 색상 */}
handleObjectUpdate({ color: e.target.value })}
className="mt-1.5 h-9"
/>
{/* 삭제 버튼 */}
) : (
)}