대시보드 설정 저장 및 디지털 트윈 UX 개선

This commit is contained in:
dohyeons 2025-11-21 12:22:27 +09:00
parent 8727ef02f3
commit 1e1bc0b2c6
4 changed files with 103 additions and 78 deletions

View File

@ -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"
/>

View File

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

View File

@ -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]">

View File

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