diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs index 8cd8a4d..b6c4efa 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs @@ -238,8 +238,13 @@ namespace AGVNavigationCore.Controls for (int i = 0; i < path.Path.Count - 1; i++) { - var currentNodeId = path.Path[i]; - var nextNodeId = path.Path[i + 1]; + var currentNode = path.Path[i]; + var nextNode = path.Path[i + 1]; + + if (currentNode == null || nextNode == null) continue; + + var currentNodeId = currentNode.NodeId; + var nextNodeId = nextNode.NodeId; // 왕복 구간 키 생성 (양방향 모두 같은 키) var segmentKey = string.Compare(currentNodeId, nextNodeId) < 0 @@ -250,9 +255,6 @@ namespace AGVNavigationCore.Controls visitedSegments[segmentKey] = 0; visitedSegments[segmentKey]++; - var currentNode = _nodes?.FirstOrDefault(n => n.NodeId == currentNodeId); - var nextNode = _nodes?.FirstOrDefault(n => n.NodeId == nextNodeId); - if (currentNode != null && nextNode != null) { // 왕복 경로면 더 진한 색상으로 표시 @@ -295,9 +297,8 @@ namespace AGVNavigationCore.Controls const int JUNCTION_CONNECTIONS = 3; // 교차로 판정 기준: 3개 이상의 연결 - foreach (var nodeId in path.Path) + foreach (var node in path.Path) { - var node = _nodes.FirstOrDefault(n => n.NodeId == nodeId); if (node == null) continue; // 교차로 판정: 3개 이상의 노드가 연결된 경우 @@ -339,7 +340,7 @@ namespace AGVNavigationCore.Controls } // 교차로 라벨 추가 - DrawJunctionLabel(g, junctionNode); + //DrawJunctionLabel(g, junctionNode); } /// diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs index ca8c67e..5ec47da 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs @@ -525,10 +525,7 @@ namespace AGVNavigationCore.Controls if (_currentPath != null && _currentPath.Success && _currentPath.Path != null && _currentPath.Path.Count > 0) { // 경로의 마지막 노드가 목적지 - string destinationNodeId = _currentPath.Path[_currentPath.Path.Count - 1]; - - // 노드 목록에서 해당 노드 찾기 - _destinationNode = _nodes?.FirstOrDefault(n => n.NodeId == destinationNodeId); + _destinationNode = _currentPath.Path[_currentPath.Path.Count - 1]; } } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs index 47ca282..6387855 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs @@ -272,7 +272,7 @@ namespace AGVNavigationCore.Models } _currentPath = path; - _remainingNodes = new List(path.Path); + _remainingNodes = new List(path.Path.Select(n => n.NodeId).ToList()); _currentNodeIndex = 0; // 시작 노드와 목표 노드 설정 diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs index f7e5295..fa4b925 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs @@ -18,9 +18,9 @@ namespace AGVNavigationCore.PathFinding.Core public bool Success { get; set; } /// - /// 경로 노드 ID 목록 (시작 → 목적지 순서) + /// 경로 노드 목록 (시작 → 목적지 순서) /// - public List Path { get; set; } + public List Path { get; set; } /// /// AGV 명령어 목록 (이동 방향 시퀀스) @@ -96,13 +96,19 @@ namespace AGVNavigationCore.PathFinding.Core /// public MapNode PrevNode { get; set; } + + /// + /// PrevNode 에서 현재위치까지 이동한 모터의 방향값 + /// + public AgvDirection PrevDirection { get; set; } + /// /// 기본 생성자 /// public AGVPathResult() { Success = false; - Path = new List(); + Path = new List(); Commands = new List(); DetailedPath = new List(); TotalDistance = 0; @@ -116,6 +122,7 @@ namespace AGVNavigationCore.PathFinding.Core DirectionChangeNode = string.Empty; DockingValidation = DockingValidationResult.CreateNotRequired(); PrevNode = null; + PrevDirection = AgvDirection.Stop; } /// @@ -126,12 +133,12 @@ namespace AGVNavigationCore.PathFinding.Core /// 총 거리 /// 계산 시간 /// 성공 결과 - public static AGVPathResult CreateSuccess(List path, List commands, float totalDistance, long calculationTimeMs) + public static AGVPathResult CreateSuccess(List path, List commands, float totalDistance, long calculationTimeMs) { var result = new AGVPathResult { Success = true, - Path = new List(path), + Path = new List(path), Commands = new List(commands), TotalDistance = totalDistance, CalculationTimeMs = calculationTimeMs @@ -287,7 +294,7 @@ namespace AGVNavigationCore.PathFinding.Core /// - /// 단순 경로 목록 반환 (호환성용) + /// 단순 경로 목록 반환 (호환성용 - 노드 ID 문자열 목록) /// /// 노드 ID 목록 public List GetSimplePath() @@ -296,7 +303,7 @@ namespace AGVNavigationCore.PathFinding.Core { return DetailedPath.Select(n => n.NodeId).ToList(); } - return Path ?? new List(); + return Path?.Select(n => n.NodeId).ToList() ?? new List(); } /// diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs index bd66618..e7adebf 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs @@ -14,6 +14,7 @@ namespace AGVNavigationCore.PathFinding.Core { private Dictionary _nodeMap; private List _mapNodes; + private Dictionary _mapNodeLookup; // Quick lookup for node ID -> MapNode /// /// 휴리스틱 가중치 (기본값: 1.0) @@ -33,6 +34,7 @@ namespace AGVNavigationCore.PathFinding.Core { _nodeMap = new Dictionary(); _mapNodes = new List(); + _mapNodeLookup = new Dictionary(); } /// @@ -43,10 +45,13 @@ namespace AGVNavigationCore.PathFinding.Core { _mapNodes = mapNodes ?? new List(); _nodeMap.Clear(); + _mapNodeLookup.Clear(); // 모든 네비게이션 노드를 PathNode로 변환하고 양방향 연결 생성 foreach (var mapNode in _mapNodes) { + _mapNodeLookup[mapNode.NodeId] = mapNode; // Add to lookup table + if (mapNode.IsNavigationNode()) { var pathNode = new PathNode(mapNode.NodeId, mapNode.Position); @@ -82,6 +87,14 @@ namespace AGVNavigationCore.PathFinding.Core } } + /// + /// 노드 ID로 MapNode 가져오기 (헬퍼 메서드) + /// + private MapNode GetMapNode(string nodeId) + { + return _mapNodeLookup.ContainsKey(nodeId) ? _mapNodeLookup[nodeId] : null; + } + /// /// 경로 찾기 (A* 알고리즘) /// @@ -106,7 +119,8 @@ namespace AGVNavigationCore.PathFinding.Core if (startNodeId == endNodeId) { - var singlePath = new List { startNodeId }; + var startMapNode = GetMapNode(startNodeId); + var singlePath = new List { startMapNode }; return AGVPathResult.CreateSuccess(singlePath, new List(), 0, stopwatch.ElapsedMilliseconds); } @@ -240,7 +254,7 @@ namespace AGVNavigationCore.PathFinding.Core validWaypoints = deduplicatedWaypoints; // 최종 경로 리스트와 누적 값 - var combinedPath = new List(); + var combinedPath = new List(); float totalDistance = 0; long totalCalculationTime = 0; @@ -353,23 +367,25 @@ namespace AGVNavigationCore.PathFinding.Core return previousResult; // 합친 경로 생성 - var combinedPath = new List(previousResult.Path); + var combinedPath = new List(previousResult.Path); var combinedCommands = new List(previousResult.Commands); var combinedDetailedPath = new List(previousResult.DetailedPath ?? new List()); // 이전 경로의 마지막 노드와 현재 경로의 시작 노드 비교 - string lastNodeOfPrevious = previousResult.Path[previousResult.Path.Count - 1]; - string firstNodeOfCurrent = currentResult.Path[0]; + string lastNodeOfPrevious = previousResult.Path[previousResult.Path.Count - 1].NodeId; + string firstNodeOfCurrent = currentResult.Path[0].NodeId; if (lastNodeOfPrevious == firstNodeOfCurrent) { // 첫 번째 노드 제거 (중복 제거) - combinedPath.AddRange(currentResult.Path.Skip(1)); + combinedPath.RemoveAt(combinedPath.Count - 1); + combinedPath.AddRange(currentResult.Path); // DetailedPath도 첫 번째 노드 제거 if (currentResult.DetailedPath != null && currentResult.DetailedPath.Count > 0) { - combinedDetailedPath.AddRange(currentResult.DetailedPath.Skip(1)); + combinedDetailedPath.RemoveAt(combinedDetailedPath.Count - 1); + combinedDetailedPath.AddRange(currentResult.DetailedPath); } } else @@ -404,6 +420,7 @@ namespace AGVNavigationCore.PathFinding.Core // DetailedPath 설정 result.DetailedPath = combinedDetailedPath; result.PrevNode = previousResult.PrevNode; + result.PrevDirection = previousResult.PrevDirection; return result; } @@ -463,14 +480,18 @@ namespace AGVNavigationCore.PathFinding.Core /// /// 경로 재구성 (부모 노드를 따라 역추적) /// - private List ReconstructPath(PathNode endNode) + private List ReconstructPath(PathNode endNode) { - var path = new List(); + var path = new List(); var current = endNode; while (current != null) { - path.Add(current.NodeId); + var mapNode = GetMapNode(current.NodeId); + if (mapNode != null) + { + path.Add(mapNode); + } current = current.Parent; } @@ -481,16 +502,19 @@ namespace AGVNavigationCore.PathFinding.Core /// /// 경로의 총 거리 계산 /// - private float CalculatePathDistance(List path) + private float CalculatePathDistance(List path) { if (path.Count < 2) return 0; float totalDistance = 0; for (int i = 0; i < path.Count - 1; i++) { - if (_nodeMap.ContainsKey(path[i]) && _nodeMap.ContainsKey(path[i + 1])) + var nodeId1 = path[i].NodeId; + var nodeId2 = path[i + 1].NodeId; + + if (_nodeMap.ContainsKey(nodeId1) && _nodeMap.ContainsKey(nodeId2)) { - totalDistance += _nodeMap[path[i]].DistanceTo(_nodeMap[path[i + 1]]); + totalDistance += _nodeMap[nodeId1].DistanceTo(_nodeMap[nodeId2]); } } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs index f0c5833..d9925be 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs @@ -88,17 +88,16 @@ namespace AGVNavigationCore.PathFinding.Planning // 경로상의 모든 노드 중 교차로(3개 이상 연결) 찾기 var StartNode = pathResult.Path.First(); - foreach (var nodeId in pathResult.Path) + foreach (var pathNode in pathResult.Path) { - var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); - if (node != null && - node.IsActive && - node.IsNavigationNode() && - node.ConnectedNodes != null && - node.ConnectedNodes.Count >= 3) + if (pathNode != null && + pathNode.IsActive && + pathNode.IsNavigationNode() && + pathNode.ConnectedNodes != null && + pathNode.ConnectedNodes.Count >= 3) { - if (node.NodeId.Equals(StartNode) == false) - return node; + if (pathNode.NodeId.Equals(StartNode.NodeId) == false) + return pathNode; } } @@ -106,7 +105,7 @@ namespace AGVNavigationCore.PathFinding.Planning } public AGVPathResult FindPath_test(MapNode startNode, MapNode targetNode, - MapNode prevNode, AgvDirection currentDirection) + MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection) { // 입력 검증 if (startNode == null) @@ -127,8 +126,8 @@ namespace AGVNavigationCore.PathFinding.Planning return AGVPathResult.CreateFailure("각 노드간 최단 경로 계산이 실패되었습니다", 0, 0); //정방향/역방향 이동 시 다음 노드 확인 - var nextNodeForward = DirectionalHelper.GetNextNodeByDirection(startNode, prevNode, currentDirection, _mapNodes); - var nextNodeBackward = DirectionalHelper.GetNextNodeByDirection(startNode, prevNode, ReverseDirection, _mapNodes); + var nextNodeForward = DirectionalHelper.GetNextNodeByDirection(startNode, prevNode, prevDirection, currentDirection, _mapNodes); + var nextNodeBackward = DirectionalHelper.GetNextNodeByDirection(startNode, prevNode, prevDirection, ReverseDirection, _mapNodes); //2.AGV방향과 목적지에 설정된 방향이 일치하면 그대로 진행하면된다.(목적지에 방향이 없는 경우에도 그대로 진행) @@ -136,9 +135,12 @@ namespace AGVNavigationCore.PathFinding.Planning (targetNode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward) || (targetNode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward)) { - MakeDetailData(pathResult, currentDirection); - MakeMagnetDirection(pathResult); - return pathResult; + if (nextNodeForward.NodeId == pathResult.Path[1].NodeId) //예측경로와 다음진행방향 경로가 일치하면 해당 방향이 맞다 + { + MakeDetailData(pathResult, currentDirection); + MakeMagnetDirection(pathResult); + return pathResult; + } } @@ -154,15 +156,15 @@ namespace AGVNavigationCore.PathFinding.Planning // return pathResult; //} - + //뒤로 이동시 경로상의 처음 만나는 노드가 같다면 그 방향으로 이동하면 된다. - if (nextNodeBackward.NodeId == pathResult.Path[1] && targetNode.DockDirection == DockingDirection.Backward) + if (nextNodeBackward != null && pathResult.Path.Count > 1 && nextNodeBackward.NodeId == pathResult.Path[1].NodeId && targetNode.DockDirection == DockingDirection.Backward) { MakeDetailData(pathResult, ReverseDirection); MakeMagnetDirection(pathResult); return pathResult; } - if(nextNodeForward.NodeId == pathResult.Path[1] && targetNode.DockDirection == DockingDirection.Forward) + if (nextNodeForward != null && pathResult.Path.Count > 1 && nextNodeForward.NodeId == pathResult.Path[1].NodeId && targetNode.DockDirection == DockingDirection.Forward) { MakeDetailData(pathResult, currentDirection); MakeMagnetDirection(pathResult); @@ -197,12 +199,14 @@ namespace AGVNavigationCore.PathFinding.Planning path1.PrevNode = prevNode; //다음좌표를 보고 정방향인지 역방향인지 체크한다. - if( nextNodeForward.NodeId.Equals( path1.Path[1])) + bool ReverseCheck = false; + if (path1.Path.Count > 1 && nextNodeForward != null && nextNodeForward.NodeId.Equals(path1.Path[1].NodeId)) { MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정) } - else if(nextNodeBackward.NodeId.Equals(path1.Path[1])) + else if (path1.Path.Count > 1 && nextNodeBackward != null && nextNodeBackward.NodeId.Equals(path1.Path[1].NodeId)) { + ReverseCheck = true; MakeDetailData(path1, ReverseDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정) } else return AGVPathResult.CreateFailure("교차로까지 계산된 경로에 현재 위치정보로 추측을 할 수 없습니다", 0, 0); @@ -215,12 +219,13 @@ namespace AGVNavigationCore.PathFinding.Planning //2.교차로 - 종료위치 var path2 = _basicPathfinder.FindPath(JunctionInPath.NodeId, targetNode.NodeId); path2.PrevNode = prevNode; - MakeDetailData(path2, ReverseDirection); + if (ReverseCheck) MakeDetailData(path2, currentDirection); + else MakeDetailData(path2, ReverseDirection); //3.방향전환을 위환 대체 노드찾기 var tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId, - path1.Path[path1.Path.Count - 2], - path2.Path[1]); + path1.Path[path1.Path.Count - 2].NodeId, + path2.Path[1].NodeId); //4. path1 + tempnode + path2 가 최종 위치가 된다. if (tempNode == null) @@ -233,7 +238,10 @@ namespace AGVNavigationCore.PathFinding.Planning var pathToTemp = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode.NodeId); if (!pathToTemp.Success) return AGVPathResult.CreateFailure("교차로에서 대체 노드까지의 경로를 찾을 수 없습니다.", 0, 0); - MakeDetailData(pathToTemp, currentDirection); + + if (ReverseCheck) MakeDetailData(pathToTemp, ReverseDirection); + else MakeDetailData(pathToTemp, currentDirection); + if (pathToTemp.DetailedPath.Count > 1) pathToTemp.DetailedPath[pathToTemp.DetailedPath.Count - 1].MotorDirection = ReverseDirection; @@ -244,7 +252,9 @@ namespace AGVNavigationCore.PathFinding.Planning var pathFromTemp = _basicPathfinder.FindPath(tempNode.NodeId, JunctionInPath.NodeId); if (!pathFromTemp.Success) return AGVPathResult.CreateFailure("대체 노드에서 교차로까지의 경로를 찾을 수 없습니다.", 0, 0); - MakeDetailData(pathFromTemp, ReverseDirection); + + if (ReverseCheck) MakeDetailData(pathFromTemp, currentDirection); + else MakeDetailData(pathFromTemp, ReverseDirection); // (path1 + pathToTemp) + pathFromTemp 합치기 combinedResult = _basicPathfinder.CombineResults(combinedResult, pathFromTemp); @@ -271,12 +281,14 @@ namespace AGVNavigationCore.PathFinding.Planning var detailedPath1 = new List(); for (int i = 0; i < path1.Path.Count; i++) { - string nodeId = path1.Path[i]; - string nextNodeId = (i + 1 < path1.Path.Count) ? path1.Path[i + 1] : null; + var node = path1.Path[i]; + string nodeId = node.NodeId; + string RfidId = node.RfidId; + string nextNodeId = (i + 1 < path1.Path.Count) ? path1.Path[i + 1].NodeId : null; // 노드 정보 생성 (현재 방향 유지) var nodeInfo = new NodeMotorInfo( - nodeId, + nodeId, RfidId, currentDirection, nextNodeId, MagnetDirection.Straight @@ -302,13 +314,13 @@ namespace AGVNavigationCore.PathFinding.Planning for (int i = 0; i < path1.DetailedPath.Count; i++) { var detailPath = path1.DetailedPath[i]; - string nodeId = path1.Path[i]; - string nextNodeId = (i + 1 < path1.Path.Count) ? path1.Path[i + 1] : null; + string nodeId = path1.Path[i].NodeId; + string nextNodeId = (i + 1 < path1.Path.Count) ? path1.Path[i + 1].NodeId : null; // 마그넷 방향 계산 (3개 이상 연결된 교차로에서만 좌/우 가중치 적용) if (i > 0 && nextNodeId != null) { - string prevNodeId = path1.Path[i - 1]; + string prevNodeId = path1.Path[i - 1].NodeId; if (path1.DetailedPath[i - 1].MotorDirection != detailPath.MotorDirection) detailPath.MagnetDirection = MagnetDirection.Straight; else diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs index 13fc52a..b16a692 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs @@ -20,19 +20,19 @@ namespace AGVNavigationCore.PathFinding.Planning public class DirectionChangePlan { public bool Success { get; set; } - public List DirectionChangePath { get; set; } + public List DirectionChangePath { get; set; } public string DirectionChangeNode { get; set; } public string ErrorMessage { get; set; } public string PlanDescription { get; set; } public DirectionChangePlan() { - DirectionChangePath = new List(); + DirectionChangePath = new List(); ErrorMessage = string.Empty; PlanDescription = string.Empty; } - public static DirectionChangePlan CreateSuccess(List path, string changeNode, string description) + public static DirectionChangePlan CreateSuccess(List path, string changeNode, string description) { return new DirectionChangePlan { @@ -89,16 +89,16 @@ namespace AGVNavigationCore.PathFinding.Planning if (directPath2.Success) { // 직접 경로에 갈림길이 포함된 경우 그 갈림길에서 방향 전환 - foreach (var nodeId in directPath2.Path.Skip(1).Take(directPath2.Path.Count - 2)) // 시작과 끝 제외 + foreach (var node in directPath2.Path.Skip(1).Take(directPath2.Path.Count - 2)) // 시작과 끝 제외 { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(nodeId); + var junctionInfo = _junctionAnalyzer.GetJunctionInfo(node.NodeId); if (junctionInfo != null && junctionInfo.IsJunction) { // 간단한 방향 전환: 직접 경로 사용하되 방향 전환 노드 표시 return DirectionChangePlan.CreateSuccess( directPath2.Path, - nodeId, - $"갈림길 {nodeId}에서 방향 전환: {currentDirection} → {requiredDirection}" + node.NodeId, + $"갈림길 {node.NodeId}에서 방향 전환: {currentDirection} → {requiredDirection}" ); } } @@ -163,16 +163,16 @@ namespace AGVNavigationCore.PathFinding.Planning var directPath = _pathfinder.FindPath(startNodeId, targetNodeId); if (directPath.Success) { - foreach (var nodeId in directPath.Path.Skip(2)) // 시작점과 다음 노드는 제외 + foreach (var node in directPath.Path.Skip(2)) // 시작점과 다음 노드는 제외 { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(nodeId); + var junctionInfo = _junctionAnalyzer.GetJunctionInfo(node.NodeId); if (junctionInfo != null && junctionInfo.IsJunction) { // 직진 경로상에서는 더 엄격한 조건 적용 - if (!suitableJunctions.Contains(nodeId) && - HasMultipleExitOptions(nodeId)) + if (!suitableJunctions.Contains(node.NodeId) && + HasMultipleExitOptions(node.NodeId)) { - suitableJunctions.Add(nodeId); + suitableJunctions.Add(node.NodeId); } } } @@ -290,7 +290,7 @@ namespace AGVNavigationCore.PathFinding.Planning string actualDirectionChangeNode = FindActualDirectionChangeNode(changePath, junctionNodeId); string description = $"갈림길 {GetDisplayName(junctionNodeId)}를 통해 {GetDisplayName(actualDirectionChangeNode)}에서 방향 전환: {currentDirection} → {requiredDirection}"; - System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ✅ 유효한 방향전환 경로: {string.Join(" → ", changePath)}"); + System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ✅ 유효한 방향전환 경로: {string.Join(" → ", changePath.Select(n => n.NodeId))}"); return DirectionChangePlan.CreateSuccess(changePath, actualDirectionChangeNode, description); } @@ -305,9 +305,9 @@ namespace AGVNavigationCore.PathFinding.Planning /// /// 방향 전환 경로 생성 (인근 갈림길 우회 방식) /// - private List GenerateDirectionChangePath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) + private List GenerateDirectionChangePath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) { - var fullPath = new List(); + var fullPath = new List(); // 1. 시작점에서 갈림길까지의 경로 var toJunctionPath = _pathfinder.FindPath(startNodeId, junctionNodeId); @@ -316,7 +316,7 @@ namespace AGVNavigationCore.PathFinding.Planning // 2. 인근 갈림길을 통한 우회인지, 직진 경로상 갈림길인지 판단 var directPath = _pathfinder.FindPath(startNodeId, targetNodeId); - bool isNearbyDetour = !directPath.Success || !directPath.Path.Contains(junctionNodeId); + bool isNearbyDetour = !directPath.Success || !directPath.Path.Any(n => n.NodeId == junctionNodeId); if (isNearbyDetour) { @@ -333,9 +333,9 @@ namespace AGVNavigationCore.PathFinding.Planning /// /// 인근 갈림길을 통한 우회 경로 생성 (예: 012 → 013 → 마그넷으로 016 방향) /// - private List GenerateNearbyDetourPath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) + private List GenerateNearbyDetourPath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) { - var fullPath = new List(); + var fullPath = new List(); // 1. 시작점에서 갈림길까지 직진 (현재 방향 유지) var toJunctionPath = _pathfinder.FindPath(startNodeId, junctionNodeId); @@ -358,9 +358,9 @@ namespace AGVNavigationCore.PathFinding.Planning /// /// 직진 경로상 갈림길에서 방향 전환 경로 생성 (기존 방식 개선) /// - private List GenerateDirectPathChangeRoute(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) + private List GenerateDirectPathChangeRoute(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) { - var fullPath = new List(); + var fullPath = new List(); // 1. 시작점에서 갈림길까지의 경로 var toJunctionPath = _pathfinder.FindPath(startNodeId, junctionNodeId); @@ -373,17 +373,17 @@ namespace AGVNavigationCore.PathFinding.Planning if (currentDirection != requiredDirection) { string fromNodeId = toJunctionPath.Path.Count >= 2 ? - toJunctionPath.Path[toJunctionPath.Path.Count - 2] : startNodeId; + toJunctionPath.Path[toJunctionPath.Path.Count - 2].NodeId : startNodeId; var changeSequence = GenerateDirectionChangeSequence(junctionNodeId, fromNodeId, currentDirection, requiredDirection); if (changeSequence.Count > 1) { - fullPath.AddRange(changeSequence.Skip(1)); + fullPath.AddRange(changeSequence.Skip(1).Select(nodeId => _mapNodes.FirstOrDefault(n => n.NodeId == nodeId)).Where(n => n != null)); } } // 3. 갈림길에서 목표점까지의 경로 - string lastNode = fullPath.LastOrDefault() ?? junctionNodeId; + string lastNode = fullPath.LastOrDefault()?.NodeId ?? junctionNodeId; var fromJunctionPath = _pathfinder.FindPath(lastNode, targetNodeId); if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1) { @@ -549,7 +549,7 @@ namespace AGVNavigationCore.PathFinding.Planning /// /// 실제 방향 전환이 일어나는 노드 찾기 /// - private string FindActualDirectionChangeNode(List changePath, string junctionNodeId) + private string FindActualDirectionChangeNode(List changePath, string junctionNodeId) { // 방향전환 경로 구조: [start...junction, detourNode, junction...target] // 실제 방향전환은 detourNode에서 일어남 (AGV가 한 태그 더 지나간 후) @@ -558,14 +558,23 @@ namespace AGVNavigationCore.PathFinding.Planning return junctionNodeId; // 기본값으로 갈림길 반환 // 갈림길이 두 번 나타나는 위치 찾기 - int firstJunctionIndex = changePath.IndexOf(junctionNodeId); - int lastJunctionIndex = changePath.LastIndexOf(junctionNodeId); + int firstJunctionIndex = changePath.FindIndex(n => n.NodeId == junctionNodeId); + int lastJunctionIndex = -1; + for (int i = changePath.Count - 1; i >= 0; i--) + { + if (changePath[i].NodeId == junctionNodeId) + { + lastJunctionIndex = i; + break; + } + } // 갈림길이 두 번 나타나고, 그 사이에 노드가 있는 경우 - if (firstJunctionIndex != lastJunctionIndex && lastJunctionIndex - firstJunctionIndex == 2) + if (firstJunctionIndex != -1 && lastJunctionIndex != -1 && + firstJunctionIndex != lastJunctionIndex && lastJunctionIndex - firstJunctionIndex == 2) { // 첫 번째와 두 번째 갈림길 사이에 있는 노드가 실제 방향전환 노드 - string detourNode = changePath[firstJunctionIndex + 1]; + string detourNode = changePath[firstJunctionIndex + 1].NodeId; return detourNode; } @@ -617,7 +626,7 @@ namespace AGVNavigationCore.PathFinding.Planning /// /// 방향전환 경로 검증 - 되돌아가기 패턴 및 물리적 실현성 검증 /// - private PathValidationResult ValidateDirectionChangePath(List path, string startNodeId, string junctionNodeId) + private PathValidationResult ValidateDirectionChangePath(List path, string startNodeId, string junctionNodeId) { if (path == null || path.Count == 0) { @@ -635,11 +644,11 @@ namespace AGVNavigationCore.PathFinding.Planning } string errorMessage = $"되돌아가기 패턴 검출 ({backtrackingPatterns.Count}개): {string.Join(", ", issues)}"; - System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 경로: {string.Join(" → ", path)}"); + System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 경로: {string.Join(" → ", path.Select(n => n.NodeId))}"); System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 되돌아가기 패턴: {errorMessage}"); return PathValidationResult.CreateInvalidWithBacktracking( - path, backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage); + path.Select(n => n.NodeId).ToList(), backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage); } // 2. 연속된 중복 노드 검증 @@ -658,27 +667,27 @@ namespace AGVNavigationCore.PathFinding.Planning } // 4. 갈림길 포함 여부 검증 - if (!path.Contains(junctionNodeId)) + if (!path.Any(n => n.NodeId == junctionNodeId)) { return PathValidationResult.CreateInvalid(startNodeId, "", $"갈림길 {junctionNodeId}이 경로에 포함되지 않음"); } - System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path)}"); - return PathValidationResult.CreateValid(path, startNodeId, "", junctionNodeId); + System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path.Select(n => n.NodeId))}"); + return PathValidationResult.CreateValid(path.Select(n => n.NodeId).ToList(), startNodeId, "", junctionNodeId); } /// /// 되돌아가기 패턴 검출 (A → B → A) /// - private List DetectBacktrackingPatterns(List path) + private List DetectBacktrackingPatterns(List path) { var patterns = new List(); for (int i = 0; i < path.Count - 2; i++) { - string nodeA = path[i]; - string nodeB = path[i + 1]; - string nodeC = path[i + 2]; + string nodeA = path[i].NodeId; + string nodeB = path[i + 1].NodeId; + string nodeC = path[i + 2].NodeId; // A → B → A 패턴 검출 if (nodeA == nodeC && nodeA != nodeB) @@ -694,15 +703,15 @@ namespace AGVNavigationCore.PathFinding.Planning /// /// 연속된 중복 노드 검출 /// - private List DetectConsecutiveDuplicates(List path) + private List DetectConsecutiveDuplicates(List path) { var duplicates = new List(); for (int i = 0; i < path.Count - 1; i++) { - if (path[i] == path[i + 1]) + if (path[i].NodeId == path[i + 1].NodeId) { - duplicates.Add(path[i]); + duplicates.Add(path[i].NodeId); } } @@ -712,12 +721,12 @@ namespace AGVNavigationCore.PathFinding.Planning /// /// 경로 연결성 검증 /// - private PathValidationResult ValidatePathConnectivity(List path) + private PathValidationResult ValidatePathConnectivity(List path) { for (int i = 0; i < path.Count - 1; i++) { - string currentNode = path[i]; - string nextNode = path[i + 1]; + string currentNode = path[i].NodeId; + string nextNode = path[i + 1].NodeId; // 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedNodes 리스트 사용) var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNode); diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs index 7cbbc9f..b8096e0 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs @@ -33,6 +33,11 @@ namespace AGVNavigationCore.PathFinding.Planning /// public string NodeId { get; set; } + /// + /// RFID Value + /// + public string RfidId { get; set; } + /// /// 해당 노드에서의 모터방향 /// @@ -68,9 +73,10 @@ namespace AGVNavigationCore.PathFinding.Planning /// public string SpecialActionDescription { get; set; } - public NodeMotorInfo(string nodeId, AgvDirection motorDirection, string nextNodeId = null, MagnetDirection magnetDirection = MagnetDirection.Straight) + public NodeMotorInfo(string nodeId,string rfid, AgvDirection motorDirection, string nextNodeId = null, MagnetDirection magnetDirection = MagnetDirection.Straight) { NodeId = nodeId; + RfidId = rfid; MotorDirection = motorDirection; MagnetDirection = magnetDirection; NextNodeId = nextNodeId; @@ -80,27 +86,13 @@ namespace AGVNavigationCore.PathFinding.Planning SpecialActionDescription = string.Empty; } - /// - /// 방향 전환 정보를 포함한 생성자 - /// - public NodeMotorInfo(string nodeId, AgvDirection motorDirection, string nextNodeId, bool canRotate, bool isDirectionChangePoint, MagnetDirection magnetDirection = MagnetDirection.Straight, bool requiresSpecialAction = false, string specialActionDescription = "") - { - NodeId = nodeId; - MotorDirection = motorDirection; - MagnetDirection = magnetDirection; - NextNodeId = nextNodeId; - CanRotate = canRotate; - IsDirectionChangePoint = isDirectionChangePoint; - RequiresSpecialAction = requiresSpecialAction; - SpecialActionDescription = specialActionDescription ?? string.Empty; - } - + /// /// 디버깅용 문자열 표현 /// public override string ToString() { - var result = $"{NodeId}:{MotorDirection}"; + var result = $"{RfidId}[{NodeId}]:{MotorDirection}"; // 마그넷 방향이 직진이 아닌 경우 표시 if (MagnetDirection != MagnetDirection.Straight) diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs index d79c0fe..4e2d36f 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs @@ -9,20 +9,25 @@ namespace AGVNavigationCore.Utils /// /// AGV 방향 계산 헬퍼 유틸리티 /// 현재 위치에서 주어진 모터 방향으로 이동할 때 다음 노드를 계산 + /// 이전 이동 방향을 고려하여 더 정확한 경로 예측 /// public static class DirectionalHelper { + /// - /// 현재 노드에서 주어진 방향(Forward/Backward)으로 이동할 때 다음 노드를 반환 + /// 현재 노드에서 주어진 방향(Forward/Backward)으로 이동할 때 다음 노드를 반환 (개선된 버전) + /// 이전 모터 방향 정보를 고려하여 더 정확한 경로 예측 /// /// 현재 노드 /// 이전 노드 (진행 방향 기준점) /// 이동 방향 (Forward 또는 Backward) /// 모든 맵 노드 + /// 이전 구간에서의 모터 방향 (선택사항) /// 다음 노드 (또는 null) public static MapNode GetNextNodeByDirection( MapNode currentNode, MapNode prevNode, + AgvDirection prevDirection, AgvDirection direction, List allNodes) { @@ -41,8 +46,7 @@ namespace AGVNavigationCore.Utils if (candidateNodes.Count == 0) return null; - // Forward인 경우: 이전→현재 방향으로 계속 직진하는 노드 우선 - // Backward인 경우: 이전→현재 방향의 반대로 이동하는 노드 우선 + // 이전→현재 이동 벡터 var movementVector = new PointF( currentNode.Position.X - prevNode.Position.X, currentNode.Position.Y - prevNode.Position.Y @@ -101,6 +105,14 @@ namespace AGVNavigationCore.Utils score = -dotProduct; } + // 이전 모터 방향이 제공된 경우: 방향 일관성 보너스 추가 + score = ApplyMotorDirectionConsistencyBonus( + score, + direction, + prevDirection, + dotProduct + ); + if (score > bestScore) { bestScore = score; @@ -110,5 +122,153 @@ namespace AGVNavigationCore.Utils return bestNode; } + + /// + /// 모터 방향 일관성을 고려한 점수 보정 + /// 같은 방향으로 계속 이동하는 경우 보너스 점수 부여 + /// + /// 기본 점수 + /// 현재 모터 방향 + /// 이전 모터 방향 + /// 벡터 내적값 + /// 조정된 점수 + private static float ApplyMotorDirectionConsistencyBonus( + float baseScore, + AgvDirection currentDirection, + AgvDirection prevMotorDirection, + float dotProduct) + { + float adjustedScore = baseScore; + + // 모터 방향이 변경되지 않은 경우: 일관성 보너스 + if (currentDirection == prevMotorDirection) + { + // Forward 지속: 직진 방향으로의 이동 선호 + // Backward 지속: 반대 방향으로의 이동 선호 + const float CONSISTENCY_BONUS = 0.2f; + adjustedScore += CONSISTENCY_BONUS; + + System.Diagnostics.Debug.WriteLine( + $"[DirectionalHelper] 모터 방향 일관성 보너스: {currentDirection} → {currentDirection} " + + $"(점수: {baseScore:F3} → {adjustedScore:F3})"); + } + else + { + // 모터 방향이 변경된 경우: 방향 변경 페널티 + const float DIRECTION_CHANGE_PENALTY = 0.15f; + adjustedScore -= DIRECTION_CHANGE_PENALTY; + + System.Diagnostics.Debug.WriteLine( + $"[DirectionalHelper] 모터 방향 변경 페널티: {prevMotorDirection} → {currentDirection} " + + $"(점수: {baseScore:F3} → {adjustedScore:F3})"); + } + + return adjustedScore; + } + + /// + /// 모터 방향을 고려한 다음 노드 선택 (디버깅/분석용) + /// + public static (MapNode node, float score, string reason) GetNextNodeByDirectionWithDetails( + MapNode currentNode, + MapNode prevNode, + AgvDirection direction, + List allNodes, + AgvDirection? prevMotorDirection) + { + if (currentNode == null || prevNode == null || allNodes == null) + return (null, 0, "입력 파라미터가 null입니다"); + + var connectedNodeIds = currentNode.ConnectedNodes; + if (connectedNodeIds == null || connectedNodeIds.Count == 0) + return (null, 0, "연결된 노드가 없습니다"); + + var candidateNodes = allNodes.Where(n => + connectedNodeIds.Contains(n.NodeId) + ).ToList(); + + if (candidateNodes.Count == 0) + return (null, 0, "후보 노드가 없습니다"); + + var movementVector = new PointF( + currentNode.Position.X - prevNode.Position.X, + currentNode.Position.Y - prevNode.Position.Y + ); + + var movementLength = (float)Math.Sqrt( + movementVector.X * movementVector.X + + movementVector.Y * movementVector.Y + ); + + if (movementLength < 0.001f) + return (candidateNodes[0], 1.0f, "움직임이 거의 없음"); + + var normalizedMovement = new PointF( + movementVector.X / movementLength, + movementVector.Y / movementLength + ); + + MapNode bestNode = null; + float bestScore = float.MinValue; + string reason = ""; + + foreach (var candidate in candidateNodes) + { + var toNextVector = new PointF( + candidate.Position.X - currentNode.Position.X, + candidate.Position.Y - currentNode.Position.Y + ); + + var toNextLength = (float)Math.Sqrt( + toNextVector.X * toNextVector.X + + toNextVector.Y * toNextVector.Y + ); + + if (toNextLength < 0.001f) + continue; + + var normalizedToNext = new PointF( + toNextVector.X / toNextLength, + toNextVector.Y / toNextLength + ); + + float dotProduct = (normalizedMovement.X * normalizedToNext.X) + + (normalizedMovement.Y * normalizedToNext.Y); + + float score = (direction == AgvDirection.Forward) ? dotProduct : -dotProduct; + + if (prevMotorDirection.HasValue) + { + score = ApplyMotorDirectionConsistencyBonus( + score, + direction, + prevMotorDirection.Value, + dotProduct + ); + } + + if (score > bestScore) + { + bestScore = score; + bestNode = candidate; + + // 선택 이유 생성 + if (prevMotorDirection.HasValue && direction == prevMotorDirection) + { + reason = $"모터 방향 일관성 유지 ({direction}) → {candidate.NodeId}"; + } + else if (prevMotorDirection.HasValue) + { + reason = $"모터 방향 변경 ({prevMotorDirection} → {direction}) → {candidate.NodeId}"; + } + else + { + reason = $"방향 기반 선택 ({direction}) → {candidate.NodeId}"; + } + } + } + + return (bestNode, bestScore, reason); + } } } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs index 1869bdb..0829ca3 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs @@ -29,17 +29,16 @@ namespace AGVNavigationCore.Utils return DockingValidationResult.CreateNotRequired(); } - // 목적지 노드 찾기 - string targetNodeId = pathResult.Path[pathResult.Path.Count - 1]; - var LastNode = mapNodes?.FirstOrDefault(n => n.NodeId == targetNodeId); + // 목적지 노드 가져오기 (Path는 이제 List) + var LastNode = pathResult.Path[pathResult.Path.Count - 1]; if (LastNode == null) { - System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드 찾을 수 없음: {targetNodeId}"); + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드가 null입니다"); return DockingValidationResult.CreateNotRequired(); } - System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드: {targetNodeId} 타입:{LastNode.Type} ({(int)LastNode.Type})"); + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드: {LastNode.NodeId} 타입:{LastNode.Type} ({(int)LastNode.Type})"); //detail 경로 이동 예측 검증 for (int i = 0; i < pathResult.DetailedPath.Count - 1; i++) @@ -53,11 +52,17 @@ namespace AGVNavigationCore.Utils if (curNode != null && nextNode != null) { MapNode prevNode = null; - if (i == 0) prevNode = pathResult.PrevNode; + AgvDirection prevDir = AgvDirection.Stop; + if (i == 0) + { + prevNode = pathResult.PrevNode; + prevDir = pathResult.PrevDirection; + } else { var prevNodeId = pathResult.DetailedPath[i - 1].NodeId; prevNode = mapNodes?.FirstOrDefault(n => n.NodeId == prevNodeId); + prevDir = pathResult.DetailedPath[i - 1].MotorDirection; } @@ -67,21 +72,20 @@ namespace AGVNavigationCore.Utils var expectedNextNode = DirectionalHelper.GetNextNodeByDirection( curNode, prevNode, + prevDir, pathResult.DetailedPath[i].MotorDirection, mapNodes ); if (expectedNextNode != null && !expectedNextNode.NodeId.Equals(nextNode.NodeId)) { - - string error = $"[DockingValidator] ⚠️ 경로 방향 불일치: " + $"현재={curNode.RfidId}[{curNodeId}] 이전={prevNode.RfidId}[{(prevNode?.NodeId ?? string.Empty)}] " + $"예상다음={expectedNextNode.RfidId}[{expectedNextNode.NodeId}] 실제다음={nextNode.RfidId}[{nextNodeId}]"; System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}"); return DockingValidationResult.CreateInvalid( - targetNodeId, + LastNode.NodeId, LastNode.Type, pathResult.DetailedPath[i].MotorDirection, pathResult.DetailedPath[i].MotorDirection, @@ -101,6 +105,8 @@ namespace AGVNavigationCore.Utils return DockingValidationResult.CreateNotRequired(); } + + // 필요한 도킹 방향 확인 var requiredDirection = GetRequiredDockingDirection(LastNode.DockDirection); System.Diagnostics.Debug.WriteLine($"[DockingValidator] 필요한 도킹 방향: {requiredDirection}"); @@ -111,7 +117,7 @@ namespace AGVNavigationCore.Utils string error = $"마지막 노드의 도킹방향과 경로정보의 노드ID 불일치: 필요={LastNode.NodeId}, 계산됨={LastNodeInfo.NodeId }"; System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}"); return DockingValidationResult.CreateInvalid( - targetNodeId, + LastNode.NodeId, LastNode.Type, requiredDirection, LastNodeInfo.MotorDirection, @@ -119,11 +125,11 @@ namespace AGVNavigationCore.Utils } // 검증 수행 - if (LastNodeInfo.MotorDirection == requiredDirection) + if (LastNodeInfo.MotorDirection == requiredDirection && pathResult.DetailedPath[pathResult.DetailedPath.Count - 2].MotorDirection == requiredDirection) { System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공"); return DockingValidationResult.CreateValid( - targetNodeId, + LastNode.NodeId, LastNode.Type, requiredDirection, LastNodeInfo.MotorDirection); @@ -133,7 +139,7 @@ namespace AGVNavigationCore.Utils string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(LastNodeInfo.MotorDirection)}"; System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}"); return DockingValidationResult.CreateInvalid( - targetNodeId, + LastNode.NodeId, LastNode.Type, requiredDirection, LastNodeInfo.MotorDirection, @@ -170,7 +176,7 @@ namespace AGVNavigationCore.Utils /// 경로 기반 최종 방향 계산 /// 개선된 구현: 경로 진행 방향과 목적지 노드 타입을 고려 /// - private static AgvDirection CalculateFinalDirection(List path, List mapNodes, AgvDirection currentDirection) + private static AgvDirection CalculateFinalDirection(List path, List mapNodes, AgvDirection currentDirection) { System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 입력 - 경로 수: {path?.Count}, 현재 방향: {currentDirection}"); @@ -181,13 +187,12 @@ namespace AGVNavigationCore.Utils return currentDirection; } - // 목적지 노드 확인 - var lastNodeId = path[path.Count - 1]; - var lastNode = mapNodes?.FirstOrDefault(n => n.NodeId == lastNodeId); + // 목적지 노드 확인 (Path는 이제 List) + var lastNode = path[path.Count - 1]; if (lastNode == null) { - System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 목적지 노드 찾을 수 없음: {lastNodeId}"); + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 목적지 노드가 null입니다"); return currentDirection; } @@ -208,12 +213,11 @@ namespace AGVNavigationCore.Utils } // 일반 노드인 경우 마지막 구간의 이동 방향 분석 - var secondLastNodeId = path[path.Count - 2]; - var secondLastNode = mapNodes?.FirstOrDefault(n => n.NodeId == secondLastNodeId); + var secondLastNode = path[path.Count - 2]; if (secondLastNode == null) { - System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 이전 노드 찾을 수 없음: {secondLastNodeId}"); + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 이전 노드가 null입니다"); return currentDirection; } @@ -222,7 +226,7 @@ namespace AGVNavigationCore.Utils var deltaY = lastNode.Position.Y - secondLastNode.Position.Y; var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 마지막 구간: {secondLastNodeId} → {lastNodeId}, 벡터: ({deltaX}, {deltaY}), 거리: {distance:F2}"); + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 마지막 구간: {secondLastNode.NodeId} → {lastNode.NodeId}, 벡터: ({deltaX}, {deltaY}), 거리: {distance:F2}"); // 이동 거리가 매우 작으면 현재 방향 유지 if (distance < 1.0) diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs index 53c9cc2..614e513 100644 --- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs +++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs @@ -300,13 +300,19 @@ namespace AGVSimulator.Forms // 현재 AGV 방향 가져오기 var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; - var currentDirection = selectedAGV?.CurrentDirection ?? AgvDirection.Forward; + if(selectedAGV == null) + { + MessageBox.Show("Virtual AGV 가 없습니다", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + var currentDirection = selectedAGV.CurrentDirection; // AGV의 이전 위치에서 가장 가까운 노드 찾기 - var prevNode = selectedAGV?.PrevNode; + var prevNode = selectedAGV.PrevNode; + var prevDir = selectedAGV.PrevDirection; // 고급 경로 계획 사용 (노드 객체 직접 전달) - var advancedResult = _advancedPathfinder.FindPath_test(startNode, targetNode, prevNode, currentDirection); + var advancedResult = _advancedPathfinder.FindPath_test(startNode, targetNode, prevNode, prevDir, currentDirection); if (advancedResult.Success) { diff --git a/Cs_HMI/Data/NewMap.agvmap b/Cs_HMI/Data/NewMap.agvmap index 2950223..df05d4c 100644 --- a/Cs_HMI/Data/NewMap.agvmap +++ b/Cs_HMI/Data/NewMap.agvmap @@ -6,7 +6,9 @@ "Position": "65, 229", "Type": 2, "DockDirection": 2, - "ConnectedNodes": [], + "ConnectedNodes": [ + "N002" + ], "CanRotate": false, "StationId": "", "StationType": null, @@ -24,7 +26,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -37,13 +39,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ + "N003", "N001" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:48.2957516+09:00", - "ModifiedDate": "2025-09-15T10:16:10.1841326+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "002", @@ -56,7 +59,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -69,13 +72,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ + "N004", "N002" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:49.2226656+09:00", - "ModifiedDate": "2025-09-15T10:16:09.1753358+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "003", @@ -88,7 +92,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -98,18 +102,19 @@ "NodeId": "N004", "Name": "N004", "Position": "380, 340", - "Type": 1, + "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N003", "N022", - "N031" + "N031", + "N011", + "N003" ], - "CanRotate": true, + "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:50.1681027+09:00", - "ModifiedDate": "2025-09-15T11:18:47.8876112+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "004", @@ -122,7 +127,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -135,7 +140,8 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N007" + "N007", + "N022" ], "CanRotate": false, "StationId": "", @@ -154,7 +160,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -166,12 +172,15 @@ "Position": "600, 180", "Type": 0, "DockDirection": 0, - "ConnectedNodes": [], + "ConnectedNodes": [ + "N019", + "N006" + ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:51.9266982+09:00", - "ModifiedDate": "2025-09-11T11:46:43.5813583+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "014", @@ -184,7 +193,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -217,7 +226,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -230,13 +239,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N010" + "N010", + "N008" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:54.5035702+09:00", - "ModifiedDate": "2025-09-15T10:16:14.696211+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "010", @@ -249,7 +259,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -261,12 +271,14 @@ "Position": "52, 466", "Type": 2, "DockDirection": 2, - "ConnectedNodes": [], + "ConnectedNodes": [ + "N009" + ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:55.0563237+09:00", - "ModifiedDate": "2025-09-15T11:19:40.1582831+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "011", @@ -279,7 +291,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -293,14 +305,14 @@ "DockDirection": 0, "ConnectedNodes": [ "N012", - "N004", - "N015" + "N015", + "N004" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:55.8875335+09:00", - "ModifiedDate": "2025-09-15T10:16:28.6957855+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "005", @@ -313,7 +325,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -326,13 +338,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N013" + "N013", + "N011" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:56.3678144+09:00", - "ModifiedDate": "2025-09-11T11:46:27.9224943+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "006", @@ -345,7 +358,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -358,13 +371,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N014" + "N014", + "N012" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:56.8390845+09:00", - "ModifiedDate": "2025-09-11T11:46:29.5788308+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "007", @@ -377,7 +391,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -389,12 +403,14 @@ "Position": "720, 580", "Type": 2, "DockDirection": 2, - "ConnectedNodes": [], + "ConnectedNodes": [ + "N013" + ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:34:57.2549726+09:00", - "ModifiedDate": "2025-09-15T11:19:35.3431797+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "008", @@ -407,7 +423,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -426,7 +442,7 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:35:56.5359098+09:00", - "ModifiedDate": "2025-09-15T11:19:49.2931335+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Red", "RfidId": "015", @@ -439,7 +455,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -453,13 +469,14 @@ "DockDirection": 0, "ConnectedNodes": [ "N023", + "N004", "N006" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T08:36:48.0311551+09:00", - "ModifiedDate": "2025-09-15T10:16:22.8799696+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "012", @@ -472,7 +489,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -485,13 +502,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N024" + "N024", + "N022" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T09:41:36.8738794+09:00", - "ModifiedDate": "2025-09-15T10:16:20.0378544+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "016", @@ -504,7 +522,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -517,13 +535,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N025" + "N025", + "N023" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T09:41:37.4551853+09:00", - "ModifiedDate": "2025-09-15T10:16:20.8801598+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "017", @@ -536,7 +555,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -549,13 +568,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N026" + "N026", + "N024" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T09:41:38.0142374+09:00", - "ModifiedDate": "2025-09-15T10:16:21.6723809+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "018", @@ -568,7 +588,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -580,12 +600,14 @@ "Position": "660, 100", "Type": 3, "DockDirection": 1, - "ConnectedNodes": [], + "ConnectedNodes": [ + "N025" + ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-11T09:41:38.5834487+09:00", - "ModifiedDate": "2025-09-15T11:19:58.0225184+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "019", @@ -598,7 +620,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -628,7 +650,7 @@ "ForeColor": "Black", "BackColor": "255, 255, 192", "ShowBackground": true, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -671,13 +693,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N016" + "N016", + "N011" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:47.8065756+09:00", - "ModifiedDate": "2025-09-15T15:40:38.2050196+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "037", @@ -690,7 +713,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -703,13 +726,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N017" + "N017", + "N015" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:48.6628848+09:00", - "ModifiedDate": "2025-09-15T15:40:36.7952276+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "036", @@ -722,7 +746,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -735,13 +759,15 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N018" + "N018", + "N032", + "N016" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:49.8138877+09:00", - "ModifiedDate": "2025-09-15T15:40:35.5342054+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "035", @@ -754,7 +780,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -767,13 +793,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N005" + "N030", + "N017" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:50.6790623+09:00", - "ModifiedDate": "2025-09-15T15:40:33.4719206+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "034", @@ -786,7 +813,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -799,13 +826,15 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ - "N020" + "N020", + "N029", + "N018" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:51.5267199+09:00", - "ModifiedDate": "2025-09-15T15:40:31.7321878+09:00", + "ModifiedDate": "2025-10-27T13:44:43.5601294+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "033", @@ -818,7 +847,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -830,12 +859,16 @@ "Position": "148, 545", "Type": 0, "DockDirection": 0, - "ConnectedNodes": [], + "ConnectedNodes": [ + "N021", + "N028", + "N005" + ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:52.3666114+09:00", - "ModifiedDate": "2025-09-15T15:40:30.1486235+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "032", @@ -848,7 +881,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -861,13 +894,14 @@ "Type": 0, "DockDirection": 0, "ConnectedNodes": [ + "N027", "N020" ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:53.0958619+09:00", - "ModifiedDate": "2025-09-15T15:40:27.7345798+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "031", @@ -880,7 +914,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -899,7 +933,7 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:54.7345704+09:00", - "ModifiedDate": "2025-09-16T16:25:24.8062758+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Green", "RfidId": "041", @@ -912,7 +946,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -931,7 +965,7 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:55.5263512+09:00", - "ModifiedDate": "2025-09-16T16:25:28.6358219+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Green", "RfidId": "040", @@ -944,7 +978,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -963,7 +997,7 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:56.6623294+09:00", - "ModifiedDate": "2025-09-16T16:25:34.5699894+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Green", "RfidId": "039", @@ -976,7 +1010,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -995,7 +1029,7 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:57.5510908+09:00", - "ModifiedDate": "2025-09-16T16:25:40.3838199+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Green", "RfidId": "038", @@ -1008,7 +1042,7 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, @@ -1020,12 +1054,15 @@ "Position": "337, 397", "Type": 0, "DockDirection": 0, - "ConnectedNodes": [], + "ConnectedNodes": [ + "N004", + "N008" + ], "CanRotate": false, "StationId": "", "StationType": null, "CreatedDate": "2025-09-15T11:18:40.5366059+09:00", - "ModifiedDate": "2025-09-15T15:40:24.0443882+09:00", + "ModifiedDate": "2025-10-27T13:44:39.9998473+09:00", "IsActive": true, "DisplayColor": "Blue", "RfidId": "030", @@ -1038,13 +1075,45 @@ "ForeColor": "Black", "BackColor": "Transparent", "ShowBackground": false, - "ImageBase64": null, + "ImageBase64": "", "Scale": "1, 1", "Opacity": 1.0, "Rotation": 0.0, "DisplayText": "N031 - [030]" + }, + { + "NodeId": "N032", + "Name": "", + "Position": "416, 625", + "Type": 0, + "DockDirection": 0, + "ConnectedNodes": [ + "N017" + ], + "CanRotate": false, + "StationId": "", + "StationType": null, + "CreatedDate": "2025-10-27T13:44:12.9351531+09:00", + "ModifiedDate": "2025-10-27T13:44:12.9351531+09:00", + "IsActive": true, + "DisplayColor": "Blue", + "RfidId": "", + "RfidStatus": "정상", + "RfidDescription": "", + "LabelText": "", + "FontFamily": "Arial", + "FontSize": 12.0, + "FontStyle": 0, + "ForeColor": "Black", + "BackColor": "Transparent", + "ShowBackground": false, + "ImageBase64": "", + "Scale": "1, 1", + "Opacity": 1.0, + "Rotation": 0.0, + "DisplayText": "N032" } ], - "CreatedDate": "2025-10-23T13:00:18.6562481+09:00", + "CreatedDate": "2025-10-27T13:44:45.04346+09:00", "Version": "1.0" } \ No newline at end of file