대시보드 설정 저장 및 디지털 트윈 UX 개선
This commit is contained in:
parent
8727ef02f3
commit
1e1bc0b2c6
|
|
@ -25,6 +25,7 @@ import {
|
|||
import type { MaterialData } from "@/types/digitalTwin";
|
||||
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
||||
import HierarchyConfigPanel, { HierarchyConfig } from "./HierarchyConfigPanel";
|
||||
import { OBJECT_COLORS, DEFAULT_COLOR } from "./constants";
|
||||
import { validateSpatialContainment, updateChildrenPositions, getAllDescendants } from "./spatialContainment";
|
||||
|
||||
// 백엔드 DB 객체 타입 (snake_case)
|
||||
|
|
@ -702,7 +703,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
name: objectName,
|
||||
position: { x, y: yPosition, z },
|
||||
size: defaults.size || { x: 5, y: 5, z: 5 },
|
||||
color: defaults.color || "#9ca3af",
|
||||
color: OBJECT_COLORS[draggedTool] || DEFAULT_COLOR, // 타입별 기본 색상
|
||||
areaKey,
|
||||
locaKey,
|
||||
locType,
|
||||
|
|
@ -1169,8 +1170,8 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
<span className="text-muted-foreground text-sm font-medium">도구:</span>
|
||||
{[
|
||||
{ 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: "location-bed" as ToolType, label: "베드", icon: Package, color: "text-blue-600" },
|
||||
{ type: "location-stp" as ToolType, label: "정차", icon: Move, color: "text-gray-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" },
|
||||
|
|
@ -1221,54 +1222,6 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 창고 테이블 및 컬럼 매핑 */}
|
||||
{selectedDbConnection && (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">창고 선택</Label>
|
||||
|
||||
{/* 이 레이아웃의 창고 선택 */}
|
||||
{hierarchyConfig?.warehouse?.tableName && hierarchyConfig?.warehouse?.keyColumn && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground mb-1 block text-xs">이 레이아웃의 창고</Label>
|
||||
{loadingWarehouses ? (
|
||||
<div className="flex h-9 items-center justify-center rounded-md border">
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={selectedWarehouse || ""}
|
||||
onValueChange={(value) => {
|
||||
setSelectedWarehouse(value);
|
||||
// hierarchyConfig 업데이트 (없으면 새로 생성)
|
||||
setHierarchyConfig((prev) => ({
|
||||
warehouseKey: value,
|
||||
levels: prev?.levels || [],
|
||||
material: prev?.material,
|
||||
}));
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-xs">
|
||||
<SelectValue placeholder="창고 선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{warehouses.map((wh: any) => {
|
||||
const keyCol = hierarchyConfig.warehouse!.keyColumn;
|
||||
const nameCol = hierarchyConfig.warehouse!.nameColumn;
|
||||
return (
|
||||
<SelectItem key={wh[keyCol]} value={wh[keyCol]} className="text-xs">
|
||||
{wh[nameCol] || wh[keyCol]}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 계층 설정 패널 (신규) */}
|
||||
{selectedDbConnection && (
|
||||
<HierarchyConfigPanel
|
||||
|
|
@ -1327,6 +1280,53 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* 창고 선택 (HierarchyConfigPanel 아래로 이동) */}
|
||||
{selectedDbConnection && hierarchyConfig?.warehouse?.tableName && hierarchyConfig?.warehouse?.keyColumn && (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">창고 선택</Label>
|
||||
|
||||
<div>
|
||||
<Label className="text-muted-foreground mb-1 block text-xs">배치할 창고를 선택하세요</Label>
|
||||
{loadingWarehouses ? (
|
||||
<div className="flex h-9 items-center justify-center rounded-md border">
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={selectedWarehouse || ""}
|
||||
onValueChange={(value) => {
|
||||
setSelectedWarehouse(value);
|
||||
// hierarchyConfig 업데이트
|
||||
setHierarchyConfig((prev) => ({
|
||||
...prev,
|
||||
warehouseKey: value,
|
||||
levels: prev?.levels || [],
|
||||
material: prev?.material,
|
||||
warehouse: prev?.warehouse,
|
||||
}));
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-xs">
|
||||
<SelectValue placeholder="창고 선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{warehouses.map((wh: any) => {
|
||||
const keyCol = hierarchyConfig.warehouse!.keyColumn;
|
||||
const nameCol = hierarchyConfig.warehouse!.nameColumn;
|
||||
return (
|
||||
<SelectItem key={wh[keyCol]} value={wh[keyCol]} className="text-xs">
|
||||
{wh[nameCol] || wh[keyCol]}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Area 목록 */}
|
||||
{selectedDbConnection && selectedWarehouse && (
|
||||
<div className="space-y-3">
|
||||
|
|
@ -1605,7 +1605,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
</Label>
|
||||
<Input
|
||||
id="object-name"
|
||||
value={selectedObject.name}
|
||||
value={selectedObject.name || ""}
|
||||
onChange={(e) => handleObjectUpdate({ name: e.target.value })}
|
||||
className="mt-1.5 h-9 text-sm"
|
||||
/>
|
||||
|
|
@ -1622,7 +1622,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
<Input
|
||||
id="pos-x"
|
||||
type="number"
|
||||
value={selectedObject.position.x.toFixed(1)}
|
||||
value={(selectedObject.position?.x || 0).toFixed(1)}
|
||||
onChange={(e) =>
|
||||
handleObjectUpdate({
|
||||
position: {
|
||||
|
|
@ -1641,7 +1641,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
<Input
|
||||
id="pos-z"
|
||||
type="number"
|
||||
value={selectedObject.position.z.toFixed(1)}
|
||||
value={(selectedObject.position?.z || 0).toFixed(1)}
|
||||
onChange={(e) =>
|
||||
handleObjectUpdate({
|
||||
position: {
|
||||
|
|
@ -1669,7 +1669,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
type="number"
|
||||
step="5"
|
||||
min="5"
|
||||
value={selectedObject.size.x}
|
||||
value={selectedObject.size?.x || 5}
|
||||
onChange={(e) =>
|
||||
handleObjectUpdate({
|
||||
size: {
|
||||
|
|
@ -1688,7 +1688,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
<Input
|
||||
id="size-y"
|
||||
type="number"
|
||||
value={selectedObject.size.y}
|
||||
value={selectedObject.size?.y || 5}
|
||||
onChange={(e) =>
|
||||
handleObjectUpdate({
|
||||
size: {
|
||||
|
|
@ -1709,7 +1709,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
type="number"
|
||||
step="5"
|
||||
min="5"
|
||||
value={selectedObject.size.z}
|
||||
value={selectedObject.size?.z || 5}
|
||||
onChange={(e) =>
|
||||
handleObjectUpdate({
|
||||
size: {
|
||||
|
|
@ -1732,7 +1732,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
<Input
|
||||
id="object-color"
|
||||
type="color"
|
||||
value={selectedObject.color}
|
||||
value={selectedObject.color || "#3b82f6"}
|
||||
onChange={(e) => handleObjectUpdate({ color: e.target.value })}
|
||||
className="mt-1.5 h-9"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import dynamic from "next/dynamic";
|
|||
import { useToast } from "@/hooks/use-toast";
|
||||
import type { PlacedObject, MaterialData } from "@/types/digitalTwin";
|
||||
import { getLayoutById, getMaterials } from "@/lib/api/digitalTwin";
|
||||
import { OBJECT_COLORS, DEFAULT_COLOR } from "./constants";
|
||||
|
||||
const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), {
|
||||
ssr: false,
|
||||
|
|
@ -81,7 +82,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
z: parseFloat(obj.size_z),
|
||||
},
|
||||
rotation: obj.rotation ? parseFloat(obj.rotation) : 0,
|
||||
color: getObjectColor(objectType), // 타입별 기본 색상 사용
|
||||
color: getObjectColor(objectType, obj.color), // 저장된 색상 우선, 없으면 타입별 기본 색상
|
||||
areaKey: obj.area_key,
|
||||
locaKey: obj.loca_key,
|
||||
locType: obj.loc_type,
|
||||
|
|
@ -225,17 +226,11 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
|
||||
// 객체 타입별 기본 색상 (useMemo로 최적화)
|
||||
const getObjectColor = useMemo(() => {
|
||||
return (type: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
area: "#3b82f6", // 파란색
|
||||
"location-bed": "#2563eb", // 진한 파란색
|
||||
"location-stp": "#6b7280", // 회색
|
||||
"location-temp": "#f59e0b", // 주황색
|
||||
"location-dest": "#10b981", // 초록색
|
||||
"crane-mobile": "#8b5cf6", // 보라색
|
||||
rack: "#ef4444", // 빨간색
|
||||
};
|
||||
return colorMap[type] || "#3b82f6";
|
||||
return (type: string, savedColor?: string): string => {
|
||||
// 저장된 색상이 있으면 우선 사용
|
||||
if (savedColor) return savedColor;
|
||||
// 없으면 타입별 기본 색상 사용
|
||||
return OBJECT_COLORS[type] || DEFAULT_COLOR;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -383,7 +378,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
<div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: getObjectColor(obj.type) }}
|
||||
style={{ backgroundColor: obj.color }}
|
||||
/>
|
||||
<span>{typeLabel}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ export default function HierarchyConfigPanel({
|
|||
<div className="flex items-center gap-2">
|
||||
<GripVertical className="text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
value={level.name}
|
||||
value={level.name || ""}
|
||||
onChange={(e) => handleLevelChange(level.level, "name", e.target.value)}
|
||||
className="h-7 w-32 text-xs"
|
||||
placeholder="레벨명"
|
||||
|
|
@ -276,7 +276,7 @@ export default function HierarchyConfigPanel({
|
|||
<div>
|
||||
<Label className="text-[10px]">테이블</Label>
|
||||
<Select
|
||||
value={level.tableName}
|
||||
value={level.tableName || ""}
|
||||
onValueChange={(val) => {
|
||||
handleLevelChange(level.level, "tableName", val);
|
||||
handleTableChange(val, level.level);
|
||||
|
|
@ -300,7 +300,7 @@ export default function HierarchyConfigPanel({
|
|||
<div>
|
||||
<Label className="text-[10px]">ID 컬럼</Label>
|
||||
<Select
|
||||
value={level.keyColumn}
|
||||
value={level.keyColumn || ""}
|
||||
onValueChange={(val) => handleLevelChange(level.level, "keyColumn", val)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-[10px]">
|
||||
|
|
@ -319,7 +319,7 @@ export default function HierarchyConfigPanel({
|
|||
<div>
|
||||
<Label className="text-[10px]">이름 컬럼</Label>
|
||||
<Select
|
||||
value={level.nameColumn}
|
||||
value={level.nameColumn || ""}
|
||||
onValueChange={(val) => handleLevelChange(level.level, "nameColumn", val)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-[10px]">
|
||||
|
|
@ -338,7 +338,7 @@ export default function HierarchyConfigPanel({
|
|||
<div>
|
||||
<Label className="text-[10px]">부모 키 컬럼</Label>
|
||||
<Select
|
||||
value={level.parentKeyColumn}
|
||||
value={level.parentKeyColumn || ""}
|
||||
onValueChange={(val) => handleLevelChange(level.level, "parentKeyColumn", val)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-[10px]">
|
||||
|
|
@ -422,7 +422,7 @@ export default function HierarchyConfigPanel({
|
|||
<div>
|
||||
<Label className="text-[10px]">ID 컬럼</Label>
|
||||
<Select
|
||||
value={localConfig.material.keyColumn}
|
||||
value={localConfig.material.keyColumn || ""}
|
||||
onValueChange={(val) => handleMaterialChange("keyColumn", val)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-[10px]">
|
||||
|
|
@ -441,7 +441,7 @@ export default function HierarchyConfigPanel({
|
|||
<div>
|
||||
<Label className="text-[10px]">위치 키 컬럼</Label>
|
||||
<Select
|
||||
value={localConfig.material.locationKeyColumn}
|
||||
value={localConfig.material.locationKeyColumn || ""}
|
||||
onValueChange={(val) => handleMaterialChange("locationKeyColumn", val)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-[10px]">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* 디지털 트윈 3D 야드 - 공통 상수
|
||||
*/
|
||||
|
||||
// 객체 타입별 색상 매핑 (HEX 코드)
|
||||
export const OBJECT_COLORS: Record<string, string> = {
|
||||
area: "#3b82f6", // 파란색
|
||||
"location-bed": "#2563eb", // 진한 파란색
|
||||
"location-stp": "#6b7280", // 회색
|
||||
"location-temp": "#f59e0b", // 주황색
|
||||
"location-dest": "#10b981", // 초록색
|
||||
"crane-mobile": "#8b5cf6", // 보라색
|
||||
rack: "#ef4444", // 빨간색
|
||||
};
|
||||
|
||||
// Tailwind 색상 클래스 매핑 (아이콘용)
|
||||
export const OBJECT_COLOR_CLASSES: Record<string, string> = {
|
||||
area: "text-blue-500",
|
||||
"location-bed": "text-blue-600",
|
||||
"location-stp": "text-gray-500",
|
||||
"location-temp": "text-orange-500",
|
||||
"location-dest": "text-emerald-500",
|
||||
"crane-mobile": "text-purple-500",
|
||||
rack: "text-red-500",
|
||||
};
|
||||
|
||||
// 기본 색상
|
||||
export const DEFAULT_COLOR = "#3b82f6";
|
||||
export const DEFAULT_COLOR_CLASS = "text-blue-500";
|
||||
|
||||
Loading…
Reference in New Issue