using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Windows.Forms; using AGVMapEditor.Models; using AGVNavigationCore.Controls; using AGVNavigationCore.Models; using Newtonsoft.Json; namespace AGVMapEditor.Forms { /// /// AGV 맵 에디터 메인 폼 /// public partial class MainForm : Form { #region Fields private List _mapNodes; private UnifiedAGVCanvas _mapCanvas; // 현재 선택된 노드 private MapNode _selectedNode; // 파일 경로 private string _currentMapFile = string.Empty; private bool _hasChanges = false; private bool _hasCommandLineArgs = false; // 노드 연결 정보를 표현하는 클래스 public class NodeConnectionInfo { public string FromNodeId { get; set; } public string FromNodeName { get; set; } public string FromRfidId { get; set; } public string ToNodeId { get; set; } public string ToNodeName { get; set; } public string ToRfidId { get; set; } public string ConnectionType { get; set; } public override string ToString() { // RFID가 있으면 RFID(노드이름), 없으면 NodeID(노드이름) 형태로 표시 string fromDisplay = !string.IsNullOrEmpty(FromRfidId) ? $"{FromRfidId}({FromNodeName})" : $"---({FromNodeId})"; string toDisplay = !string.IsNullOrEmpty(ToRfidId) ? $"{ToRfidId}({ToNodeName})" : $"---({ToNodeId})"; // 양방향 연결은 ↔ 기호 사용 string arrow = ConnectionType == "양방향" ? "↔" : "→"; return $"{fromDisplay} {arrow} {toDisplay}"; } } #endregion #region Constructor public MainForm() : this(null) { } public MainForm(string[] args) { InitializeComponent(); InitializeData(); InitializeMapCanvas(); UpdateTitle(); // 명령줄 인수로 파일이 전달되었으면 자동으로 열기 if (args != null && args.Length > 0) { _hasCommandLineArgs = true; string filePath = args[0]; if (System.IO.File.Exists(filePath)) { LoadMapFromFile(filePath); } else { MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } // 명령줄 인수가 없는 경우는 Form_Load에서 마지막 맵 파일 자동 로드 확인 } #endregion #region Initialization private void InitializeData() { _mapNodes = new List(); } private void InitializeMapCanvas() { _mapCanvas = new UnifiedAGVCanvas(); _mapCanvas.Dock = DockStyle.Fill; _mapCanvas.Nodes = _mapNodes; // RfidMappings 제거 - MapNode에 통합됨 // 이벤트 연결 _mapCanvas.NodeAdded += OnNodeAdded; _mapCanvas.NodeSelected += OnNodeSelected; _mapCanvas.NodeMoved += OnNodeMoved; _mapCanvas.NodeDeleted += OnNodeDeleted; _mapCanvas.ConnectionDeleted += OnConnectionDeleted; _mapCanvas.MapChanged += OnMapChanged; // 스플리터 패널에 맵 캔버스 추가 splitContainer1.Panel2.Controls.Add(_mapCanvas); // 편집 모드 툴바 초기화 InitializeEditModeToolbar(); } private void InitializeEditModeToolbar() { // 툴바 패널 생성 var toolbarPanel = new Panel(); toolbarPanel.Height = 35; toolbarPanel.Dock = DockStyle.Top; toolbarPanel.BackColor = SystemColors.Control; // 선택 모드 버튼 var btnSelect = new Button(); btnSelect.Text = "선택 (S)"; btnSelect.Size = new Size(70, 28); btnSelect.Location = new Point(5, 3); btnSelect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select; // 이동 모드 버튼 var btnMove = new Button(); btnMove.Text = "이동 (M)"; btnMove.Size = new Size(70, 28); btnMove.Location = new Point(80, 3); btnMove.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Move; // 노드 추가 버튼 var btnAddNode = new Button(); btnAddNode.Text = "노드 추가 (A)"; btnAddNode.Size = new Size(80, 28); btnAddNode.Location = new Point(155, 3); btnAddNode.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddNode; // 라벨 추가 버튼 var btnAddLabel = new Button(); btnAddLabel.Text = "라벨 추가 (L)"; btnAddLabel.Size = new Size(80, 28); btnAddLabel.Location = new Point(240, 3); btnAddLabel.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddLabel; // 이미지 추가 버튼 var btnAddImage = new Button(); btnAddImage.Text = "이미지 추가 (I)"; btnAddImage.Size = new Size(90, 28); btnAddImage.Location = new Point(325, 3); btnAddImage.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddImage; // 연결 모드 버튼 var btnConnect = new Button(); btnConnect.Text = "연결 (C)"; btnConnect.Size = new Size(70, 28); btnConnect.Location = new Point(420, 3); btnConnect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Connect; // 삭제 모드 버튼 var btnDelete = new Button(); btnDelete.Text = "삭제 (D)"; btnDelete.Size = new Size(70, 28); btnDelete.Location = new Point(495, 3); btnDelete.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Delete; // 연결 삭제 버튼 var btnDeleteConnection = new Button(); btnDeleteConnection.Text = "연결삭제 (X)"; btnDeleteConnection.Size = new Size(80, 28); btnDeleteConnection.Location = new Point(570, 3); btnDeleteConnection.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.DeleteConnection; // 구분선 var separator1 = new Label(); separator1.Text = "|"; separator1.Size = new Size(10, 28); separator1.Location = new Point(655, 3); separator1.TextAlign = ContentAlignment.MiddleCenter; // 그리드 토글 버튼 var btnToggleGrid = new Button(); btnToggleGrid.Text = "그리드"; btnToggleGrid.Size = new Size(60, 28); btnToggleGrid.Location = new Point(670, 3); btnToggleGrid.Click += (s, e) => _mapCanvas.ShowGrid = !_mapCanvas.ShowGrid; // 맵 맞춤 버튼 var btnFitMap = new Button(); btnFitMap.Text = "맵 맞춤"; btnFitMap.Size = new Size(70, 28); btnFitMap.Location = new Point(735, 3); btnFitMap.Click += (s, e) => _mapCanvas.FitToNodes(); // 툴바에 버튼들 추가 toolbarPanel.Controls.AddRange(new Control[] { btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, btnDeleteConnection, separator1, btnToggleGrid, btnFitMap }); // 스플리터 패널에 툴바 추가 (맨 위에) splitContainer1.Panel2.Controls.Add(toolbarPanel); toolbarPanel.BringToFront(); } #endregion #region Event Handlers private void MainForm_Load(object sender, EventArgs e) { RefreshNodeList(); // 속성 변경 시 이벤트 연결 _propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged; // 명령줄 인수가 없는 경우에만 마지막 맵 파일 자동 로드 확인 if (!_hasCommandLineArgs) { this.Show(); Application.DoEvents(); CheckAndLoadLastMapFile(); } } private void OnNodeAdded(object sender, MapNode node) { _hasChanges = true; UpdateTitle(); RefreshNodeList(); // RFID 자동 할당 } private void OnNodeSelected(object sender, MapNode node) { _selectedNode = node; UpdateNodeProperties(); } private void OnNodeMoved(object sender, MapNode node) { _hasChanges = true; UpdateTitle(); RefreshNodeList(); } private void OnNodeDeleted(object sender, MapNode node) { _hasChanges = true; UpdateTitle(); RefreshNodeList(); ClearNodeProperties(); // RFID 자동 할당 } private void OnConnectionCreated(object sender, (MapNode From, MapNode To) connection) { _hasChanges = true; UpdateTitle(); UpdateNodeProperties(); // 연결 정보 업데이트 } private void OnConnectionDeleted(object sender, (MapNode From, MapNode To) connection) { _hasChanges = true; UpdateTitle(); RefreshNodeConnectionList(); UpdateNodeProperties(); // 연결 정보 업데이트 } private void OnMapChanged(object sender, EventArgs e) { _hasChanges = true; UpdateTitle(); } private void OnBackgroundClicked(object sender, Point location) { _selectedNode = null; ClearNodeProperties(); } #endregion #region ToolStrip Button Event Handlers private void btnNew_Click(object sender, EventArgs e) { if (CheckSaveChanges()) { NewMap(); } } private void btnOpen_Click(object sender, EventArgs e) { if (CheckSaveChanges()) { OpenMap(); } } private void btnReopen_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(_currentMapFile)) { MessageBox.Show("다시 열 파일이 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (!File.Exists(_currentMapFile)) { MessageBox.Show($"파일을 찾을 수 없습니다: {_currentMapFile}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } if (CheckSaveChanges()) { LoadMapFromFile(_currentMapFile); UpdateStatusBar($"파일을 다시 열었습니다: {Path.GetFileName(_currentMapFile)}"); } } private void btnClose_Click(object sender, EventArgs e) { CloseMap(); } private void btnSave_Click(object sender, EventArgs e) { SaveMap(); } private void btnSaveAs_Click(object sender, EventArgs e) { SaveAsMap(); } private void btnExit_Click(object sender, EventArgs e) { this.Close(); } #endregion #region Keyboard Shortcuts protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { switch (keyData) { case Keys.Control | Keys.N: btnNew_Click(null, null); return true; case Keys.Control | Keys.O: btnOpen_Click(null, null); return true; case Keys.Control | Keys.S: btnSave_Click(null, null); return true; } return base.ProcessCmdKey(ref msg, keyData); } #endregion #region Button Event Handlers private void btnAddNode_Click(object sender, EventArgs e) { AddNewNode(); } private void btnDeleteNode_Click(object sender, EventArgs e) { DeleteSelectedNode(); } private void btnAddConnection_Click(object sender, EventArgs e) { AddConnectionToSelectedNode(); } private void btnRemoveConnection_Click(object sender, EventArgs e) { RemoveConnectionFromSelectedNode(); } #endregion #region Node Management private void AddNewNode() { var nodeId = GenerateNodeId(); var nodeName = $"노드{_mapNodes.Count + 1}"; var position = new Point(100 + _mapNodes.Count * 50, 100 + _mapNodes.Count * 50); var node = new MapNode(nodeId, nodeName, position, NodeType.Normal); _mapNodes.Add(node); _hasChanges = true; RefreshNodeList(); RefreshMapCanvas(); UpdateTitle(); } private void DeleteSelectedNode() { if (_selectedNode == null) { MessageBox.Show("삭제할 노드를 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var result = MessageBox.Show($"노드 '{_selectedNode.Name}'를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.", "삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { // 노드 제거 _mapNodes.Remove(_selectedNode); _selectedNode = null; _hasChanges = true; RefreshNodeList(); RefreshMapCanvas(); ClearNodeProperties(); UpdateTitle(); } } private void AddConnectionToSelectedNode() { if (_selectedNode == null) { MessageBox.Show("연결을 추가할 노드를 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 다른 노드들 중에서 선택 var availableNodes = _mapNodes.Where(n => n.NodeId != _selectedNode.NodeId && !_selectedNode.ConnectedNodes.Contains(n.NodeId)).ToList(); if (availableNodes.Count == 0) { MessageBox.Show("연결 가능한 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 간단한 선택 다이얼로그 (실제로는 별도 폼을 만들어야 함) var nodeNames = availableNodes.Select(n => $"{n.NodeId}: {n.Name}").ToArray(); var input = Microsoft.VisualBasic.Interaction.InputBox("연결할 노드를 선택하세요:", "노드 연결", nodeNames[0]); var targetNode = availableNodes.FirstOrDefault(n => input.StartsWith(n.NodeId)); if (targetNode != null) { _selectedNode.AddConnection(targetNode.NodeId); _hasChanges = true; RefreshMapCanvas(); UpdateNodeProperties(); UpdateTitle(); } } private void RemoveConnectionFromSelectedNode() { if (_selectedNode == null || _selectedNode.ConnectedNodes.Count == 0) { MessageBox.Show("연결을 제거할 노드를 선택하거나 연결된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 연결된 노드들 중에서 선택 var connectedNodeNames = _selectedNode.ConnectedNodes.Select(connectedNodeId => { var node = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); return node != null ? $"{node.NodeId}: {node.Name}" : connectedNodeId; }).ToArray(); var input = Microsoft.VisualBasic.Interaction.InputBox("제거할 연결을 선택하세요:", "연결 제거", connectedNodeNames[0]); var targetNodeId = input.Split(':')[0]; if (_selectedNode.ConnectedNodes.Contains(targetNodeId)) { _selectedNode.RemoveConnection(targetNodeId); _hasChanges = true; RefreshMapCanvas(); UpdateNodeProperties(); UpdateTitle(); } } private string GenerateNodeId() { int counter = 1; string nodeId; do { nodeId = $"N{counter:D3}"; counter++; } while (_mapNodes.Any(n => n.NodeId == nodeId)); return nodeId; } #endregion #region File Operations private void NewMap() { _mapNodes.Clear(); _selectedNode = null; _currentMapFile = string.Empty; _hasChanges = false; RefreshAll(); UpdateTitle(); } private void CloseMap() { if (CheckSaveChanges()) { _mapNodes.Clear(); _selectedNode = null; _currentMapFile = string.Empty; _hasChanges = false; RefreshAll(); UpdateTitle(); } } private void OpenMap() { var openFileDialog = new OpenFileDialog { Filter = "AGV Map Files (*.agvmap)|*.agvmap|All Files (*.*)|*.*", DefaultExt = "agvmap" }; if (openFileDialog.ShowDialog() == DialogResult.OK) { try { LoadMapFromFile(openFileDialog.FileName); _currentMapFile = openFileDialog.FileName; _hasChanges = false; RefreshAll(); UpdateTitle(); } catch (Exception ex) { MessageBox.Show($"맵 로드 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void SaveMap() { if (string.IsNullOrEmpty(_currentMapFile)) { SaveAsMap(); } else { try { SaveMapToFile(_currentMapFile); _hasChanges = false; UpdateTitle(); MessageBox.Show("맵이 성공적으로 저장되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"맵 저장 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void SaveAsMap() { var saveFileDialog = new SaveFileDialog { Filter = "AGV Map Files (*.agvmap)|*.agvmap", DefaultExt = "agvmap", FileName = "NewMap.agvmap" }; if (saveFileDialog.ShowDialog() == DialogResult.OK) { try { SaveMapToFile(saveFileDialog.FileName); _currentMapFile = saveFileDialog.FileName; _hasChanges = false; UpdateTitle(); MessageBox.Show("맵이 성공적으로 저장되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"맵 저장 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void LoadMapFromFile(string filePath) { var result = MapLoader.LoadMapFromFile(filePath); if (result.Success) { _mapNodes = result.Nodes; // 맵 캔버스에 데이터 설정 _mapCanvas.Nodes = _mapNodes; // RfidMappings 제거됨 - MapNode에 통합 // 현재 파일 경로 업데이트 _currentMapFile = filePath; _hasChanges = false; // 설정에 마지막 맵 파일 경로 저장 EditorSettings.Instance.UpdateLastMapFile(filePath); UpdateTitle(); UpdateNodeList(); RefreshNodeConnectionList(); // 맵 로드 후 자동으로 맵에 맞춤 _mapCanvas.FitToNodes(); UpdateStatusBar($"맵 파일을 성공적으로 로드했습니다: {Path.GetFileName(filePath)}"); } else { MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void SaveMapToFile(string filePath) { if (MapLoader.SaveMapToFile(filePath, _mapNodes)) { // 현재 파일 경로 업데이트 _currentMapFile = filePath; _hasChanges = false; // 설정에 마지막 맵 파일 경로 저장 EditorSettings.Instance.UpdateLastMapFile(filePath); UpdateTitle(); UpdateStatusBar($"맵 파일을 성공적으로 저장했습니다: {Path.GetFileName(filePath)}"); } else { MessageBox.Show("맵 파일 저장 실패", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// RFID 매핑 업데이트 (공용 MapLoader 사용) /// private void UpdateRfidMappings() { // RFID 자동 할당 제거 - 사용자가 직접 입력한 값 유지 // MapLoader.AssignAutoRfidIds(_mapNodes); } private bool CheckSaveChanges() { if (_hasChanges) { var result = MessageBox.Show("변경사항이 있습니다. 저장하시겠습니까?", "변경사항 저장", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); if (result == DialogResult.Yes) { SaveMap(); return !_hasChanges; // 저장이 성공했으면 true } else if (result == DialogResult.Cancel) { return false; } } return true; } /// /// 마지막 맵 파일이 있는지 확인하고 사용자에게 로드할지 물어봄 /// private void CheckAndLoadLastMapFile() { var settings = EditorSettings.Instance; if (settings.AutoLoadLastMapFile && settings.HasValidLastMapFile()) { string fileName = Path.GetFileName(settings.LastMapFilePath); var result = MessageBox.Show( $"마지막으로 사용한 맵 파일을 찾았습니다:\n\n{fileName}\n\n이 파일을 열까요?", "마지막 맵 파일 로드", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { LoadMapFromFile(settings.LastMapFilePath); } } } #endregion #region UI Updates private void RefreshAll() { RefreshNodeList(); RefreshNodeConnectionList(); RefreshMapCanvas(); ClearNodeProperties(); } private void RefreshNodeList() { listBoxNodes.DataSource = null; listBoxNodes.DataSource = _mapNodes; listBoxNodes.DisplayMember = "DisplayText"; listBoxNodes.ValueMember = "NodeId"; // 노드 목록 클릭 이벤트 연결 listBoxNodes.SelectedIndexChanged -= ListBoxNodes_SelectedIndexChanged; listBoxNodes.SelectedIndexChanged += ListBoxNodes_SelectedIndexChanged; // 노드 타입별 색상 적용 listBoxNodes.DrawMode = DrawMode.OwnerDrawFixed; listBoxNodes.DrawItem -= ListBoxNodes_DrawItem; listBoxNodes.DrawItem += ListBoxNodes_DrawItem; } private void ListBoxNodes_SelectedIndexChanged(object sender, EventArgs e) { if (listBoxNodes.SelectedItem is MapNode selectedNode) { _selectedNode = selectedNode; UpdateNodeProperties(); // 맵 캔버스에서도 선택된 노드 표시 if (_mapCanvas != null) { _mapCanvas.Invalidate(); } } } private void ListBoxNodes_DrawItem(object sender, DrawItemEventArgs e) { e.DrawBackground(); if (e.Index >= 0 && e.Index < _mapNodes.Count) { var node = _mapNodes[e.Index]; // 노드 타입에 따른 색상 설정 Color foreColor = Color.Black; Color backColor = e.BackColor; if ((e.State & DrawItemState.Selected) == DrawItemState.Selected) { backColor = SystemColors.Highlight; foreColor = SystemColors.HighlightText; } else { switch (node.Type) { case NodeType.Normal: foreColor = Color.Black; backColor = Color.White; break; case NodeType.Rotation: foreColor = Color.DarkOrange; backColor = Color.LightYellow; break; case NodeType.Docking: foreColor = Color.DarkGreen; backColor = Color.LightGreen; break; case NodeType.Charging: foreColor = Color.DarkRed; backColor = Color.LightPink; break; } } // 배경 그리기 using (var brush = new SolidBrush(backColor)) { e.Graphics.FillRectangle(brush, e.Bounds); } // 텍스트 그리기 (노드ID - 노드명 - RFID 순서) var displayText = node.NodeId; if (!string.IsNullOrEmpty(node.Name)) { displayText += $" - {node.Name}"; } if (!string.IsNullOrEmpty(node.RfidId)) { displayText += $" - [{node.RfidId}]"; } using (var brush = new SolidBrush(foreColor)) { e.Graphics.DrawString(displayText, e.Font, brush, e.Bounds.X + 2, e.Bounds.Y + 2); } } e.DrawFocusRectangle(); } private void RefreshNodeConnectionList() { var connections = new List(); var processedPairs = new HashSet(); // 모든 노드의 연결 정보를 수집 (중복 방지) foreach (var fromNode in _mapNodes) { foreach (var toNodeId in fromNode.ConnectedNodes) { var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId); if (toNode != null) { // 중복 체크 (단일 연결만 표시) string pairKey1 = $"{fromNode.NodeId}-{toNode.NodeId}"; string pairKey2 = $"{toNode.NodeId}-{fromNode.NodeId}"; if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2)) { // 사전 순으로 정렬하여 일관성 있게 표시 var (firstNode, secondNode) = string.Compare(fromNode.NodeId, toNode.NodeId) < 0 ? (fromNode, toNode) : (toNode, fromNode); connections.Add(new NodeConnectionInfo { FromNodeId = firstNode.NodeId, FromNodeName = firstNode.Name, FromRfidId = firstNode.RfidId, ToNodeId = secondNode.NodeId, ToNodeName = secondNode.Name, ToRfidId = secondNode.RfidId, ConnectionType = "양방향" // 모든 연결이 양방향 }); processedPairs.Add(pairKey1); processedPairs.Add(pairKey2); } } } } // 리스트박스에 표시 lstNodeConnection.DataSource = null; lstNodeConnection.DataSource = connections; lstNodeConnection.DisplayMember = "ToString"; // 리스트박스 클릭 이벤트 연결 lstNodeConnection.SelectedIndexChanged -= LstNodeConnection_SelectedIndexChanged; lstNodeConnection.SelectedIndexChanged += LstNodeConnection_SelectedIndexChanged; // 더블클릭 이벤트 연결 (연결 삭제) lstNodeConnection.DoubleClick -= LstNodeConnection_DoubleClick; lstNodeConnection.DoubleClick += LstNodeConnection_DoubleClick; } private void LstNodeConnection_SelectedIndexChanged(object sender, EventArgs e) { if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo) { // 캔버스에서 해당 연결선 강조 표시 _mapCanvas?.HighlightConnection(connectionInfo.FromNodeId, connectionInfo.ToNodeId); // 연결된 노드들을 맵에서 하이라이트 표시 (선택적) var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId); if (fromNode != null) { _selectedNode = fromNode; UpdateNodeProperties(); _mapCanvas?.Invalidate(); } } else { // 선택 해제 시 강조 표시 제거 _mapCanvas?.ClearHighlightedConnection(); } } private void LstNodeConnection_DoubleClick(object sender, EventArgs e) { // 더블클릭으로 연결 삭제 DeleteSelectedConnection(); } private void RefreshMapCanvas() { _mapCanvas?.Invalidate(); } private void UpdateNodeProperties() { if (_selectedNode == null) { ClearNodeProperties(); return; } // 노드 래퍼 객체 생성 (타입에 따라 다른 래퍼 사용) var nodeWrapper = NodePropertyWrapperFactory.CreateWrapper(_selectedNode, _mapNodes); _propertyGrid.SelectedObject = nodeWrapper; _propertyGrid.Focus(); } private void ClearNodeProperties() { _propertyGrid.SelectedObject = null; } private void UpdateTitle() { var title = "AGV Map Editor"; if (!string.IsNullOrEmpty(_currentMapFile)) { title += $" - {Path.GetFileName(_currentMapFile)}"; } if (_hasChanges) { title += " *"; } this.Text = title; } /// /// 노드 목록을 업데이트 /// private void UpdateNodeList() { if (listBoxNodes != null) { listBoxNodes.DataSource = null; listBoxNodes.DataSource = _mapNodes; listBoxNodes.DisplayMember = "DisplayText"; } } /// /// 상태바에 메시지 표시 /// /// 표시할 메시지 private void UpdateStatusBar(string message) { if (toolStripStatusLabel1 != null) { toolStripStatusLabel1.Text = message; } } #endregion #region Form Events private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (!CheckSaveChanges()) { e.Cancel = true; } } #endregion #region PropertyGrid private void PropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e) { // RFID 값 변경시 중복 검사 if (e.ChangedItem.PropertyDescriptor.Name == "RFID") { string newRfidValue = e.ChangedItem.Value?.ToString(); if (!string.IsNullOrEmpty(newRfidValue) && CheckRfidDuplicate(newRfidValue)) { // 중복된 RFID 값 발견 MessageBox.Show($"RFID 값 '{newRfidValue}'이(가) 이미 다른 노드에서 사용 중입니다.\n입력값을 되돌립니다.", "RFID 중복 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning); // 원래 값으로 되돌리기 - PropertyGrid의 SelectedObject 사용 e.ChangedItem.PropertyDescriptor.SetValue(_propertyGrid.SelectedObject, e.OldValue); _propertyGrid.Refresh(); return; } } // 속성이 변경되었을 때 자동으로 변경사항 표시 _hasChanges = true; UpdateTitle(); // 현재 선택된 노드를 기억 var currentSelectedNode = _selectedNode; RefreshNodeList(); RefreshMapCanvas(); // 선택된 노드를 다시 선택 if (currentSelectedNode != null) { var nodeIndex = _mapNodes.IndexOf(currentSelectedNode); if (nodeIndex >= 0) { listBoxNodes.SelectedIndex = nodeIndex; } } } /// /// RFID 값 중복 검사 /// /// 검사할 RFID 값 /// 중복되면 true, 아니면 false private bool CheckRfidDuplicate(string rfidValue) { if (string.IsNullOrEmpty(rfidValue) || _mapNodes == null) return false; // 현재 편집 중인 노드 제외하고 중복 검사 string currentNodeId = null; var selectedObject = _propertyGrid.SelectedObject; // 다양한 PropertyWrapper 타입 처리 if (selectedObject is NodePropertyWrapper nodeWrapper) { currentNodeId = nodeWrapper.WrappedNode?.NodeId; } else if (selectedObject is LabelNodePropertyWrapper labelWrapper) { currentNodeId = labelWrapper.WrappedNode?.NodeId; } else if (selectedObject is ImageNodePropertyWrapper imageWrapper) { currentNodeId = imageWrapper.WrappedNode?.NodeId; } int duplicateCount = 0; foreach (var node in _mapNodes) { // 현재 편집 중인 노드는 제외 if (node.NodeId == currentNodeId) continue; // 같은 RFID 값을 가진 노드가 있는지 확인 if (!string.IsNullOrEmpty(node.RfidId) && node.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase)) { duplicateCount++; break; // 하나라도 발견되면 중복 } } return duplicateCount > 0; } #endregion #region Data Model for Serialization #endregion private void btNodeRemove_Click(object sender, EventArgs e) { DeleteSelectedConnection(); } private void DeleteSelectedConnection() { if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo) { var result = MessageBox.Show( $"다음 연결을 삭제하시겠습니까?\n{connectionInfo}", "연결 삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { // 단일 연결 삭제 var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId); var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.ToNodeId); if (fromNode != null && toNode != null) { // 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제) if (fromNode.ConnectedNodes.Contains(toNode.NodeId)) { fromNode.RemoveConnection(toNode.NodeId); } else if (toNode.ConnectedNodes.Contains(fromNode.NodeId)) { toNode.RemoveConnection(fromNode.NodeId); } _hasChanges = true; RefreshNodeConnectionList(); RefreshMapCanvas(); UpdateNodeProperties(); UpdateTitle(); toolStripStatusLabel1.Text = $"연결 삭제됨: {connectionInfo.FromNodeId} ↔ {connectionInfo.ToNodeId}"; } } } else { MessageBox.Show("삭제할 연결을 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); } } } }