alarm class 정리

This commit is contained in:
logonkhi
2025-07-08 19:17:32 +09:00
parent 551a08e0fe
commit efffdb1ecc
38 changed files with 2483 additions and 1172 deletions

View File

@@ -0,0 +1,97 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace UVC.Factory.Alarm
{
/// <summary>
/// 여러 개의 알람이 한 곳에 발생했을 때 이를 묶어서 표시하는 '클러스터' 아이콘을 관리하는 클래스입니다.
/// 아이콘에는 총 알람 개수가 표시되며, 클릭 시 개별 알람을 펼쳐보는 기능을 트리거합니다.
/// </summary>
public class AlarmClusterIcon : MonoBehaviour
{
[Tooltip("클릭 가능한 UI 버튼입니다. 클러스터를 나타냅니다.")]
[SerializeField] private Button button;
// 알람 개수를 표시할 TextMeshPro 컴포넌트입니다.
private TextMeshProUGUI buttonText;
/// <summary>
/// 클러스터 아이콘에 표시될 알람 개수 텍스트를 가져오거나 설정하는 프로퍼티입니다.
/// </summary>
public string AlarmCount
{
get => buttonText != null ? buttonText.text : string.Empty;
set
{
if (buttonText != null)
{
buttonText.text = value;
}
}
}
/// <summary>
/// 이 클러스터 아이콘이 클릭되었을 때 호출될 이벤트를 정의합니다.
/// 상위 관리자인 AlarmIconManager가 이 이벤트를 구독하여 클러스터를 펼치는 동작을 수행합니다.
/// </summary>
public Action OnClickHandler;
/// <summary>
/// 컴포넌트가 생성될 때 가장 먼저 호출되는 Unity 생명주기 메서드입니다.
/// </summary>
private void Awake()
{
// 자식 오브젝트에서 TextMeshProUGUI 컴포넌트를 찾아 할당합니다.
buttonText = GetComponentInChildren<TextMeshProUGUI>();
if (buttonText == null)
{
Debug.LogWarning("Text component not found in children.", this);
}
}
/// <summary>
/// 첫 번째 프레임 업데이트 전에 호출되는 Unity 생명주기 메서드입니다.
/// </summary>
private void Start()
{
if (button != null)
{
// 버튼의 onClick 이벤트에 HandleClick 메서드를 리스너로 등록합니다.
button.onClick.AddListener(HandleClick);
}
else
{
Debug.LogWarning("Button is not assigned.");
}
}
/// <summary>
/// 버튼 클릭 이벤트가 발생했을 때 실행되는 메서드입니다.
/// </summary>
private void HandleClick()
{
// OnClickHandler 이벤트에 등록된 메서드가 있다면 호출합니다.
OnClickHandler?.Invoke();
}
/// <summary>
/// 이 컴포넌트가 파괴될 때 호출되는 Unity 생명주기 메서드입니다.
/// </summary>
private void OnDestroy()
{
if (button != null)
{
// Start에서 등록했던 클릭 이벤트 리스너를 제거하여 메모리 누수를 방지합니다.
button.onClick.RemoveListener(HandleClick);
}
else
{
Debug.LogWarning("Button is not assigned.");
}
// 이벤트 핸들러 참조를 정리합니다.
OnClickHandler = null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 287d101765c6b454c9827e68416792fc

View File

@@ -0,0 +1,94 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Data;
namespace UVC.Factory.Alarm
{
/// <summary>
/// 알람의 상세 정보를 표시하는 UI를 관리하는 클래스입니다.
/// DataObject에 담긴 모든 키-값 쌍을 리스트 형태로 보여주는 역할을 합니다.
/// </summary>
public class AlarmDetailView : MonoBehaviour
{
[Tooltip("알람 상세 내용이 표시될 TextMeshPro UI 컴포넌트입니다.")]
[SerializeField] private TextMeshProUGUI detailTxt;
[Tooltip("상세 정보창을 닫기 위한 버튼 UI 컴포넌트입니다.")]
[SerializeField] private Button closeButton;
/// <summary>
/// 컴포넌트가 처음 활성화될 때 호출되는 Unity 생명주기 메서드입니다.
/// UI의 초기 상태를 설정합니다.
/// </summary>
private void Awake()
{
// 닫기 버튼에 클릭 이벤트 리스너를 추가합니다. 버튼을 클릭하면 Hide() 메서드가 호출됩니다.
closeButton.onClick.AddListener(Hide);
// 초기에는 상세 정보창이 보이지 않도록 비활성화합니다.
gameObject.SetActive(false);
}
/// <summary>
/// 알람 데이터를 받아와 상세 정보 UI에 표시하고, 창을 활성화합니다.
/// </summary>
/// <param name="data">표시할 알람의 모든 정보가 담긴 DataObject</param>
public void Show(DataObject data)
{
string combinedString = string.Empty;
// DataObject에 있는 모든 키-값 쌍을 순회합니다.
foreach (var kvp in data)
{
// <indent> 태그를 사용하여 줄바꿈 시에도 정렬이 유지되도록 합니다.
//combinedString += $"{kvp.Key}<pos=40%><indent=40%>{kvp.Value ?? "null"}</indent>\n";
// <margin-right=60%>: 키가 텍스트 컨테이너 너비의 40%를 넘지 않도록 제한합니다.
// 키가 길면 이 지점에서 자동으로 줄바꿈됩니다.
// <pos=40%>: 값의 시작 위치를 40% 지점으로 지정합니다.
// <margin-right=0>: 다음 줄에 영향을 주지 않도록 마진을 리셋합니다.
//combinedString += $"<margin-right=60%>{kvp.Key}</margin-right><pos=40%><margin-right=0>{kvp.Value ?? "null"}</margin-right>\n";
// <width=40%>: 키 텍스트의 너비를 전체의 40%로 제한합니다. 키가 이 너비를 초과하면 자동으로 줄바꿈됩니다.
// <pos=40%>: 값 텍스트의 시작 위치를 40% 지점으로 설정합니다.
combinedString += $"<width=40%>{kvp.Key}</width><pos=40%><valign='top'><indent=40%>{kvp.Value ?? "null"}</indent></valign>\n";
}
combinedString = combinedString.TrimEnd('\n'); // 마지막 줄바꿈 제거
detailTxt.text = combinedString;
// 내용의 길이에 맞게 상세 정보창의 높이를 동적으로 조절합니다.
RectTransform rect = GetComponent<RectTransform>();
if (rect != null && detailTxt != null)
{
RectTransform textRect = detailTxt.GetComponent<RectTransform>();
// 현재 배경(rect)의 높이와 텍스트(textRect) 영역 높이의 차이를 계산하여 상하 여백(margin)을 구합니다.
float marginHeight = rect.rect.height - textRect.rect.height;
// 배경의 높이를 '텍스트의 실제 높이(preferredHeight)' + '상하 여백'으로 설정하여 내용이 잘리지 않게 합니다.
rect.sizeDelta = new Vector2(rect.rect.width, detailTxt.preferredHeight + marginHeight);
}
}
/// <summary>
/// 상세 정보 UI를 비활성화하여 화면에서 숨깁니다.
/// </summary>
public void Hide()
{
gameObject.SetActive(false);
}
/// <summary>
/// 이 컴포넌트가 파괴될 때 호출되는 Unity 생명주기 메서드입니다.
/// 이벤트 리스너를 제거하여 메모리 누수를 방지합니다.
/// </summary>
private void OnDestroy()
{
if (closeButton != null)
{
// Awake에서 등록했던 클릭 이벤트 리스너를 제거합니다.
closeButton.onClick.RemoveListener(Hide);
}
else
{
Debug.LogWarning("Detail Close Button is not assigned.");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4bdd056a90a112140b16875c3162bbea

View File

@@ -0,0 +1,448 @@
using DG.Tweening;
using System.Collections.Generic;
using UnityEngine;
using UVC.Data;
using UVC.Factory.Component;
namespace UVC.Factory.Alarm
{
/// <summary>
/// 특정 설비(FactoryObject)에 연결된 알람 아이콘 UI를 관리하는 클래스입니다.
/// 알람이 하나일 때는 단일 아이콘, 여러 개일 때는 클러스터(묶음) 아이콘을 표시합니다.
/// 클러스터 아이콘을 클릭하면 개별 알람 아이콘들을 원형으로 펼쳐 보여주는 기능을 담당합니다.
/// </summary>
public class AlarmIconManager : MonoBehaviour
{
[Header("UI Components")]
[Tooltip("알람이 여러 개일 때 표시될 묶음 아이콘 UI입니다.")]
[SerializeField] private AlarmClusterIcon clusterView;
[Tooltip("알람 아이콘들이 펼쳐질 때 부모가 될 UI 영역입니다.")]
[SerializeField] private GameObject expandedView;
[Tooltip("개별 알람을 표시할 아이콘의 프리팹입니다.")]
[SerializeField] private GameObject alarmSingleIconManagerPrefab; // 개별 알람 아이콘
[Header("UI Layout Settings")]
[Tooltip("3D 설비 객체와 UI 아이콘 사이의 수직(Y) 간격입니다.")]
[SerializeField] private float objectYOffset = 0f;
[Tooltip("아이콘을 펼쳤을 때 개별 아이콘 사이의 간격입니다.")]
[SerializeField] private float iconSpacing = 20f;
[Tooltip("아이콘을 펼쳤을 때의 중심점 X축 오프셋입니다.")]
[SerializeField] private float expandOffsetX = 0f;
[Tooltip("아이콘을 펼쳤을 때의 중심점 Y축 오프셋입니다.")]
[SerializeField] private float expandOffsetY = 0f;
// 이 UI가 따라다닐 3D 설비 객체의 Transform입니다.
private Transform targetObject;
// 이 UI가 관리하는 모든 알람 데이터의 리스트입니다.
private List<DataObject> alarms = new List<DataObject>();
// 현재 클러스터가 펼쳐진 상태인지 여부를 나타내는 플래그입니다.
private bool isExpanded = false;
// 알람이 하나일 때 사용되는 단일 알람 아이콘의 인스턴스입니다.
private AlarmSigleIconManager singleAlarmIcon1 = null;
// UI 위치 계산에 필요한 컴포넌트들입니다.
private RectTransform rectTransform;
private Canvas canvas;
// UI 애니메이션을 위한 DoTween 핸들러입니다.
private Tweener uiSpacingTweener;
// 사용자가 클러스터를 클릭하여 줌인했는지 상태를 추적하는 변수입니다.
private bool isZoomIn = false;
/// <summary>
/// MonoBehaviour가 활성화될 때 호출되는 Unity 생명주기 메서드입니다.
/// UI 위치 계산에 필요한 RectTransform과 Canvas 컴포넌트를 찾아 저장합니다.
/// </summary>
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvas = GetComponentInParent<Canvas>();
}
/// <summary>
/// AlarmIconManager를 초기화합니다. AlarmManager에 의해 호출됩니다.
/// </summary>
/// <param name="target">알람이 발생한 3D 설비 객체</param>
/// <param name="initialAlarm">최초로 추가될 알람 데이터</param>
public void Initialize(FactoryObject target, DataObject initialAlarm)
{
this.targetObject = target.transform;
AddAlarm(initialAlarm); // 첫 알람을 리스트에 추가
if (clusterView == null || expandedView == null || alarmSingleIconManagerPrefab == null)
{
Debug.LogError("필수 UI 컴포넌트가 할당되지 않았습니다. AlarmUIController를 확인하세요.");
return;
}
// 클러스터 아이콘 클릭 시 OnPointerClick 메서드가 호출되도록 이벤트 핸들러를 등록합니다.
clusterView.OnClickHandler += OnPointerClick;
// 카메라 위치가 변경될 때마다 OnCameraPositionChangedHandler가 호출되도록 등록합니다.
FactoryCameraController.Instance.OnCameraPositionChanged += OnCameraPositionChangedHandler;
}
/// <summary>
/// 매 프레임의 마지막에 호출되는 Unity 생명주기 메서드입니다.
/// 3D 객체의 위치를 2D 스크린 좌표로 변환하여 UI 아이콘이 객체를 따라다니도록 위치를 업데이트합니다.
/// </summary>
void LateUpdate()
{
if (targetObject == null || Camera.main == null || canvas == null)
{
// 필수 객체가 없으면 모든 UI를 비활성화하고 종료합니다.
if (gameObject.activeSelf) gameObject.SetActive(false);
return;
}
// 카메라의 정면 방향과 카메라에서 타겟을 향하는 방향 벡터를 계산합니다.
Vector3 cameraForward = Camera.main.transform.forward;
Vector3 toTarget = (targetObject.position - Camera.main.transform.position).normalized;
// 두 벡터의 내적(Dot product)을 계산하여 타겟 객체가 카메라 앞에 있는지 확인합니다.
// 내적 값이 0보다 크면 객체가 카메라 시야각 내에 있다는 의미입니다.
if (Vector3.Dot(cameraForward, toTarget) > 0)
{
// 객체가 카메라 앞에 있을 때만 UI를 활성화하고 위치를 업데이트합니다.
if (!gameObject.activeSelf)
{
gameObject.SetActive(true);
UpdateView(); // 비활성화 상태였다면 뷰를 다시 활성화합니다.
}
// 3D 월드 좌표를 2D 스크린 좌표로 변환합니다.
Vector3 screenPoint = Camera.main.WorldToScreenPoint(targetObject.position + Vector3.up);
// 캔버스의 렌더 모드에 따라 좌표 변환 방식을 다르게 처리합니다.
// Canvas Render Mode가 Screen Space - Camera일 경우
if (canvas.renderMode == RenderMode.ScreenSpaceCamera)
{
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, screenPoint, canvas.worldCamera, out localPoint);
localPoint.y += objectYOffset; // Y축 오프셋 적용
rectTransform.localPosition = localPoint;
}
// Canvas Render Mode가 Screen Space - Overlay일 경우
else
{
screenPoint.y += objectYOffset; // Y축 오프셋 적용
rectTransform.position = screenPoint;
}
}
else
{
// 타겟이 카메라 뒤에 있으면 UI를 비활성화합니다.
if (gameObject.activeSelf) gameObject.SetActive(false);
//if (clusterView.gameObject.activeSelf) clusterView.gameObject.SetActive(false);
//if (expandedView.activeSelf) expandedView.SetActive(false);
//if (singleAlarmIcon1 != null && singleAlarmIcon1.gameObject.activeSelf) singleAlarmIcon1.gameObject.SetActive(false);
}
}
/// <summary>
/// 이 매니저가 특정 알람을 이미 관리하고 있는지 확인합니다.
/// </summary>
/// <param name="alarm">확인할 알람 데이터</param>
/// <returns>포함하고 있으면 true, 아니면 false</returns>
public bool ContainsAlarm(DataObject alarm)
{
return alarms.Exists(a => a.Id == alarm.Id);
}
/// <summary>
/// 새로운 알람을 추가합니다.
/// </summary>
/// <param name="alarm">추가할 알람 데이터</param>
public void AddAlarm(DataObject alarm)
{
alarms.Add(alarm);
UpdateView(); // 알람 리스트가 변경되었으므로 뷰를 업데이트합니다.
}
/// <summary>
/// 기존 알람의 정보를 업데이트합니다.
/// </summary>
/// <param name="alarm">업데이트할 알람 데이터</param>
public void UpdateAlarm(DataObject alarm)
{
// 실제로는 alarmId로 찾아서 업데이트해야 함
int index = alarms.FindIndex(a => a.Id == alarm.Id);
if (index >= 0)
{
foreach (var key in alarm.Keys)
{
alarms[index][key] = alarm[key]; // 기존 알람 데이터를 새 데이터로 교체
}
UpdateView(); // 뷰 업데이트
}
else
{
Debug.LogWarning($"Alarm with ID {alarm.Id} not found for update.");
}
}
/// <summary>
/// 특정 알람을 제거합니다.
/// </summary>
/// <param name="alarm">제거할 알람 데이터</param>
public void RemoveAlarm(DataObject alarm)
{
// 실제로는 alarmId로 찾아서 지워야 함
alarms.Remove(alarm);
UpdateView(); // 뷰 업데이트
}
/// <summary>
/// 현재 알람 상태에 맞게 UI를 갱신합니다.
/// 알람 개수와 확장 상태(isExpanded)에 따라 어떤 UI를 보여줄지 결정합니다.
/// </summary>
private void UpdateView()
{
if (isExpanded)
{
// 확장된 상태라면, 개별 아이콘들을 다시 그립니다. (알람 추가/제거 시 대응)
ExpandCluster();
}
else
{
if (alarms.Count == 1)
{
// 알람이 1개이면 단일 아이콘 뷰를 활성화합니다.
clusterView.gameObject.SetActive(false);
expandedView.SetActive(false);
if (singleAlarmIcon1 == null)
{
// 단일 아이콘이 없으면 새로 생성합니다.
singleAlarmIcon1 = Instantiate(alarmSingleIconManagerPrefab, transform).GetComponent<AlarmSigleIconManager>();
}
singleAlarmIcon1.gameObject.SetActive(true);
singleAlarmIcon1.SetData(alarms[0], targetObject);
}
else if (alarms.Count > 1) // 축소된(기본) 상태
{
// 알람이 2개 이상이면 클러스터 뷰를 활성화합니다.
clusterView.gameObject.SetActive(true);
clusterView.AlarmCount = alarms.Count.ToString(); // 알람 개수 텍스트 업데이트
expandedView.SetActive(false);
if (singleAlarmIcon1 != null) singleAlarmIcon1.gameObject.SetActive(false); // 단일 뷰도 꺼야 함
}
else // alarms.Count == 0
{
// 알람이 없으면 모든 UI를 비활성화하고 이 게임 오브젝트를 파괴합니다.
// (AlarmManager에서 처리하므로 여기서는 비활성화만)
clusterView.gameObject.SetActive(false);
expandedView.SetActive(false);
if (singleAlarmIcon1 != null) singleAlarmIcon1.gameObject.SetActive(false);
}
}
}
/// <summary>
/// 클러스터 아이콘이 클릭되었을 때 호출되는 이벤트 핸들러입니다.
/// </summary>
public void OnPointerClick()
{
isZoomIn = true; // 사용자가 줌인 동작을 시작했음을 기록
if (alarms.Count > 1)
{
if (singleAlarmIcon1 != null && singleAlarmIcon1.IsDetailViewActive)
{
singleAlarmIcon1.HideDetail(); // 단일 아이콘 숨김
}
// 카메라를 타겟 객체에 포커스하고, 클러스터를 펼칩니다.
FactoryCameraController.Instance.FocusOnTarget(targetObject.position, 15.0f); // 예시: 5미터 거리로 줌
ExpandCluster();
}
}
/// <summary>
/// 클러스터 뷰를 펼쳐서 개별 알람 아이콘들을 원형으로 배열합니다.
/// </summary>
private void ExpandCluster()
{
isExpanded = true;
clusterView.gameObject.SetActive(false); // 클러스터 뷰는 숨김
expandedView.SetActive(true); // 확장 뷰는 보임
if (singleAlarmIcon1 != null) singleAlarmIcon1.gameObject.SetActive(false);
// 재활용을 위해 기존에 생성된 아이콘들을 딕셔너리에 저장합니다. (Key: 알람 ID, Value: 아이콘 인스턴스)
var existingIcons = new Dictionary<string, AlarmSigleIconManager>();
foreach (Transform child in expandedView.transform)
{
AlarmSigleIconManager icon = child.GetComponent<AlarmSigleIconManager>();
if (icon != null && icon.Data != null)
{
if (!existingIcons.ContainsKey(icon.Data.Id))
{
existingIcons.Add(icon.Data.Id, icon);
}
}
}
var activeIcons = new List<AlarmSigleIconManager>();
// 현재 알람 목록을 순회하며 아이콘을 업데이트하거나 새로 생성
foreach (var alarmData in alarms)
{
AlarmSigleIconManager icon;
if (existingIcons.TryGetValue(alarmData.Id, out icon))
{
// 기존 아이콘 재사용
icon.SetData(alarmData, targetObject);
existingIcons.Remove(alarmData.Id); // 처리된 아이콘은 딕셔너리에서 제거
}
else
{
// 새 아이콘 생성
GameObject iconObj = Instantiate(alarmSingleIconManagerPrefab, expandedView.transform);
icon = iconObj.GetComponent<AlarmSigleIconManager>();
icon.SetData(alarmData, targetObject);
icon.OnDetail = OnDetailSingle;
}
activeIcons.Add(icon);
}
// 더 이상 사용되지 않는 기존 아이콘들 삭제
foreach (var icon in existingIcons.Values)
{
icon.OnDetail = null;
Destroy(icon.gameObject);
}
// alarmSingleIconPrefab의 RectTransform을 가져와 아이콘의 너비를 계산합니다.
RectTransform iconRect = alarmSingleIconManagerPrefab.GetComponent<RectTransform>();
if (iconRect == null)
{
Debug.LogError("alarmSingleIconPrefab에 RectTransform 컴포넌트가 없습니다.");
return;
}
// 아이콘들을 원형으로 배치하는 로직
float iconWidth = iconRect.rect.width;
// 아이콘의 개수와 너비, 간격을 고려하여 필요한 원주를 계산합니다.
float circumference = (iconWidth + iconSpacing) * activeIcons.Count;
// 원주를 이용하여 반지름(radius)을 계산합니다.
float radius = circumference / (2f * Mathf.PI);
// 중심 오프셋을 적용할 Vector3를 생성합니다.
Vector3 centerOffset = new Vector3(expandOffsetX, expandOffsetY, 0);
for (int i = 0; i < activeIcons.Count; i++)
{
float angle = i * Mathf.PI * 2f / activeIcons.Count;
// 오프셋을 적용하여 아이콘의 위치를 계산합니다.
Vector3 pos = centerOffset + new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * radius;
activeIcons[i].transform.localPosition = pos;
}
// 상세 보기가 활성화된 아이콘을 찾아 맨 위로 올립니다.
foreach (var icon in activeIcons)
{
if (icon.IsDetailViewActive)
{
icon.transform.SetAsLastSibling();
break;
}
}
//AnimateUISpace(0f); // 간격을 0으로 애니메이션
}
/// <summary>
/// 개별 알람 아이콘 중 하나가 상세 보기를 활성화했을 때 호출됩니다.
/// </summary>
/// <param name="alarmSingle">상세 보기를 활성화한 아이콘</param>
private void OnDetailSingle(AlarmSigleIconManager alarmSingle)
{
// 다른 모든 아이콘들의 상세 보기는 숨깁니다.
foreach (Transform child in expandedView.transform)
{
AlarmSigleIconManager existingIcon = child.GetComponent<AlarmSigleIconManager>();
if (existingIcon != null)
{
if (existingIcon != alarmSingle) existingIcon.HideDetail(); // 다른 아이콘은 숨김
}
}
}
/// <summary>
/// 펼쳐진 클러스터를 다시 원래의 묶음 아이콘 상태로 되돌립니다.
/// </summary>
private void CollapseCluster()
{
isExpanded = false;
// 모든 개별 아이콘의 상세 보기를 숨깁니다.
foreach (Transform child in expandedView.transform)
{
AlarmSigleIconManager existingIcon = child.GetComponent<AlarmSigleIconManager>();
if (existingIcon != null && existingIcon.IsDetailViewActive) existingIcon.HideDetail();
}
if (singleAlarmIcon1 != null && singleAlarmIcon1.IsDetailViewActive)
{
singleAlarmIcon1.HideDetail(); // 단일 아이콘도 숨김
}
UpdateView(); // 뷰를 다시 축소된 상태로 업데이트합니다.
}
/// <summary>
/// 현재 관리 중인 알람의 개수를 반환합니다.
/// </summary>
public int GetAlarmCount() => alarms.Count;
private void AnimateUISpace(float targetSpacing, float duration = 1.0f)
{
if (uiSpacingTweener != null && uiSpacingTweener.IsActive() && uiSpacingTweener.IsPlaying())
{
uiSpacingTweener.Kill();
}
uiSpacingTweener = DOVirtual.Float(objectYOffset, targetSpacing, duration, (value) =>
{
objectYOffset = value;
});
}
private float cameraY = 0f;
/// <summary>
/// 카메라 위치가 변경될 때 호출되는 이벤트 핸들러입니다.
/// 사용자가 줌 아웃(카메라가 멀어짐)하면 펼쳐진 클러스터를 자동으로 닫습니다.
/// </summary>
private void OnCameraPositionChangedHandler(Vector3 position)
{
Debug.Log($"Camera position changed: {cameraY}, {position.y}, {isZoomIn}");
if (cameraY < position.y && isZoomIn)
{
// 줌 인 상태에서 카메라 위치가 변경되면 클러스터를 해제
CollapseCluster();
isZoomIn = false; // 줌 인 상태를 해제합니다.
}
cameraY = position.y; // 카메라의 Y 위치를 업데이트
}
private void OnDestroy()
{
// DoTween 애니메이션이 실행 중이면 중지시킵니다.
if (uiSpacingTweener != null && uiSpacingTweener.IsActive() && uiSpacingTweener.IsPlaying())
{
uiSpacingTweener.Kill();
}
FactoryCameraController.Instance.OnCameraPositionChanged -= OnCameraPositionChangedHandler;
clusterView.OnClickHandler -= OnPointerClick;
targetObject = null;
alarms.Clear();
alarms = null;
canvas = null;
if (singleAlarmIcon1 != null)
{
Destroy(singleAlarmIcon1.gameObject);
singleAlarmIcon1 = null;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 150b885e8a191f140a76ab655436b950

View File

@@ -9,21 +9,32 @@ using UVC.Core;
using UVC.Data;
using UVC.Extention;
using UVC.Factory.Component;
using UVC.Util;
namespace UVC.Factory.Alarm
{
/// <summary>
/// 씬에 발생하는 모든 알람을 관리하는 싱글톤 클래스입니다.
/// MQTT로부터 알람 데이터를 수신하여, 해당하는 설비(FactoryObject)에 알람 UI를 생성, 업데이트, 제거하는 역할을 담당합니다.
/// </summary>
public class AlarmManager : SingletonScene<AlarmManager>
{
[Tooltip("알람 UI 프리팹입니다. 이 프리팹은 알람 정보를 표시하는 UI 요소를 포함해야 합니다.")]
[SerializeField]
protected GameObject alarmUIPrefab; // 알람 UI 프리팹 (아래에서 설명)
private Dictionary<string, AlarmUIController> activeAlarmUIs = new Dictionary<string, AlarmUIController>();
// 현재 활성화된 알람 UI들을 관리하는 딕셔너리입니다.
// Key: 설비 ID (TRANSPORT_EQP_ID), Value: 해당 설비에 연결된 AlarmIconManager 인스턴스
private Dictionary<string, AlarmIconManager> activeAlarmUIs = new Dictionary<string, AlarmIconManager>();
// 테스트용으로 사용할 AGV 이름 리스트입니다.
private List<string> agvNames = new List<string>();
// 어떤 알람(Id)이 어떤 AGV(TRANSPORT_EQP_ID)에 할당되었는지 추적하기 위한 딕셔너리입니다.
private Dictionary<string, string> alarmAgvNames = new Dictionary<string, string>();
// FactoryDataManager에서 찾을 수 있도록 참조를 저장
// 씬에 있는 모든 FactoryObject를 관리하는 매니저의 참조입니다.
// 알람 데이터에 포함된 설비 ID를 이용해 실제 3D 객체를 찾기 위해 사용됩니다.
private FactoryObjectManager? dataManager;
@@ -33,15 +44,21 @@ namespace UVC.Factory.Alarm
/// </summary>
protected override void Init()
{
// SceneMain의 초기화가 완료되었을 때 OnSceneInitialized 메서드를 호출하도록 이벤트에 등록합니다.
// 이를 통해 필요한 다른 매니저들이 준비된 후에 로직을 실행할 수 있습니다.
SceneMain.Instance.Initialized += OnSceneInitialized;
}
/// <summary>
/// 씬의 주요 구성요소들이 초기화된 후 호출되는 메서드입니다.
/// </summary>
private void OnSceneInitialized()
{
// FactoryObjectManager의 인스턴스를 가져와 나중에 사용할 수 있도록 저장합니다.
dataManager = FactoryObjectManager.Instance;
//test code
//알람 데이터 AGV와 관련 없는것이 많아, AGV 이름을 미리 정의합니다.
// 테스트 코드: 실제 데이터가 없을 경우를 대비하여 가상의 AGV 이름 목록을 생성합니다.
// 현재는 알람 데이터 AGV 정보가 없는 경우가 많아, 임시로 AGV 이름을 할당하기 위해 사용됩니다.
for (int i = 1; i <= 115; i++)
{
//agvNames.Add($"HFF09CNA8{i.ToString("D3")}");
@@ -49,14 +66,14 @@ namespace UVC.Factory.Alarm
}
}
/// <summary>
/// Alarm 데이터를 수신하기 위한 MQTT 파이프라인을 설정합니다.
/// MQTT로부터 알람 데이터를 수신하기 위한 파이프라인을 설정하고 실행합니다.
/// 이 메서드는 외부에서 호출되어야 알람 데이터 수신이 시작됩니다.
/// </summary>
public void Run()
{
//데이터를 어떤 형식으로 받을지 정의합니다.
// 1. 데이터 마스크(DataMask) 정의:
// 수신할 데이터의 구조를 미리 정의합니다. 여기서 정의된 키(Key)들을 기준으로 데이터를 파싱합니다.
var dataMask = new DataMask();
dataMask.ObjectName = "Alarm"; // Alarm 객체의 이름을 설정합니다.
dataMask.ObjectIdKey = "ID"; // Alarm의 고유 식별자로 사용할 키를 설정합니다.
@@ -79,35 +96,39 @@ namespace UVC.Factory.Alarm
dataMask["UPDATE_TIME"] = DateTime.Now;
dataMask["TIMESTAMP"] = DateTime.Now;
// MQTT 파이프라인 정보를 생성합니다.
// 'ALARM' 토픽을 구독하고, 받은 데이터는 위에서 정의한 dataMask로 매핑하며,
// 데이터 유효성 검사를 위해 DataValidator를 설정합니다.
// 데이터가 업데이트되면 OnUpdateData 메서드를 호출하여 처리합니다.
// 2. 데이터 유효성 검사기(DataValidator) 설정:
// 수신된 데이터가 유효한지 검사하는 규칙을 추가합니다.
// 여기서는 "MACHINENAME" 필드가 null이 아닌지 확인합니다.
DataValidator validator = new DataValidator();
validator.AddValidator("MACHINENAME", value => value != null);
// 3. MQTT 파이프라인 정보(MQTTPipeLineInfo) 생성:
// - "ALARM" 토픽을 구독합니다.
// - 위에서 정의한 dataMask를 사용해 수신된 JSON 데이터를 DataObject로 변환합니다.
// - validator를 사용해 데이터의 유효성을 검사합니다.
// - 유효한 데이터가 수신되면 OnUpdateData 메서드를 호출하여 처리합니다.
var pipelineInfo = new MQTTPipeLineInfo("ALARM")
.setDataMapper(new DataMapper(dataMask))
.setValidator(validator)
.setHandler(OnUpdateData);
// 생성한 파이프라인 정보를 전역 MQTT 파이프라인에 추가합니다.
// 4. 생성한 파이프라인 전역 MQTT 파이프라인에 추가하여 데이터 수신을 시작합니다.
AppMain.Instance.MQTTPipeLine.Add(pipelineInfo);
}
// 테스트용으로, 새로 발생하는 알람에 AGV ID를 순차적으로 할당하기 위한 인덱스입니다.
int agvIdx = 50;
/// <summary>
/// 데이터 수신 시 호출되는 공개 핸들러입니다.
/// 수신된 ALARM 데이터 배열을 비동기적으로 처리하여 씬에 반영합니다.
/// 추가, 제거, 수정된 ALARM 데이터를 각각 구분하여 처리합니다.
/// MQTT 파이프라인으로부터 데이터가 업데이트될 때마다 호출되는 메인 핸들러입니다.
/// 수신된 알람 데이터 배열을 분석하여 추가, 수정, 제거된 알람을 각각 처리합니다.
/// </summary>
/// <param name="data">수신된 데이터 객체 (DataArray 형태)</param>
/// <param name="data">수신된 데이터 객체 (DataArray 형태여야 함)</param>
public void OnUpdateData(IDataObject? data)
{
if (data == null) return;
// 데이터를 DataArray 형태로 변환합니다. 아니거나 비어있으면 처리를 중단합니다.
DataArray? arr = data as DataArray;
if (arr == null || arr.Count == 0) return;
@@ -118,7 +139,8 @@ namespace UVC.Factory.Alarm
Debug.Log($"AlarmManager OnUpdateData: Added={AddedItems.Count}, Removed={RemovedItems.Count}, Modified={ModifiedList.Count}");
// clear_time이 있는 항목만 제거 리스트에 추가합니다.
// 'CLEAR_TIME'이 설정된 항목은 '해제된 알람'으로 간주하고, 제거 리스트에 추가합니다.
// AddedItems나 ModifiedList에 포함되어 있더라도 CLEAR_TIME이 있으면 즉시 해제 처리하기 위함입니다.
foreach (var item in AddedItems.ToList())
{
if (item.GetDateTime("CLEAR_TIME") != null)
@@ -126,7 +148,7 @@ namespace UVC.Factory.Alarm
if (RemovedItems.FindIndex((i) => i.Id == item.Id) == -1) RemovedItems.Add(item);
}
}
foreach (var item in ModifiedList.ToList())
{
if (item.GetDateTime("CLEAR_TIME") != null)
@@ -134,51 +156,60 @@ namespace UVC.Factory.Alarm
if (RemovedItems.FindIndex((i) => i.Id == item.Id) == -1) RemovedItems.Add(item);
}
}
// 새로 추가된 ALARM 처리
// [처리 1] 새로 추가된 알람 처리
foreach (var item in AddedItems.ToList())
{
// 알람이 해제되지 않았고(CLEAR_TIME == null), 고유 ID가 있는 경우에만 신규 알람으로 처리합니다.
if (item.GetDateTime("CLEAR_TIME") == null && !item.Id.IsNullOrEmpty())
{
item["TRANSPORT_EQP_ID"] = agvNames[agvIdx]; // AGV 이름을 TRANSPORT_EQP_ID에 설정
alarmAgvNames.Add(item.Id!, agvNames[agvIdx]);
HandleNewAlarm(item);
// 테스트 코드: 수신된 알람 데이터에 임시로 AGV ID를 할당합니다.
item["TRANSPORT_EQP_ID"] = agvNames[agvIdx];
alarmAgvNames.Add(item.Id!, agvNames[agvIdx]); // 알람 ID와 할당된 AGV ID를 매핑하여 저장합니다.
HandleNewAlarm(item); // 신규 알람 처리 로직 호출
agvIdx++;
if(agvIdx >= agvNames.Count) agvIdx = 0; // AGV 이름이 부족할 경우 순환
if (agvIdx >= agvNames.Count) agvIdx = 0; // AGV ID를 순환하며 사용합니다.
}
}
// 정보가 수정된 ALARM 처리
// [처리 2] 정보가 수정된 알람 처리
foreach (var item in ModifiedList.ToList())
{
// 해제되지 않았고, ID가 있으며, 이미 관리 중인 알람인 경우에만 수정 처리합니다.
if (item.GetDateTime("CLEAR_TIME") == null && !item.Id.IsNullOrEmpty() && alarmAgvNames.ContainsKey(item.Id!))
{
item["TRANSPORT_EQP_ID"] = alarmAgvNames[item.Id!]; // 기존 AGV 이름 유지
HandleModifyAlarm(item);
item["TRANSPORT_EQP_ID"] = alarmAgvNames[item.Id!]; // 기존에 할당된 AGV ID를 유지합니다.
HandleModifyAlarm(item); // 수정 알람 처리 로직 호출
}
}
// 제거된 ALARM 처리
// [처리 3] 제거(해제)된 알람 처리
foreach (var item in RemovedItems.ToList())
{
// ID가 있고, 관리 중인 알람인 경우에만 해제 처리합니다.
if (!item.Id.IsNullOrEmpty() && alarmAgvNames.ContainsKey(item.Id!))
{
item["TRANSPORT_EQP_ID"] = alarmAgvNames[item.Id!]; // 기존 AGV 이름 유지
HandleClearedAlarm(item);
item["TRANSPORT_EQP_ID"] = alarmAgvNames[item.Id!]; // 기존 AGV ID를 설정하여 어떤 UI를 제거할지 찾도록 합니다.
HandleClearedAlarm(item); // 해제 알람 처리 로직 호출
}
}
}
/// <summary>
/// AlarmManager가 파괴될 때 호출됩니다.
/// MQTT 파이프라인에서 'ALARM' 핸들러를 제거하여 메모리 누수 방지합니다.
/// 등록했던 MQTT 파이프라인 핸들러를 제거하여 메모리 누수 및 원치 않는 동작을 방지합니다.
/// </summary>
protected override void OnDestroy()
{
base.OnDestroy();
// AppMain의 MQTTPipeLine에서 "ALARM" 토픽에 대한 핸들러를 제거합니다.
AppMain.Instance.MQTTPipeLine.Remove("ALARM");
}
/// <summary>
/// 새로운 알람 데이터를 받아 UI를 생성하고 관리 목록에 추가합니다.
/// </summary>
/// <param name="data">새로운 알람 정보</param>
public void HandleNewAlarm(DataObject data)
{
if (data.Id == null)
@@ -187,17 +218,20 @@ namespace UVC.Factory.Alarm
return;
}
// 알람 데이터에서 설비 ID(TRANSPORT_EQP_ID)를 가져옵니다.
string? eqpId = data.GetString("TRANSPORT_EQP_ID");
if (eqpId == null) return;
// 설비 ID를 이용해 씬에서 실제 FactoryObject를 찾습니다.
FactoryObject? targetObject = dataManager!.FindByName(eqpId);
Debug.Log($"AlarmManager {targetObject==null} {data.Id}, {eqpId}");
Debug.Log($"AlarmManager {targetObject == null} {data.Id}, {eqpId}");
if (targetObject == null) return;
// 해당 설비에 이미 활성화된 알람 UI가 있는지 확인합니다.
if (activeAlarmUIs.ContainsKey(eqpId))
{
if(activeAlarmUIs[eqpId].ContainsAlarm(data))
// 이미 UI가 있다면, 해당 UI에 새로운 알람 정보를 추가하거나 업데이트합니다.
if (activeAlarmUIs[eqpId].ContainsAlarm(data))
{
activeAlarmUIs[eqpId].UpdateAlarm(data);
}
@@ -208,17 +242,22 @@ namespace UVC.Factory.Alarm
}
else
{
// 없으면 새로 생성
// 활성화된 UI가 없다면 새로 생성합니다.
GameObject newUIObject = Instantiate(alarmUIPrefab, transform); // 매니저 하위에 생성
AlarmUIController newUiController = newUIObject.GetComponent<AlarmUIController>();
newUiController.Initialize(targetObject, data);
AlarmIconManager newUiController = newUIObject.GetComponent<AlarmIconManager>(); // AlarmManager 하위에 UI 생성
newUiController.Initialize(targetObject, data); // 찾은 3D 객체와 알람 데이터로 UI 초기화
// 새로 생성된 UI를 관리 목록에 추가합니다.
activeAlarmUIs.Add(eqpId, newUiController);
}
}
/// <summary>
/// 기존 알람의 정보가 변경되었을 때 UI를 업데이트합니다.
/// </summary>
/// <param name="data">수정된 알람 정보</param>
public void HandleModifyAlarm(DataObject data)
{
if (data.Id == null)
@@ -229,22 +268,26 @@ namespace UVC.Factory.Alarm
string? eqpId = data.GetString("TRANSPORT_EQP_ID");
if (eqpId == null) return;
// 이미 해당 설비에 알람 UI가 있는지 확인
if (activeAlarmUIs.TryGetValue(eqpId, out AlarmUIController uiController))
// 해당 설비에 연결된 알람 UI가 있는지 확인합니다.
if (activeAlarmUIs.TryGetValue(eqpId, out AlarmIconManager uiController))
{
// 있으면 기존 UI에 알람 정보 업데이트
// 이미 존재하는 알람 정보가 수정된 것이라면 업데이트합니다.
if (uiController.ContainsAlarm(data))
{
uiController.UpdateAlarm(data);
}
else
{
// 없으면 새로 추가
// 드물지만, 수정 목록에 왔으나 새로운 알람인 경우 추가합니다.
uiController.AddAlarm(data);
}
}
}
/// <summary>
/// 해제된 알람을 받아 해당하는 UI를 제거하거나 업데이트합니다.
/// </summary>
/// <param name="data">해제된 알람 정보</param>
public void HandleClearedAlarm(DataObject data)
{
if (data.Id.IsNullOrEmpty())
@@ -256,15 +299,19 @@ namespace UVC.Factory.Alarm
string? eqpId = data.GetString("TRANSPORT_EQP_ID");
if (eqpId == null) return;
if (activeAlarmUIs.TryGetValue(eqpId, out AlarmUIController uiController))
// 해당 설비에 연결된 알람 UI가 있는지 확인합니다.
if (activeAlarmUIs.TryGetValue(eqpId, out AlarmIconManager uiController))
{
// 있으면 기존 UI에 알람 정보 업데이트
// UI가 관리하는 알람 목록에 해당 알람이 있는지 확인합니다.
if (uiController.ContainsAlarm(data))
{
// 목록에서 알람을 제거합니다.
uiController.RemoveAlarm(data);
// 만약 해당 UI에 더 이상 표시할 알람이 없다면,
if (uiController.GetAlarmCount() == 0)
{
// 관리 목록에서 제거하고 씬에서 UI 게임 오브젝트를 파괴합니다.
activeAlarmUIs.Remove(data.Id!);
Destroy(uiController.gameObject);
}

View File

@@ -0,0 +1,156 @@
using System;
using UnityEngine;
using UVC.Data;
namespace UVC.Factory.Alarm
{
/// <summary>
/// 개별 알람 아이콘 하나를 관리하는 클래스입니다.
/// 평소에는 간단한 아이콘(AlarmSingleIcon)으로 표시되고, 클릭하면 상세 정보(AlarmDetailView)를 보여줍니다.
/// </summary>
public class AlarmSigleIconManager : MonoBehaviour
{
[Header("UI Components")]
[Tooltip("알람을 간략하게 표시하는 아이콘 UI입니다.")]
[SerializeField] private AlarmSingleIcon singleIcon;
[Tooltip("알람 상세 정보를 표시하는 UI입니다.")]
[SerializeField] private AlarmDetailView detailView;
/// <summary>
/// 이 아이콘의 상세 보기가 활성화될 때 호출되는 이벤트입니다.
/// 상위 관리자인 AlarmIconManager가 다른 아이콘들의 상세 보기를 닫는 등의 관리를 위해 사용합니다.
/// </summary>
public Action<AlarmSigleIconManager> OnDetail;
// 이 아이콘이 표시하는 실제 알람 데이터입니다.
private DataObject data;
// 알람이 발생한 설비의 Transform 정보입니다. (현재 코드에서는 직접 사용되지 않음)
private Transform equipmentTransform;
/// <summary>
/// 이 아이콘이 가지고 있는 알람 데이터를 외부에서 읽을 수 있도록 노출하는 프로퍼티입니다.
/// </summary>
public DataObject Data => data;
/// <summary>
/// 상세 정보 UI가 현재 활성화되어 있는지 여부를 반환합니다.
/// </summary>
public bool IsDetailViewActive => detailView != null && detailView.gameObject.activeSelf;
/// <summary>
/// 컴포넌트가 시작될 때 호출되는 Unity 생명주기 메서드입니다.
/// </summary>
private void Start()
{
if (singleIcon != null)
{
// 간단한 아이콘(singleIcon)이 클릭되었을 때 OnPointerClick 메서드가 호출되도록 이벤트를 등록합니다.
singleIcon.OnClickHandler += OnPointerClick;
}
else
{
Debug.LogWarning("Single Icon이 할당되지 않았습니다.");
}
}
/// <summary>
/// 이 컴포넌트가 파괴될 때 호출되는 Unity 생명주기 메서드입니다.
/// </summary>
private void OnDestroy()
{
// 참조를 정리하여 메모리 누수를 방지합니다.
data = null;
equipmentTransform = null;
if (singleIcon != null)
{
// Start에서 등록했던 클릭 이벤트를 해제합니다.
singleIcon.OnClickHandler -= OnPointerClick;
}
else
{
Debug.LogWarning("Single Icon is not assigned.");
}
}
/// <summary>
/// 이 아이콘에 표시할 알람 데이터를 설정하거나 업데이트합니다.
/// </summary>
/// <param name="newData">설정할 새로운 알람 데이터</param>
/// <param name="equipment">알람이 발생한 설비의 Transform</param>
public void SetData(DataObject newData, Transform equipment)
{
// 데이터 업데이트
if (data == null)
{
data = newData;
}
else
{
// 기존 데이터가 있는 경우, 새로운 데이터의 내용으로 덮어씁니다.
foreach (var keyValue in newData)
{
if (data.ContainsKey(keyValue.Key))
{
data[keyValue.Key] = keyValue.Value;
}
}
}
equipmentTransform = equipment;
// 데이터에서 아이콘 정보를 가져와 UI 텍스트로 표시합니다.
string icon = data.GetString("ICON");
if (singleIcon.Text != null && icon != null)
{
singleIcon.Text = icon;
}
}
/// <summary>
/// 상세 정보 UI를 보여줍니다.
/// </summary>
private void ShowDetail()
{
// 이미 상세 보기가 활성화되어 있다면 아무것도 하지 않습니다.
if (detailView.gameObject.activeSelf) return;
// 상세 보기를 열었다는 사실을 외부(AlarmIconManager)에 알립니다.
OnDetail?.Invoke(this);
// 이 아이콘이 다른 아이콘들보다 위에 그려지도록 렌더링 순서를 맨 뒤로 보냅니다.
// (Unity UI에서 RectTransform의 계층 순서가 뒤에 있을수록 화면에 위에 그려짐)
transform.SetAsLastSibling();
// 상세 보기 UI를 활성화하고, 알람 데이터를 전달하여 내용을 채웁니다.
detailView.gameObject.SetActive(true);
detailView.Show(data);
}
/// <summary>
/// 상세 정보 UI를 숨깁니다.
/// </summary>
public void HideDetail()
{
if (detailView != null)
{
detailView.Hide();
}
}
/// <summary>
/// 아이콘(singleIcon)이 클릭되었을 때 호출되는 메서드입니다.
/// </summary>
public void OnPointerClick()
{
if (detailView == null)
{
Debug.LogWarning("Detail view or text is not assigned.");
return;
}
// 상세 보기를 보여주는 메서드를 호출합니다.
ShowDetail();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a651278faa6ca71468e19553474e6bb5

View File

@@ -1,51 +1,100 @@
using TMPro;
using System;
using TMPro;
using UnityEngine;
using UVC.Data;
using UVC.Util;
using UnityEngine.UI;
namespace UVC.Factory.Alarm
{
/// <summary>
/// 개별 알람을 나타내는 가장 기본적인 UI 컴포넌트입니다.
/// 이 컴포넌트는 버튼 기능과 텍스트 표시 기능을 가지고 있으며,
/// 상위 관리자인 'AlarmSigleIconManager'에 의해 제어됩니다.
/// </summary>
public class AlarmSingleIcon : MonoBehaviour
{
[Tooltip("클릭 가능한 UI 버튼입니다.")]
[SerializeField] private Button button;
[Tooltip("알람 내용을 표시하는 텍스트입니다.")]
[SerializeField] private TextMeshProUGUI text;
// 버튼 내에 텍스트를 표시하기 위한 TextMeshPro 컴포넌트입니다.
private TextMeshProUGUI buttonText;
private DataObject data;
private Transform equipmentTransform;
public void SetData(DataObject newData, Transform equipment)
/// <summary>
/// 아이콘에 표시될 텍스트를 가져오거나 설정하는 프로퍼티입니다.
/// </summary>
public string Text
{
if (data == null)
get => buttonText != null ? buttonText.text : string.Empty;
set
{
data = newData;
if (buttonText != null)
{
buttonText.text = value;
}
}
}
/// <summary>
/// 이 버튼이 클릭되었을 때 호출될 이벤트를 정의합니다.
/// 상위 관리자(AlarmSigleIconManager)가 이 이벤트를 구독하여 클릭 시 특정 동작(예: 상세 정보 표시)을 수행합니다.
/// </summary>
public Action OnClickHandler;
/// <summary>
/// 컴포넌트가 생성될 때 가장 먼저 호출되는 Unity 생명주기 메서드입니다.
/// </summary>
private void Awake()
{
buttonText = GetComponentInChildren<TextMeshProUGUI>();
if (buttonText == null)
{
Debug.LogWarning("Text component not found in children.", this);
}
}
/// <summary>
/// 첫 번째 프레임 업데이트 전에 호출되는 Unity 생명주기 메서드입니다.
/// </summary>
private void Start()
{
if (button != null)
{
// 버튼의 onClick 이벤트에 HandleClick 메서드를 리스너로 등록합니다.
// 이렇게 하면 버튼이 클릭될 때마다 HandleClick 메서드가 실행됩니다.
button.onClick.AddListener(HandleClick);
}
else
{
foreach (var keyValue in newData)
{
if (data.ContainsKey(keyValue.Key))
{
data[keyValue.Key] = keyValue.Value;
}
}
}
equipmentTransform = equipment;
string icon = data.GetString("ICON");
if (text != null && icon != null)
{
text.text = icon;
Debug.LogWarning("Button is not assigned.");
}
}
public void OnPointerClick()
/// <summary>
/// 버튼 클릭 이벤트가 발생했을 때 실행되는 메서드입니다.
/// </summary>
private void HandleClick()
{
// 클릭 시 해당 설비로 카메라 포커스
CameraController.Instance.FocusOnTarget(equipmentTransform.position, 3.0f);
Debug.Log($"알람 [{data.GetString("MESSAGE")}]이 발생한 설비로 이동합니다.");
// 여기서 알람 상세정보 패널을 띄워도 좋음
// OnClickHandler 이벤트에 등록된 메서드가 있는지 확인하고, 있다면 호출합니다.
// '?'는 Null 조건부 연산자로, OnClickHandler가 null이 아닐 경우에만 Invoke()를 실행합니다.
OnClickHandler?.Invoke();
}
/// <summary>
/// 이 컴포넌트가 파괴될 때 호출되는 Unity 생명주기 메서드입니다.
/// </summary>
private void OnDestroy()
{
if (button != null)
{
// Start에서 등록했던 클릭 이벤트 리스너를 제거합니다.
// 이를 통해 메모리 누수를 방지할 수 있습니다.
button.onClick.RemoveListener(HandleClick);
}
else
{
Debug.LogWarning("Button is not assigned.");
}
// 이벤트 핸들러 참조를 null로 만들어 잠재적인 메모리 문제를 방지합니다.
OnClickHandler = null;
}
}
}

View File

@@ -1,2 +1,2 @@
fileFormatVersion: 2
guid: cd395cde180b3094a91d87699139bbcf
guid: 850e5c55cc9c41a419204fc6e16ae821

View File

@@ -1,286 +0,0 @@
using DG.Tweening;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UVC.Data;
using UVC.Factory.Component;
using UVC.Util;
namespace UVC.Factory.Alarm
{
public class AlarmUIController : MonoBehaviour, IPointerClickHandler
{
[Tooltip("알람 클러스터 뷰입니다.")]
[SerializeField] private GameObject clusterView;
[Tooltip("알람 개수를 표시하는 텍스트입니다.")]
[SerializeField] private TextMeshProUGUI alarmCountText;
[Tooltip("확장된 알람 뷰입니다. 개별 알람 아이콘을 표시합니다.")]
[SerializeField] private GameObject expandedView;
[Tooltip("개별 알람 아이콘 프리팹입니다. 이 프리팹은 개별 알람 정보를 표시하는 UI 요소를 포함해야 합니다.")]
[SerializeField] private GameObject alarmSingleIconPrefab; // 개별 알람 아이콘
[Tooltip("아이콘이 중심에서의 Y Offset")]
[SerializeField] private float iconYOffset = 10f;
[Tooltip("확장 시 아이콘 간의 간격입니다.")]
[SerializeField] private float iconSpacing = 10f;
[Tooltip("확장 시 아이콘들의 중심 X 오프셋입니다.")]
[SerializeField] private float expandOffsetX = 0f;
[Tooltip("확장 시 아이콘들의 중심 Y 오프셋입니다.")]
[SerializeField] private float expandOffsetY = 0f;
private Transform targetObject;
private List<DataObject> alarms = new List<DataObject>();
private bool isExpanded = false;
private AlarmSingleIcon singleAlarmIcon1 = null;
private RectTransform rectTransform;
private Canvas mainCanvas;
private Tweener uiSpacingTweener;
private bool isZoomIn = false; // 줌 인 상태를 추적하기 위한 변수
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
mainCanvas = GetComponentInParent<Canvas>();
}
public void Initialize(FactoryObject target, DataObject initialAlarm)
{
this.targetObject = target.transform;
AddAlarm(initialAlarm);
}
void LateUpdate()
{
if (targetObject == null || Camera.main == null || mainCanvas == null)
{
// 필수 컴포넌트가 없으면 UI를 비활성화합니다.
if (clusterView.activeSelf) clusterView.SetActive(false);
if (expandedView.activeSelf) expandedView.SetActive(false);
if (alarmCountText != null && alarmCountText.gameObject.activeSelf) alarmCountText.gameObject.SetActive(false);
if (singleAlarmIcon1 != null && singleAlarmIcon1.gameObject.activeSelf) singleAlarmIcon1.gameObject.SetActive(false);
return;
}
// 카메라의 정면 방향과 타겟을 향하는 방향을 계산합니다.
Vector3 cameraForward = Camera.main.transform.forward;
Vector3 toTarget = (targetObject.position - Camera.main.transform.position).normalized;
// 두 벡터의 내적을 계산하여 타겟이 카메라 앞에 있는지 확인합니다.
// 내적 값이 0보다 크면 타겟이 카메라 앞에 있는 것입니다.
if (Vector3.Dot(cameraForward, toTarget) > 0)
{
// 타겟이 앞에 있을 때만 UI를 활성화하고 위치를 업데이트합니다.
if (!gameObject.activeSelf)
{
UpdateView(); // 비활성화 상태였다면 뷰를 다시 활성화합니다.
}
// targetObject의 월드 좌표를 스크린 좌표로 변환
Vector3 screenPoint = Camera.main.WorldToScreenPoint(targetObject.position + Vector3.up);
// Canvas Render Mode가 Screen Space - Camera일 경우
if (mainCanvas.renderMode == RenderMode.ScreenSpaceCamera)
{
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(mainCanvas.transform as RectTransform, screenPoint, mainCanvas.worldCamera, out localPoint);
localPoint.y += iconYOffset;
rectTransform.localPosition = localPoint;
}
// Canvas Render Mode가 Screen Space - Overlay일 경우
else
{
screenPoint.y += iconYOffset;
rectTransform.position = screenPoint;
}
}
else
{
// 타겟이 카메라 뒤에 있으면 모든 관련 UI를 비활성화합니다.
if (clusterView.activeSelf) clusterView.SetActive(false);
if (expandedView.activeSelf) expandedView.SetActive(false);
if (alarmCountText != null && alarmCountText.gameObject.activeSelf) alarmCountText.gameObject.SetActive(false);
if (singleAlarmIcon1 != null && singleAlarmIcon1.gameObject.activeSelf) singleAlarmIcon1.gameObject.SetActive(false);
}
}
public bool ContainsAlarm(DataObject alarm)
{
return alarms.Exists(a => a.Id == alarm.Id);
}
public void AddAlarm(DataObject alarm)
{
alarms.Add(alarm);
UpdateView();
}
public void UpdateAlarm(DataObject alarm)
{
// 실제로는 alarmId로 찾아서 업데이트해야 함
int index = alarms.FindIndex(a => a.Id == alarm.Id);
if (index >= 0)
{
foreach (var key in alarm.Keys)
{
alarms[index][key] = alarm[key]; // 기존 알람 데이터 업데이트
}
UpdateView();
}
else
{
Debug.LogWarning($"Alarm with ID {alarm.Id} not found for update.");
}
}
public void RemoveAlarm(DataObject alarm)
{
// 실제로는 alarmId로 찾아서 지워야 함
alarms.Remove(alarm);
UpdateView();
}
private void UpdateView()
{
if (isExpanded)
{
// 확장된 상태라면 개별 아이콘들을 다시 그림
ExpandCluster();
}
else
{
// 일반 상태
if (alarms.Count > 1)
{
clusterView.SetActive(true);
expandedView.SetActive(false);
if (singleAlarmIcon1 != null) singleAlarmIcon1.gameObject.SetActive(false); // 단일 뷰도 꺼야 함
alarmCountText.gameObject.SetActive(true);
alarmCountText.text = alarms.Count.ToString();
}
else if (alarms.Count == 1)
{
clusterView.SetActive(false);
expandedView.SetActive(false);
alarmCountText.gameObject.SetActive(false); // 알람 개수 텍스트 숨김
// 여기에 단일 알람 아이콘을 보여주는 로직 추가
if (singleAlarmIcon1 == null)
{
singleAlarmIcon1 = Instantiate(alarmSingleIconPrefab, transform).GetComponent<AlarmSingleIcon>();
}
singleAlarmIcon1.gameObject.SetActive(true);
singleAlarmIcon1.SetData(alarms[0], targetObject);
}
}
}
public void OnPointerClick() // 이 함수를 Event Trigger 등으로 호출
{
if (isExpanded)
{
CollapseCluster();
}
else
{
if (alarms.Count > 1)
{
isZoomIn = true;
// 클러스터 확장
CameraController.Instance.FocusOnTarget(targetObject.position, 15.0f); // 예시: 5미터 거리로 줌
ExpandCluster();
}
else if (alarms.Count == 1)
{
isZoomIn = true;
//AnimateUISpace(1f); // 간격을 0으로 애니메이션
// 단일 알람 클릭
CameraController.Instance.FocusOnTarget(targetObject.position, 10.0f);
// 추가로 알람 상세 정보 UI를 띄울 수 있음
}
}
}
private void ExpandCluster()
{
isExpanded = true;
clusterView.SetActive(false);
expandedView.SetActive(true);
singleAlarmIcon1.gameObject.SetActive(false);
alarmCountText.gameObject.SetActive(false);
// 기존 아이콘들 삭제
foreach (Transform child in expandedView.transform)
{
Destroy(child.gameObject);
}
// alarmSingleIconPrefab의 RectTransform을 가져와 아이콘의 너비를 계산합니다.
RectTransform iconRect = alarmSingleIconPrefab.GetComponent<RectTransform>();
if (iconRect == null)
{
Debug.LogError("alarmSingleIconPrefab에 RectTransform 컴포넌트가 없습니다.");
return;
}
float iconWidth = iconRect.rect.width;
// 아이콘의 개수와 너비, 간격을 고려하여 필요한 원주를 계산합니다.
float circumference = (iconWidth + iconSpacing) * alarms.Count;
// 원주를 이용하여 반지름(radius)을 계산합니다.
float radius = circumference / (2f * Mathf.PI);
// 중심 오프셋을 적용할 Vector3를 생성합니다.
Vector3 centerOffset = new Vector3(expandOffsetX, expandOffsetY, 0);
for (int i = 0; i < alarms.Count; i++)
{
float angle = i * Mathf.PI * 2f / alarms.Count;
// 오프셋을 적용하여 아이콘의 위치를 계산합니다.
Vector3 pos = centerOffset + new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * radius;
GameObject iconObj = Instantiate(alarmSingleIconPrefab, expandedView.transform);
iconObj.transform.localPosition = pos;
// iconObj의 AlarmSingleIcon 스크립트에 알람 데이터 전달
iconObj.GetComponent<AlarmSingleIcon>().SetData(alarms[i], targetObject);
}
//AnimateUISpace(0f); // 간격을 0으로 애니메이션
}
private void CollapseCluster()
{
isExpanded = false;
UpdateView();
}
public int GetAlarmCount() => alarms.Count;
public void OnPointerClick(PointerEventData eventData)
{
OnPointerClick();
}
private void AnimateUISpace(float targetSpacing, float duration = 1.0f)
{
if (uiSpacingTweener != null && uiSpacingTweener.IsActive() && uiSpacingTweener.IsPlaying())
{
uiSpacingTweener.Kill();
}
uiSpacingTweener = DOVirtual.Float(iconYOffset, targetSpacing, duration, (value) =>
{
iconYOffset = value;
});
}
private void OnDestroy()
{
if (uiSpacingTweener != null && uiSpacingTweener.IsActive() && uiSpacingTweener.IsPlaying())
{
uiSpacingTweener.Kill();
}
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 773fa02b59601044b8be752f78f63e55

View File

@@ -11,7 +11,7 @@ namespace UVC.Factory
/// - 마우스 오른쪽 버튼 드래그: 카메라 회전 (Orbit)
/// - 마우스 휠 스크롤: 카메라 줌 (Zoom)
/// </summary>
public class CameraController : SingletonScene<CameraController>
public class FactoryCameraController : SingletonScene<FactoryCameraController>
{
[Header("Panning Speed")]
[Tooltip("카메라 높이가 임계값보다 낮을 때의 평행 이동 속도")]
@@ -55,7 +55,7 @@ namespace UVC.Factory
/// <remarks>이 이벤트는 카메라의 변형이 업데이트될 때마다 트리거되며,
/// 구독자는 위치, 회전 또는 크기 변경에 응답할 수 있습니다. 이 이벤트를 사용하여
/// UI 요소 업데이트 또는 종속 값 재계산과 같은 작업을 수행할 수 있습니다.</remarks>
public event Action<Transform> OnCameraChanged;
public Action<Transform> OnCameraChanged;
/// <summary>
/// 카메라 위치가 변경될 때 발생합니다.
@@ -63,14 +63,14 @@ namespace UVC.Factory
/// <remarks>이 이벤트는 카메라 위치가 업데이트될 때마다 트리거됩니다. 구독자는
/// 이 이벤트를 사용하여 UI 요소를 업데이트하거나 새 위치를 기반으로 계산을 수행하는 등 카메라 위치 변경에 대응할 수 있습니다.
///</remarks>
public event Action<Vector3> OnCameraPositionChanged;
public Action<Vector3> OnCameraPositionChanged;
/// <summary>
/// 카메라의 회전이 변경될 때 발생합니다.
/// </summary>
/// <remarks>이 이벤트는 카메라의 회전이 업데이트될 때마다 트리거됩니다. 구독자는
/// 이 이벤트를 사용하여 카메라 방향의 변경에 응답할 수 있습니다.</remarks>
public event Action<Quaternion> OnCameraRotionChanged;
public Action<Quaternion> OnCameraRotationChanged;
/// <summary>
/// 정의된 범위 내에서 카메라의 정규화된 수직 위치를 가져옵니다.
@@ -121,7 +121,7 @@ namespace UVC.Factory
}
if (prevTransform.rotation != transform.rotation)
{
OnCameraRotionChanged?.Invoke(transform.rotation);
OnCameraRotationChanged?.Invoke(transform.rotation);
}
}
prevTransform = transform; // 현재 카메라 위치 저장