STP 정차포인트를 자재 미적재 영역으로 분리하고 시각화 개선

This commit is contained in:
dohyeons 2025-11-25 17:08:12 +09:00
parent 11782536f4
commit 710ca122ea
3 changed files with 82 additions and 73 deletions

View File

@ -2,7 +2,7 @@
import { useState, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Save, Loader2, Grid3x3, Move, Box, Package, Truck, Check } from "lucide-react";
import { ArrowLeft, Save, Loader2, Grid3x3, Move, Box, Package, Truck, Check, ParkingCircle } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@ -550,10 +550,11 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
areaKey: obj.area_key,
locaKey: obj.loca_key,
locType: obj.loc_type,
materialCount: obj.material_count,
materialPreview: obj.material_preview_height
? { height: parseFloat(obj.material_preview_height) }
: undefined,
materialCount: obj.loc_type === "STP" ? undefined : obj.material_count,
materialPreview:
obj.loc_type === "STP" || !obj.material_preview_height
? undefined
: { height: parseFloat(obj.material_preview_height) },
parentId: obj.parent_id,
displayOrder: obj.display_order,
locked: obj.locked,
@ -761,12 +762,9 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
// 기본 크기 설정
let objectSize = defaults.size || { x: 5, y: 5, z: 5 };
// Location 배치 시 자재 개수에 따라 높이 자동 설정
// Location 배치 시 자재 개수에 따라 높이 자동 설정 (BED/TMP/DES만 대상, STP는 자재 미적재)
if (
(draggedTool === "location-bed" ||
draggedTool === "location-stp" ||
draggedTool === "location-temp" ||
draggedTool === "location-dest") &&
(draggedTool === "location-bed" || draggedTool === "location-temp" || draggedTool === "location-dest") &&
locaKey &&
selectedDbConnection &&
hierarchyConfig?.material
@ -877,12 +875,9 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
setDraggedAreaData(null);
setDraggedLocationData(null);
// Location 배치 시 자재 개수 로드
// Location 배치 시 자재 개수 로드 (BED/TMP/DES만 대상, STP는 자재 미적재)
if (
(draggedTool === "location-bed" ||
draggedTool === "location-stp" ||
draggedTool === "location-temp" ||
draggedTool === "location-dest") &&
(draggedTool === "location-bed" || draggedTool === "location-temp" || draggedTool === "location-dest") &&
locaKey
) {
// 새 객체 추가 후 자재 개수 로드 (약간의 딜레이를 두어 state 업데이트 완료 후 실행)
@ -965,13 +960,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
loadLocationsForArea(obj.areaKey);
setShowMaterialPanel(false);
}
// Location을 클릭한 경우, 해당 Location의 자재 목록 로드
// Location을 클릭한 경우, 해당 Location의 자재 목록 로드 (STP는 자재 미적재이므로 제외)
else if (
obj &&
(obj.type === "location-bed" ||
obj.type === "location-stp" ||
obj.type === "location-temp" ||
obj.type === "location-dest") &&
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
obj.locaKey &&
selectedDbConnection
) {
@ -988,9 +980,15 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
try {
const response = await getMaterialCounts(selectedDbConnection, selectedTables.material, locaKeys);
if (response.success && response.data) {
// 각 Location 객체에 자재 개수 업데이트
// 각 Location 객체에 자재 개수 업데이트 (STP는 자재 미적재이므로 제외)
setPlacedObjects((prev) =>
prev.map((obj) => {
if (
!obj.locaKey ||
obj.type === "location-stp" // STP는 자재 없음
) {
return obj;
}
const materialCount = response.data?.find((mc) => mc.LOCAKEY === obj.locaKey);
if (materialCount) {
return {
@ -1278,7 +1276,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
const oldSize = actualObject.size;
const newSize = { ...oldSize, ...updates.size };
// W, D를 5 단위로 스냅
// W, D를 5 단위로 스냅 (STP 포함)
newSize.x = Math.max(5, Math.round(newSize.x / 5) * 5);
newSize.z = Math.max(5, Math.round(newSize.z / 5) * 5);
@ -1391,10 +1389,11 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
areaKey: obj.area_key,
locaKey: obj.loca_key,
locType: obj.loc_type,
materialCount: obj.material_count,
materialPreview: obj.material_preview_height
? { height: parseFloat(obj.material_preview_height) }
: undefined,
materialCount: obj.loc_type === "STP" ? undefined : obj.material_count,
materialPreview:
obj.loc_type === "STP" || !obj.material_preview_height
? undefined
: { height: parseFloat(obj.material_preview_height) },
parentId: obj.parent_id,
displayOrder: obj.display_order,
locked: obj.locked,
@ -1798,6 +1797,8 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
</div>
{isLocationPlaced ? (
<Check className="h-4 w-4 text-green-500" />
) : locationType === "location-stp" ? (
<ParkingCircle className="text-muted-foreground h-4 w-4" />
) : (
<Package className="text-muted-foreground h-4 w-4" />
)}

View File

@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { Loader2, Search, X, Grid3x3, Package } from "lucide-react";
import { Loader2, Search, X, Grid3x3, Package, ParkingCircle } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
@ -87,10 +87,11 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
areaKey: obj.area_key,
locaKey: obj.loca_key,
locType: obj.loc_type,
materialCount: obj.material_count,
materialPreview: obj.material_preview_height
? { height: parseFloat(obj.material_preview_height) }
: undefined,
materialCount: obj.loc_type === "STP" ? undefined : obj.material_count,
materialPreview:
obj.loc_type === "STP" || !obj.material_preview_height
? undefined
: { height: parseFloat(obj.material_preview_height) },
parentId: obj.parent_id,
displayOrder: obj.display_order,
locked: obj.locked,
@ -166,13 +167,10 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
const obj = placedObjects.find((o) => o.id === objectId);
setSelectedObject(obj || null);
// Location을 클릭한 경우, 자재 정보 표시
// Location을 클릭한 경우, 자재 정보 표시 (STP는 자재 미적재이므로 제외)
if (
obj &&
(obj.type === "location-bed" ||
obj.type === "location-stp" ||
obj.type === "location-temp" ||
obj.type === "location-dest") &&
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
obj.locaKey &&
externalDbConnectionId
) {
@ -471,7 +469,11 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Package className="h-3 w-3" />
{locationObj.type === "location-stp" ? (
<ParkingCircle className="h-3 w-3" />
) : (
<Package className="h-3 w-3" />
)}
<span className="text-xs font-medium">{locationObj.name}</span>
</div>
<span

View File

@ -593,52 +593,58 @@ function MaterialBox({
);
case "location-stp":
// 정차포인트(STP): 주황색 낮은 플랫폼
return (
<>
<Box args={[boxWidth, boxHeight, boxDepth]}>
<meshStandardMaterial
color={placement.color}
roughness={0.6}
metalness={0.2}
emissive={isSelected ? placement.color : "#000000"}
emissiveIntensity={isSelected ? glowIntensity * 0.8 : 0}
/>
</Box>
// 정차포인트(STP): 회색 타원형 플랫폼 + 'P' 마크 (자재 미적재 영역)
{
const baseRadius = 0.5; // 스케일로 실제 W/D를 반영 (타원형)
const labelFontSize = Math.min(boxWidth, boxDepth) * 0.15;
const iconFontSize = Math.min(boxWidth, boxDepth) * 0.3;
{/* Location 이름 */}
{placement.name && (
return (
<>
{/* 타원형 플랫폼: 단위 실린더를 W/D로 스케일 */}
<mesh scale={[boxWidth, 1, boxDepth]}>
<cylinderGeometry args={[baseRadius, baseRadius, boxHeight, 32]} />
<meshStandardMaterial
color={placement.color}
roughness={0.6}
metalness={0.2}
emissive={isSelected ? placement.color : "#000000"}
emissiveIntensity={isSelected ? glowIntensity * 0.8 : 0}
/>
</mesh>
{/* 상단 'P' 마크 (주차 아이콘 역할) */}
<Text
position={[0, boxHeight / 2 + 0.3, 0]}
position={[0, boxHeight / 2 + 0.05, 0]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={Math.min(boxWidth, boxDepth) * 0.15}
fontSize={iconFontSize}
color="#ffffff"
anchorX="center"
anchorY="middle"
outlineWidth={0.03}
outlineWidth={0.08}
outlineColor="#000000"
>
{placement.name}
P
</Text>
)}
{/* 자재 개수 (STP는 정차포인트라 자재가 없을 수 있음) */}
{placement.material_count !== undefined && placement.material_count > 0 && (
<Text
position={[0, boxHeight / 2 + 0.6, 0]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={Math.min(boxWidth, boxDepth) * 0.12}
color="#fbbf24"
anchorX="center"
anchorY="middle"
outlineWidth={0.03}
outlineColor="#000000"
>
{`자재: ${placement.material_count}`}
</Text>
)}
</>
);
{/* Location 이름 */}
{placement.name && (
<Text
position={[0, boxHeight / 2 + 0.4, 0]}
rotation={[-Math.PI / 2, 0, 0]}
fontSize={labelFontSize}
color="#ffffff"
anchorX="center"
anchorY="middle"
outlineWidth={0.03}
outlineColor="#000000"
>
{placement.name}
</Text>
)}
</>
);
}
// case "gantry-crane":
// // 겐트리 크레인: 기둥 2개 + 상단 빔