refactor: PathFinding 폴더 구조 개선 및 도킹 에러 기능 추가

- PathFinding 폴더를 Core, Validation, Planning, Analysis로 세분화
- 네임스페이스 정리 및 using 문 업데이트
- UnifiedAGVCanvas에 SetDockingError 메서드 추가
- 도킹 검증 시스템 인프라 구축
- DockingValidator 유틸리티 클래스 추가
- 빌드 오류 수정 및 안정성 개선

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-09-16 14:46:53 +09:00
parent debbf712d4
commit ef72b77f1c
37 changed files with 1085 additions and 2796 deletions

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding.Core;
using AGVNavigationCore.PathFinding.Validation;
namespace AGVNavigationCore.Utils
{
/// <summary>
/// AGV 도킹 방향 검증 유틸리티
/// 경로 계산 후 마지막 도킹 방향이 올바른지 검증
/// </summary>
public static class DockingValidator
{
/// <summary>
/// 경로의 도킹 방향 검증
/// </summary>
/// <param name="pathResult">경로 계산 결과</param>
/// <param name="mapNodes">맵 노드 목록</param>
/// <param name="currentDirection">AGV 현재 방향</param>
/// <returns>도킹 검증 결과</returns>
public static DockingValidationResult ValidateDockingDirection(AGVPathResult pathResult, List<MapNode> mapNodes, AgvDirection currentDirection)
{
// 경로가 없거나 실패한 경우
if (pathResult == null || !pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0)
{
return DockingValidationResult.CreateNotRequired();
}
// 목적지 노드 찾기
string targetNodeId = pathResult.Path[pathResult.Path.Count - 1];
var targetNode = mapNodes?.FirstOrDefault(n => n.NodeId == targetNodeId);
if (targetNode == null)
{
return DockingValidationResult.CreateNotRequired();
}
// 도킹이 필요한 노드 타입인지 확인
if (!IsDockingRequired(targetNode.Type))
{
return DockingValidationResult.CreateNotRequired();
}
// 필요한 도킹 방향 확인
var requiredDirection = GetRequiredDockingDirection(targetNode.Type);
// 경로 기반 최종 방향 계산
var calculatedDirection = CalculateFinalDirection(pathResult.Path, mapNodes, currentDirection);
// 검증 수행
if (calculatedDirection == requiredDirection)
{
return DockingValidationResult.CreateValid(
targetNodeId,
targetNode.Type,
requiredDirection,
calculatedDirection);
}
else
{
string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(calculatedDirection)}";
return DockingValidationResult.CreateInvalid(
targetNodeId,
targetNode.Type,
requiredDirection,
calculatedDirection,
error);
}
}
/// <summary>
/// 도킹이 필요한 노드 타입인지 확인
/// </summary>
private static bool IsDockingRequired(NodeType nodeType)
{
return nodeType == NodeType.Charging || nodeType == NodeType.Docking;
}
/// <summary>
/// 노드 타입에 따른 필요한 도킹 방향 반환
/// </summary>
private static AgvDirection GetRequiredDockingDirection(NodeType nodeType)
{
switch (nodeType)
{
case NodeType.Charging:
return AgvDirection.Forward; // 충전기는 전진 도킹
case NodeType.Docking:
return AgvDirection.Backward; // 일반 도킹은 후진 도킹
default:
return AgvDirection.Forward; // 기본값
}
}
/// <summary>
/// 경로 기반 최종 방향 계산
/// 현재 구현: 간단한 추정 (향후 고도화 가능)
/// </summary>
private static AgvDirection CalculateFinalDirection(List<string> path, List<MapNode> mapNodes, AgvDirection currentDirection)
{
// 경로가 2개 이상일 때만 방향 변화 추정
if (path.Count < 2)
{
return currentDirection;
}
// 마지막 구간의 노드들 찾기
var secondLastNodeId = path[path.Count - 2];
var lastNodeId = path[path.Count - 1];
var secondLastNode = mapNodes?.FirstOrDefault(n => n.NodeId == secondLastNodeId);
var lastNode = mapNodes?.FirstOrDefault(n => n.NodeId == lastNodeId);
if (secondLastNode == null || lastNode == null)
{
return currentDirection;
}
// 마지막 구간의 이동 방향 분석
var deltaX = lastNode.Position.X - secondLastNode.Position.X;
var deltaY = lastNode.Position.Y - secondLastNode.Position.Y;
// 이동 거리가 매우 작으면 현재 방향 유지
var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < 1.0)
{
return currentDirection;
}
// 간단한 방향 추정 (향후 더 정교한 로직으로 개선 가능)
// 현재는 현재 방향을 유지한다고 가정
return currentDirection;
}
/// <summary>
/// 방향을 텍스트로 변환
/// </summary>
private static string GetDirectionText(AgvDirection direction)
{
switch (direction)
{
case AgvDirection.Forward:
return "전진";
case AgvDirection.Backward:
return "후진";
default:
return direction.ToString();
}
}
/// <summary>
/// 도킹 검증 결과를 문자열로 변환 (디버깅용)
/// </summary>
public static string GetValidationSummary(DockingValidationResult validation)
{
if (validation == null)
return "검증 결과 없음";
if (!validation.IsValidationRequired)
return "도킹 검증 불필요";
if (validation.IsValid)
{
return $"도킹 검증 통과: {validation.TargetNodeId}({validation.TargetNodeType}) - {GetDirectionText(validation.RequiredDockingDirection)} 도킹";
}
else
{
return $"도킹 검증 실패: {validation.TargetNodeId}({validation.TargetNodeType}) - {validation.ValidationError}";
}
}
}
}

View File

@@ -31,9 +31,10 @@ namespace AGVNavigationCore.Utils
}
else if (motorDirection == AgvDirection.Backward)
{
// 후진 모터: AGV가 리프트 쪽으로 이동 (현재 → 타겟 방향이 리프트 방향)
var dx = targetPos.X - currentPos.X;
var dy = targetPos.Y - currentPos.Y;
// 후진 모터: AGV가 리프트 쪽으로 이동하므로 리프트는 AGV 이동 방향에 위치
// 007→006 후진시: 리프트는 006방향(이동방향)을 향해야 함 (타겟→현재 반대방향)
var dx = currentPos.X - targetPos.X;
var dy = currentPos.Y - targetPos.Y;
return Math.Atan2(dy, dx);
}
else
@@ -131,7 +132,7 @@ namespace AGVNavigationCore.Utils
if (motorDirection == AgvDirection.Forward)
calculationMethod = "이동방향 + 180도 (전진모터)";
else if (motorDirection == AgvDirection.Backward)
calculationMethod = "이동방향과 동일 (후진모터)";
calculationMethod = "이동방향과 동일 (후진모터 - 리프트는 이동방향에 위치)";
else
calculationMethod = "기본값 (전진모터)";