diff --git a/Assets/Scripts/UVC/Data/DataArray.cs b/Assets/Scripts/UVC/Data/DataArray.cs index 81bf56b5..e63326e0 100644 --- a/Assets/Scripts/UVC/Data/DataArray.cs +++ b/Assets/Scripts/UVC/Data/DataArray.cs @@ -212,7 +212,12 @@ namespace UVC.Data return this; } - + /// + /// 업데이트 된 속성의 수. + /// + /// 업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다. + public int UpdatedCount { get => addedList.Count + modifiedList.Count + removedList.Count; } + /// /// 항목을 추가합니다. diff --git a/Assets/Scripts/UVC/Data/DataMapper.cs b/Assets/Scripts/UVC/Data/DataMapper.cs index 92f224b7..cbb36f3e 100644 --- a/Assets/Scripts/UVC/Data/DataMapper.cs +++ b/Assets/Scripts/UVC/Data/DataMapper.cs @@ -41,10 +41,19 @@ namespace UVC.Data // 재귀 호출 제한하기 위한 설정 추가 private int maxRecursionDepth = 10; - private int currentDepth = 0; private DataMask mask; + /// + /// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다. + /// + public bool SupportsStreamParsing { get; internal set; } = true; + + /// + /// 대용량 JSON 스트림을 판단 할때 스트림 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다. + /// + public int SupportsStreamLength { get; internal set; } = 10000; + /// /// DataMapper 클래스의 새 인스턴스를 초기화합니다. /// @@ -116,7 +125,7 @@ namespace UVC.Data /// /// JSON 데이터 스트림 /// 매핑된 DataObject - public DataObject MapStream(System.IO.Stream jsonStream) + public DataObject MapObjectStream(System.IO.Stream jsonStream) { using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream))) { @@ -126,6 +135,21 @@ namespace UVC.Data } } + /// + /// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다. + /// + /// JSON 데이터 스트림 + /// 매핑된 DataObject + public DataArray MapArrayStream(System.IO.Stream jsonStream) + { + using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream))) + { + var serializer = new Newtonsoft.Json.JsonSerializer(); + var sourceArray = serializer.Deserialize(reader); + return Mapping(sourceArray); + } + } + /// /// 객체를 재귀적으로 매핑합니다. /// diff --git a/Assets/Scripts/UVC/Data/DataMask.cs b/Assets/Scripts/UVC/Data/DataMask.cs index 5ae555a4..c3ae7b86 100644 --- a/Assets/Scripts/UVC/Data/DataMask.cs +++ b/Assets/Scripts/UVC/Data/DataMask.cs @@ -5,21 +5,96 @@ using System.Collections.Generic; namespace UVC.Data { + /// + /// JSON 데이터의 구조와 변환 규칙을 정의하는 마스크 클래스입니다. + /// Newtonsoft.Json.Linq.JObject를 상속하여 JSON 데이터 구조를 표현하고, + /// 데이터 매핑, 필드 이름 변환, 그리고 타입 변환을 위한 메타데이터를 제공합니다. + /// + /// + /// DataMask는 주로 DataMapper 클래스와 함께 사용되어 서로 다른 JSON 형식 간의 + /// 데이터 변환 및 매핑 규칙을 정의합니다. 기본 JSON 구조 외에도 추가 속성들을 + /// 통해 매핑 과정에서 필요한 메타데이터를 제공합니다. + /// + /// + /// 기본 사용 예시: + /// + /// // 마스크 객체 생성 및 설정 + /// var mask = new DataMask(); + /// mask["name"] = ""; // 문자열 타입 지정 + /// mask["age"] = 0; // 정수 타입 지정 + /// mask["isActive"] = false; // 불리언 타입 지정 + /// mask["height"] = 0.0; // 실수 타입 지정 + /// + /// // 메타 속성 설정 + /// mask.ObjectIdKey = "name"; // DataObject의 ID로 사용할 필드 지정 + /// mask.ObjectName = "employees"; // 데이터 객체의 이름 지정 + /// + /// // 필드 이름 변환 규칙 설정 + /// mask.NamesForReplace = new Dictionary<string, string> + /// { + /// { "full_name", "name" }, // JSON의 full_name을 name으로 변환 + /// { "employee_age", "age" } // JSON의 employee_age를 age로 변환 + /// }; + /// + /// // 매핑 예시 (DataMapper 클래스와 함께 사용) + /// var sourceJson = JObject.Parse(@"{ + /// ""full_name"": ""김철수"", + /// ""employee_age"": 30, + /// ""isActive"": true, + /// ""height"": 175.5 + /// }"); + /// + /// var mapper = new DataMapper(mask); + /// DataObject result = mapper.Mapping(sourceJson); + /// // result는 변환된 필드 이름과 타입을 가진 DataObject 객체 + /// + /// public class DataMask : JObject { /// - /// DataObject의 Id에 해당하는 key 문자열 + /// DataObject의 Id에 해당하는 key 문자열입니다. + /// 이 속성은 매핑된 DataObject에서 고유 식별자로 사용될 필드를 지정합니다. + /// null인 경우 DataObject의 기본 동작을 따릅니다. /// + /// + /// + /// var mask = new DataMask(); + /// mask.ObjectIdKey = "userId"; // DataObject에서 "userId" 필드가 Id로 사용됨 + /// + /// public string? ObjectIdKey { get; set; } = null; /// /// DataObject의 이름을 나타내는 속성입니다. DataRepository에서 사용됩니다. + /// 이 이름은 매핑된 DataObject를 분류하거나 식별하는 데 사용될 수 있습니다. /// + /// + /// + /// var mask = new DataMask(); + /// mask.ObjectName = "users"; // 매핑된 DataObject는 "users"라는 이름을 가짐 + /// + /// public string ObjectName { get; set; } = string.Empty; /// - /// 교체 할 이름을 나타내는 딕셔너리입니다. + /// 원본 JSON에서 매핑된 DataObject로 필드 이름을 변환하는 규칙을 정의하는 딕셔너리입니다. + /// 키는 원본 JSON의 필드 이름이고, 값은 변환될 대상 필드 이름입니다. /// + /// + /// 이 속성을 통해 서로 다른 명명 규칙을 사용하는 JSON 구조 간의 매핑을 용이하게 할 수 있습니다. + /// 예를 들어, snake_case를 사용하는 API 응답을 camelCase로 변환하는 데 사용할 수 있습니다. + /// + /// + /// + /// var mask = new DataMask(); + /// mask.NamesForReplace = new Dictionary<string, string> + /// { + /// { "first_name", "firstName" }, + /// { "last_name", "lastName" }, + /// { "birth_date", "birthDate" } + /// }; + /// + /// public Dictionary? NamesForReplace { get; set; } } diff --git a/Assets/Scripts/UVC/Data/DataObject.cs b/Assets/Scripts/UVC/Data/DataObject.cs index f160e42a..d34d1b9a 100644 --- a/Assets/Scripts/UVC/Data/DataObject.cs +++ b/Assets/Scripts/UVC/Data/DataObject.cs @@ -8,6 +8,44 @@ using System.Linq; namespace UVC.Data { + /// + /// 키-값 쌍의 데이터를 관리하고 변경 사항을 추적하는 동적 데이터 객체입니다. + /// SortedDictionary를 상속하여 키를 기준으로 정렬된 데이터를 제공합니다. + /// + /// + /// 이 클래스는 JSON과 호환되는 데이터 구조를 표현하며, 데이터 변경 추적 기능을 통해 + /// 효율적인 데이터 동기화를 지원합니다. + /// + /// + /// + /// // DataMask 생성 및 설정 + /// var mask = new DataMask(); + /// mask.ObjectIdKey = "id"; + /// mask.ObjectName = "users"; + /// + /// // 필드 이름 변환 설정 + /// mask.NamesForReplace = new Dictionary<string, string> + /// { + /// { "userName", "name" }, + /// { "userEmail", "email" } + /// }; + /// + /// // JObject 속성으로 마스킹 규칙 추가 + /// mask["include"] = new JArray("name", "email", "age"); + /// mask["exclude"] = new JArray("password", "token"); + /// + /// // DataObject에 마스크 적용하는 예시 + /// var dataObject = new DataObject(); + /// dataObject["userName"] = "홍길동"; + /// dataObject["userEmail"] = "hong@example.com"; + /// dataObject["password"] = "secret123"; + /// + /// // 마스크 적용 결과 (개념적 예시): + /// // - userName은 name으로 변환됨 + /// // - userEmail은 email로 변환됨 + /// // - password는 제외됨 + /// + /// public class DataObject : SortedDictionary, IDataObject { @@ -27,7 +65,9 @@ namespace UVC.Data public string Name { get; internal set; } = string.Empty; - // 직접적인 변경이 있었던 키를 저장하는 리스트 + /// + /// 직접적인 변경이 있었던 키를 저장하는 리스트입니다. + /// protected List changedProperies = new List(); /// @@ -132,7 +172,7 @@ namespace UVC.Data } /// - /// 새 속성을 추가할 때 이벤트 연결을 처리합니다. + /// 새 속성을 추가하고 변경 사항을 추적합니다. /// /// 추가할 속성의 이름 /// 속성의 값 @@ -152,9 +192,11 @@ namespace UVC.Data } /// - /// 인덱서를 통한 속성 설정을 처리합니다. - /// 속성 변경 시 변경 사항을 자동으로 추적합니다. + /// 인덱서를 통해 속성값에 접근하고 설정합니다. + /// 속성값이 변경될 때마다 변경 사항을 자동으로 추적합니다. /// + /// 접근할 속성의 키 + /// 속성값 public new object this[string key] { get => base[key]; @@ -184,7 +226,12 @@ namespace UVC.Data } } - + /// + /// 지정된 속성의 값을 정수(int)로 변환하여 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 + /// 변환된 정수 값 또는 기본값 public int GetInt(string propertyName, int defaultValue = 0) { if (TryGetValue(propertyName, out object value) && value != null) @@ -196,6 +243,12 @@ namespace UVC.Data return defaultValue; } + /// + /// 지정된 속성의 값을 문자열(string)로 변환하여 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 null인 경우 반환할 기본값 + /// 변환된 문자열 값 또는 기본값 public string? GetString(string propertyName, string? defaultValue = null) { if (TryGetValue(propertyName, out object value) && value != null) @@ -205,6 +258,12 @@ namespace UVC.Data return defaultValue; } + /// + /// 지정된 속성의 값을 불리언(bool)으로 변환하여 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 + /// 변환된 불리언 값 또는 기본값 public bool GetBool(string propertyName, bool defaultValue = false) { if (TryGetValue(propertyName, out object value) && value != null) @@ -216,6 +275,12 @@ namespace UVC.Data return defaultValue; } + /// + /// 지정된 속성의 값을 부동 소수점(float)으로 변환하여 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 + /// 변환된 부동 소수점 값 또는 기본값 public float GetFloat(string propertyName, float defaultValue = 0f) { if (TryGetValue(propertyName, out object value) && value != null) @@ -227,6 +292,12 @@ namespace UVC.Data return defaultValue; } + /// + /// 지정된 속성의 값을 더블 정밀도 부동 소수점(double)으로 변환하여 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 + /// 변환된 더블 값 또는 기본값 public double GetDouble(string propertyName, double defaultValue = 0.0) { if (TryGetValue(propertyName, out object value) && value != null) @@ -238,6 +309,12 @@ namespace UVC.Data return defaultValue; } + /// + /// 지정된 속성의 값을 DateTime으로 변환하여 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 + /// 변환된 DateTime 값 또는 기본값 public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null) { if (TryGetValue(propertyName, out object value) && value != null) @@ -249,6 +326,13 @@ namespace UVC.Data return defaultValue; } + /// + /// 지정된 속성의 값을 열거형(Enum)으로 변환하여 반환합니다. + /// + /// 변환할 열거형 타입 + /// 속성 이름 + /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 + /// 변환된 열거형 값 또는 기본값 public T GetEnum(string propertyName, T defaultValue = default) where T : Enum { if (TryGetValue(propertyName, out object value) && value != null) @@ -260,6 +344,27 @@ namespace UVC.Data return defaultValue; } + /// + /// 지정된 속성의 값을 IDataObject로 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 IDataObject가 아닌 경우 반환할 기본값 + /// IDataObject 인터페이스를 구현한 객체 또는 기본값 + public IDataObject? Get(string propertyName, IDataObject? defaultValue = null) + { + if (TryGetValue(propertyName, out object value) && value != null) + { + if (value is IDataObject dataObject) return dataObject; + } + return defaultValue; + } + + /// + /// 지정된 속성의 값을 DataArray로 변환하여 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 + /// DataArray 객체 또는 기본값 public DataArray? GetDataArray(string propertyName, DataArray? defaultValue = null) { if (TryGetValue(propertyName, out object value) && value != null) @@ -272,6 +377,12 @@ namespace UVC.Data return defaultValue; } + /// + /// 지정된 속성의 값을 DataObject로 변환하여 반환합니다. + /// + /// 속성 이름 + /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 + /// DataObject 객체 또는 기본값 public DataObject? GetDataObject(string propertyName, DataObject? defaultValue = null) { if (TryGetValue(propertyName, out object value) && value != null) @@ -303,7 +414,7 @@ namespace UVC.Data } /// - /// 모든 속성을 제거할 때 기존 속성 목록과 변경된 키 목록도 초기화합니다. + /// 모든 속성을 제거하고 추적 리스트를 초기화합니다. /// public void RemoveAll() { @@ -362,6 +473,17 @@ namespace UVC.Data return updated; } + /// + /// 업데이트 된 속성의 수. + /// + /// 업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다. + public int UpdatedCount { get => changedProperies.Count; } + + /// + /// DataObject의 내용을 문자열로 반환합니다. + /// 각 키-값 쌍이 "키:값" 형식으로 쉼표로 구분되어 표시됩니다. + /// + /// DataObject의 내용을 나타내는 문자열 public override string ToString() { return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}")); diff --git a/Assets/Scripts/UVC/Data/DataRepository.cs b/Assets/Scripts/UVC/Data/DataRepository.cs index ae90f2df..96cd3a68 100644 --- a/Assets/Scripts/UVC/Data/DataRepository.cs +++ b/Assets/Scripts/UVC/Data/DataRepository.cs @@ -1,50 +1,398 @@ -using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; +using UnityEngine; namespace UVC.Data { + /// + /// 데이터 객체들을 키-값 쌍으로 관리하는 중앙 저장소입니다. + /// + /// + /// 이 클래스는 싱글톤 패턴으로 구현되어 있어 애플리케이션 전체에서 + /// 하나의 인스턴스만 존재합니다. IDataObject 인터페이스를 구현하는 + /// 모든 데이터 객체를 저장하고 검색할 수 있습니다. + /// public class DataRepository { #region Singleton + /// + /// DataRepository 싱글톤 인스턴스를 생성하는 지연 초기화 객체입니다. + /// protected static readonly Lazy instance = new Lazy(() => new DataRepository()); + /// + /// 외부에서의 인스턴스 생성을 방지하는 보호된 생성자입니다. + /// protected DataRepository() { } + /// + /// DataRepository의 단일 인스턴스에 대한 접근자입니다. + /// public static DataRepository Instance { get { return instance.Value; } } #endregion + /// + /// 키로 식별되는 데이터 객체를 저장하는 컬렉션입니다. + /// private Dictionary dataObjects = new Dictionary(); - public IDataObject AddData(string key, IDataObject dataObject) + /// + /// 스레드 동기화를 위한 잠금 객체입니다. + /// + private readonly object syncLock = new object(); + + /// + /// 데이터 업데이트 시 호출될 핸들러 함수들을 저장하는 딕셔너리입니다. + /// 각 키에 연결된 데이터가 업데이트될 때 해당 핸들러가 호출됩니다. + /// + private Dictionary> dataUpdateHandlers = new Dictionary>(); + + + /// + /// 저장소에 데이터 객체를 추가하거나 기존 객체를 업데이트합니다. + /// + /// 데이터 객체를 식별하는 고유 키 + /// 저장할 데이터 객체 + /// true인 경우 업데이트된 속성만 반환, false인 경우 전체 객체 반환 + /// 새로 추가된 객체 또는 업데이트된 기존 객체 + public IDataObject AddData(string key, IDataObject dataObject, bool updatedDataOnly = true) { - if (!dataObjects.ContainsKey(key)) + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다."); + + if (dataObject == null) + throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다."); + + lock (syncLock) { - dataObject.InitData(); - dataObjects.Add(key, dataObject); - return dataObject; - } - else - { - IDataObject obj = dataObjects[key]; - obj.UpdateDifferent(dataObject); - return obj.GetUpdatedObject(); + if (!dataObjects.ContainsKey(key)) + { + dataObject.InitData(); + dataObjects.Add(key, dataObject); + return dataObject; + } + else + { + IDataObject obj = dataObjects[key]; + obj.UpdateDifferent(dataObject); + var newDataObject = dataObject; + if (updatedDataOnly) newDataObject = obj.GetUpdatedObject(); + NotifyDataUpdate(key, newDataObject); + return newDataObject; + } } } - - + /// + /// 지정된 키에 해당하는 데이터 객체를 저장소에서 제거합니다. + /// + /// 제거할 데이터 객체의 키 public void RemoveData(string key) { - if (dataObjects.ContainsKey(key)) + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다."); + + lock (syncLock) { - dataObjects.Remove(key); + if (dataObjects.ContainsKey(key)) + { + dataObjects.Remove(key); + } } } + + /// + /// 지정된 키에 해당하는 데이터 객체를 저장소에서 검색합니다. + /// + /// 검색할 데이터 객체의 키 + /// 키가 존재하면 해당 데이터 객체, 존재하지 않으면 null public IDataObject GetData(string key) { - if (dataObjects.ContainsKey(key)) + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다."); + + lock (syncLock) { - return dataObjects[key]; + if (dataObjects.ContainsKey(key)) + { + return dataObjects[key]; + } + return null; } - return null; } + + /// + /// 지정된 키에 대한 데이터 업데이트 핸들러를 추가합니다. + /// + /// 데이터 업데이트를 감시할 키 + /// 데이터가 업데이트되었을 때 호출될 콜백 함수 + /// + /// 같은 키에 대해 여러 핸들러를 등록할 수 있습니다. + /// 핸들러는 해당 키의 데이터가 AddData를 통해 업데이트될 때마다 호출됩니다. + /// + public void AddDataUpdateHandler(string key, Action handler) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다."); + if (handler == null) + throw new ArgumentNullException(nameof(handler), "핸들러는 null일 수 없습니다."); + lock (syncLock) + { + if (!dataUpdateHandlers.ContainsKey(key)) + { + dataUpdateHandlers[key] = handler; + } + else + { + dataUpdateHandlers[key] += handler; // 기존 핸들러에 추가 + } + } + } + + /// + /// 지정된 키에서 데이터 업데이트 핸들러를 제거합니다. + /// + /// 핸들러를 제거할 데이터 키 + /// 제거할 핸들러 함수 + /// + /// 지정된 키에 연결된 핸들러가 없거나, 제거하려는 핸들러가 등록되지 않은 경우 아무 작업도 수행하지 않습니다. + /// 키에 연결된 마지막 핸들러가 제거되면 해당 키는 dataUpdateHandlers 딕셔너리에서 삭제됩니다. + /// + public void RemoveDataUpdateHandler(string key, Action handler) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다."); + if (handler == null) + throw new ArgumentNullException(nameof(handler), "핸들러는 null일 수 없습니다."); + lock (syncLock) + { + if (dataUpdateHandlers.ContainsKey(key)) + { + dataUpdateHandlers[key] -= handler; // 기존 핸들러에서 제거 + if (dataUpdateHandlers[key] == null) + { + dataUpdateHandlers.Remove(key); // 핸들러가 없으면 제거 + } + } + } + } + + /// + /// 모든 데이터 업데이트 핸들러를 제거합니다. + /// + /// + /// 이 메서드는 모든 키에 대한 모든 핸들러를 한 번에 제거합니다. + /// 예를 들어, 씬이 변경되거나 애플리케이션이 종료될 때 + /// 모든 핸들러를 정리하는 데 유용합니다. + /// + public void ClearDataUpdateHandlers() + { + lock (syncLock) + { + dataUpdateHandlers.Clear(); + } + } + + /// + /// 지정된 키의 데이터가 업데이트되었음을 알리고 등록된 핸들러들을 호출합니다. + /// + /// 업데이트된 데이터의 키 + /// 업데이트된 데이터 객체 + /// + /// 이 메서드는 주로 내부적으로 AddData 메서드에서 호출되어 + /// 특정 키의 데이터가 변경되었을 때 등록된 핸들러들에게 알립니다. + /// + private void NotifyDataUpdate(string key, IDataObject dataObject) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다."); + if (dataObject == null) + throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다."); + lock (syncLock) + { + if (dataUpdateHandlers.ContainsKey(key)) + { + dataUpdateHandlers[key]?.Invoke(dataObject); + } + } + } + + /// + /// 저장소에 저장된 모든 데이터 객체의 키 목록을 반환합니다. + /// + /// 저장소에 저장된 모든 데이터 객체의 키 목록 + public IEnumerable GetAllKeys() + { + lock (syncLock) + { + return new List(dataObjects.Keys); + } + } + + /// + /// 저장된 모든 데이터 객체를 반환합니다. + /// + /// 저장소의 모든 데이터 객체 + public IEnumerable GetAllData() + { + lock (syncLock) + { + return new List(dataObjects.Values); + } + } + + /// + /// 지정된 키에 대한 데이터 객체가 저장소에 존재하는지 확인합니다. + /// + /// 확인할 키 + /// 키가 존재하면 true, 그렇지 않으면 false + public bool ContainsKey(string key) + { + lock (syncLock) + { + return dataObjects.ContainsKey(key); + } + } + + /// + /// 저장소의 모든 데이터를 삭제합니다. + /// + public void Clear() + { + lock (syncLock) + { + dataObjects.Clear(); + dataUpdateHandlers.Clear(); + } + } + + + /// + /// 지정된 키의 데이터 객체를 특정 타입으로 변환하여 반환합니다. + /// + /// 변환할 타입 (IDataObject를 구현해야 함) + /// 검색할 키 + /// 변환된 객체 또는 null + public T GetDataAs(string key) where T : class, IDataObject + { + lock (syncLock) + { + if (dataObjects.ContainsKey(key) && dataObjects[key] is T typedObject) + { + return typedObject; + } + return null; + } + } + + /// + /// 지정된 조건을 만족하는 첫 번째 데이터 객체를 찾습니다. + /// + /// 데이터 객체를 필터링할 조건 + /// 조건을 만족하는 첫 번째 데이터 객체와 해당 키, 없으면 null + public (string Key, IDataObject Data) FindFirst(Func predicate) + { + if (predicate == null) + throw new ArgumentNullException(nameof(predicate)); + + lock (syncLock) + { + foreach (var pair in dataObjects) + { + if (predicate(pair.Value)) + { + return (pair.Key, pair.Value); + } + } + return (null, null); + } + } + + /// + /// 지정된 조건을 만족하는 모든 데이터 객체를 찾습니다. + /// + /// 데이터 객체를 필터링할 조건 + /// 조건을 만족하는 모든 데이터 객체와 해당 키 + public IEnumerable<(string Key, IDataObject Data)> FindAll(Func predicate) + { + if (predicate == null) + throw new ArgumentNullException(nameof(predicate)); + + lock (syncLock) + { + List<(string, IDataObject)> result = new List<(string, IDataObject)>(); + foreach (var pair in dataObjects) + { + if (predicate(pair.Value)) + { + result.Add((pair.Key, pair.Value)); + } + } + return result; + } + } + + /// + /// 저장소의 전체 내용을 JSON 문자열로 직렬화합니다. + /// + /// 저장소 데이터의 JSON 표현 + public string ToJson() + { + JObject result = new JObject(); + + lock (syncLock) + { + foreach (var pair in dataObjects) + { + if (pair.Value is DataObject dataObject) + { + result[pair.Key] = JObject.FromObject(dataObject); + } + else if (pair.Value is DataArray dataArray) + { + result[pair.Key] = JArray.FromObject(dataArray); + } + } + } + + return result.ToString(); + } + + /// + /// JSON 문자열에서 데이터를 불러와 저장소에 추가합니다. + /// + /// 저장소 데이터의 JSON 표현 + public void LoadFromJson(string json) + { + if (string.IsNullOrEmpty(json)) + return; + + try + { + JObject data = JObject.Parse(json); + + foreach (var property in data.Properties()) + { + string key = property.Name; + JToken value = property.Value; + + if (value is JObject jObject) + { + DataObject dataObject = new DataObject(jObject); + AddData(key, dataObject, false); + } + else if (value is JArray jArray) + { + DataArray dataArray = new DataArray(jArray); + AddData(key, dataArray, false); + } + } + } + catch (JsonException ex) + { + Debug.LogError($"JSON 파싱 오류: {ex.Message}"); + } + } + } } diff --git a/Assets/Scripts/UVC/Data/HttpPipeLine.cs b/Assets/Scripts/UVC/Data/HttpPipeLine.cs index 9c54db91..c685c03f 100644 --- a/Assets/Scripts/UVC/Data/HttpPipeLine.cs +++ b/Assets/Scripts/UVC/Data/HttpPipeLine.cs @@ -1,14 +1,114 @@ #nullable enable +using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using UnityEngine; using UVC.Network; +using UVC.Tests; namespace UVC.Data { + /// + /// HTTP 요청 파이프라인을 관리하는 클래스입니다. + /// + /// + /// 이 클래스는 HTTP 요청의 실행 및 반복 요청을 관리합니다. + /// 등록된 요청(HttpPipeLineInfo)을 키 값으로 관리하며, + /// 주기적 데이터 수집을 위한 반복 요청 기능을 제공합니다. + /// + /// 주요 기능: + /// - 단일 및 반복 HTTP 요청 관리 + /// - 요청 결과의 JSON 데이터를 IDataObject로 변환 + /// - 안전한 요청 취소 및 자원 정리 + /// - 테스트를 위한 목업 기능 지원 + /// + /// 모든 반복 실행은 CancellationTokenSource를 통해 취소할 수 있으며, + /// 취소 후 현재 진행 중인 모든 요청이 안전하게 완료되는 것을 보장합니다. + /// + /// + /// + /// // HttpPipeLine 인스턴스 생성 + /// var httpPipeline = new HttpPipeLine(); + /// + /// // 데이터 매퍼 설정 (응답 데이터 변환용) + /// var dataMask = new DataMask(); + /// dataMask["name"] = "이름"; + /// dataMask["value"] = 0; + /// var dataMapper = new DataMapper(dataMask); + /// + /// // 단일 요청 설정 및 등록 + /// var singleRequest = new HttpPipeLineInfo("https://api.example.com/data") + /// .setDataMapper(dataMapper) + /// .setHandler(data => { + /// // 데이터 처리 로직 + /// Debug.Log($"데이터 수신: {data?.ToString() ?? "null"}"); + /// }); + /// httpPipeline.Add("dataRequest", singleRequest); + /// + /// // 반복 요청 설정 및 등록 + /// var repeatingRequest = new HttpPipeLineInfo("https://api.example.com/status") + /// .setDataMapper(dataMapper) + /// .setHandler(data => { + /// // 상태 데이터 처리 + /// Debug.Log($"상태 업데이트: {data?.ToString() ?? "null"}"); + /// }) + /// .setRepeat(true, 0, 5000); // 5초마다 무한 반복 + /// httpPipeline.Add("statusMonitor", repeatingRequest); + /// + /// // 요청 실행 + /// await httpPipeline.Excute("dataRequest"); // 단일 실행 + /// await httpPipeline.Excute("statusMonitor"); // 반복 실행 시작 + /// + /// // 나중에 반복 요청 중지 + /// httpPipeline.StopRepeat("statusMonitor"); + /// + /// // 더 이상 필요없는 요청 제거 + /// httpPipeline.Remove("dataRequest"); + /// + /// public class HttpPipeLine { + /// + /// 테스트를 위한 목업 모드 활성화 여부를 설정하거나 가져옵니다. + /// + /// + /// true로 설정하면 실제 HTTP 요청 대신 MockHttpRequester를 사용합니다. + /// 테스트 환경에서 외부 의존성 없이 HTTP 요청을 시뮬레이션할 때 유용합니다. + /// + public bool UseMockup { get; internal set; } = false; + + /// + /// 등록된 HTTP 파이프라인 정보를 저장하는 사전 + /// private Dictionary infoList = new Dictionary(); + /// + /// 실행 중인 반복 작업의 취소 토큰을 관리하는 사전 + /// + private Dictionary repeatTokenSources = new Dictionary(); + + /// + /// 진행 중인 요청의 상태를 추적하는 사전입니다. + /// + /// + /// 키는 요청 식별자이고, 값은 현재 요청이 실행 중인지 여부를 나타냅니다. + /// 이 상태 추적은 StopRepeat 메서드가 요청의 완전한 종료를 보장하기 위해 사용됩니다. + /// + private Dictionary requestInProgress = new Dictionary(); + + /// + /// 새로운 HTTP 요청 정보를 추가하거나 기존 정보를 업데이트합니다. + /// + /// 요청을 식별하는 키 + /// HTTP 요청 정보 + /// + /// 동일한 키가 이미 존재하는 경우 새로운 정보로 대체됩니다. + /// public void Add(string key, HttpPipeLineInfo info) { if (!infoList.ContainsKey(key)) @@ -21,42 +121,448 @@ namespace UVC.Data } } - public void Remove(string key) + /// + /// 지정한 키의 HTTP 요청 정보를 제거합니다. + /// + /// 제거할 요청의 키 + /// + /// 실행 중인 반복 작업이 있다면 함께 중지됩니다. + /// + public async UniTask RemoveAsync(string key) { if (infoList.ContainsKey(key)) { + await StopRepeat(key); infoList.Remove(key); } } - public async void Excute(string key) + /// + /// 지정한 키의 HTTP 요청을 실행합니다. + /// + /// 실행할 요청의 키 + /// 비동기 작업 + /// + /// 요청 정보의 repeat 속성에 따라 단일 실행 또는 반복 실행을 시작합니다. + /// 이미 실행 중인 반복 작업이 있다면 먼저 중지하고 완료를 대기한 후 새로운 요청을 시작합니다. + /// 단일 실행의 경우 완료될 때까지 대기하지만, 반복 실행은 백그라운드에서 실행됩니다. + /// + /// 지정된 키가 등록되어 있지 않은 경우 + public async UniTask Excute(string key) { if (infoList.ContainsKey(key)) { HttpPipeLineInfo info = infoList[key]; - - string result = await HttpRequester.Request(info.url, info.method, info.body, info.headers); - IDataObject? dataObject = null; - if (!string.IsNullOrEmpty(result)) + // 반복 설정에 관계없이 이전에 실행 중인 반복 작업이 있다면 중지 + await StopRepeat(key); + + if (!info.Repeat) { - result = result.Trim(); - if (result.StartsWith("{")) + // 단일 실행 로직 호출 + await ExecuteSingle(key, info); + } + else + { + // 반복 설정이 있는 경우에만 StartRepeat 호출 + StartRepeat(key).Forget(); + } + } + else + { + throw new KeyNotFoundException($"No HTTP request found with key '{key}'."); + } + } + + /// + /// 단일 HTTP 요청을 실행하고 결과를 처리합니다. + /// + /// 요청을 식별하는 키 + /// HTTP 요청 정보 + /// 요청 취소를 위한 취소 토큰 + /// 비동기 작업 + /// + /// 이 메서드는 HTTP 요청을 보내고, 응답 데이터를 파싱하여 IDataObject로 변환합니다. + /// JSON 객체 또는 배열 형식의 응답을 처리할 수 있으며, 취소 토큰을 통해 언제든지 작업을 취소할 수 있습니다. + /// + /// 작업이 취소된 경우 발생 + /// JSON 응답 파싱 중 오류가 발생한 경우 + /// HTTP 요청 중 다른 예외가 발생한 경우 + private async UniTask ExecuteSingle(string key, HttpPipeLineInfo info, CancellationToken cancellationToken = default) + { + + int retryCount = 0; + Exception lastException = null; + + while (retryCount <= info.MaxRetryCount) + { + // 취소 요청 확인 + if (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException("Operation cancelled", cancellationToken); + } + + lock (requestInProgress) + { + requestInProgress[key] = true; + } + + try + { + string result = string.Empty; + if (!UseMockup) { - JObject source = JObject.Parse(result); - if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source); + result = await HttpRequester.Request(info.Url, info.Method, info.Body, info.Headers); } - else if(result.StartsWith("[")) + else { - JArray source = JArray.Parse(result); - if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source); + result = await MockHttpRequester.Request(info.Url, info.Method, info.Body, info.Headers); + } + + // 응답 처리 전에 다시 취소 요청 확인 + if (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException("Operation cancelled", cancellationToken); + } + + IDataObject? dataObject = null; + if (!string.IsNullOrEmpty(result)) + { + result = result.Trim(); + try + { + if (result.StartsWith("{")) + { + if (info.DataMapper != null) + { + if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength) + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result))) + { + dataObject = info.DataMapper.MapObjectStream(stream); + } + } + else + { + JObject source = JObject.Parse(result); + dataObject = info.DataMapper.Mapping(source); + } + } + } + else if (result.StartsWith("[")) + { + if (info.DataMapper != null) + { + if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength) + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result))) + { + dataObject = info.DataMapper.MapArrayStream(stream); + } + } + else + { + JArray source = JArray.Parse(result); + dataObject = info.DataMapper.Mapping(source); + } + } + } + } + catch (JsonException ex) + { + Debug.LogError($"JSON parsing error in ExecuteSingle for {key}: {ex.Message}\nResponse: {result}"); + throw; // 상위에서 처리하도록 다시 throw + } + } + + // 핸들러 호출 전에 다시 취소 요청 확인 + if (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException("Operation cancelled", cancellationToken); + } + if (dataObject != null) dataObject = DataRepository.Instance.AddData(key, dataObject, info.UpdatedDataOnly); + + // 갱신 된 데이터가 있는 경우 핸들러 호출 + if (info.UpdatedDataOnly) + { + if (dataObject != null && dataObject.UpdatedCount > 0) info.Handler?.Invoke(dataObject); + } + else + { + info.Handler?.Invoke(dataObject); + } + return; + } + catch (OperationCanceledException) + { + // 취소 예외는 그대로 전파 + throw; + } + catch (Exception ex) + { + lastException = ex; + retryCount++; + + if (retryCount <= info.MaxRetryCount) + { + // 재시도 전에 취소 요청 확인 + if (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException("Operation cancelled", cancellationToken); + } + Debug.LogWarning($"Request failed for '{key}', retry {retryCount}/{info.MaxRetryCount} after {info.RetryDelay}ms: {ex.Message}"); + await UniTask.Delay(info.RetryDelay); + } + } + finally + { + lock (requestInProgress) + { + requestInProgress[key] = false; } } - if(dataObject != null) dataObject = DataRepository.Instance.AddData(key, dataObject); - info.handler?.Invoke(dataObject); } - + + // 모든 재시도 후에도 실패 + Debug.LogError($"Request failed for '{key}' after {info.MaxRetryCount} retries: {lastException?.Message}"); + throw lastException; + } + + /// + /// 반복 실행을 시작합니다. + /// + /// 반복 실행할 요청의 키 + /// 비동기 작업 + /// + /// 지정된 간격(repeatInterval)으로 HTTP 요청을 반복 실행합니다. + /// repeatCount가 0인 경우 무한 반복하며, 0보다 큰 경우 지정된 횟수만큼만 실행합니다. + /// 작업 실행 중 예외가 발생하면 로그를 기록하고 다음 실행을 시도합니다. + /// 취소 요청이 있거나 최대 실행 횟수에 도달하면 반복이 종료됩니다. + /// + /// 지정된 키가 등록되어 있지 않은 경우 + private async UniTask StartRepeat(string key) + { + if (!infoList.ContainsKey(key)) + { + throw new KeyNotFoundException($"No HTTP request found with key '{key}'."); + return; + } + + HttpPipeLineInfo info = infoList[key]; + if (!info.Repeat) return; + + // 새 취소 토큰 생성 + CancellationTokenSource cts = new CancellationTokenSource(); + repeatTokenSources[key] = cts; + + int executionCount = 0; + + try + { + while (!cts.IsCancellationRequested) + { + try + { + // 단일 실행 로직 호출 + await ExecuteSingle(key, info, cts.Token); + + // 지정된 횟수만큼 반복한 경우 중지 + if (info.RepeatCount > 0) + { + executionCount++; + if (executionCount >= info.RepeatCount) + { + break; + } + } + // 토큰이 취소되지 않은 경우에만 지연 + if (!cts.IsCancellationRequested) + { + // 지정된 간격만큼 대기 + await UniTask.Delay(info.RepeatInterval, cancellationToken: cts.Token); + } + } + catch (OperationCanceledException) + { + // 취소된 경우 루프 종료 + break; + } + catch (Exception ex) + { + // 다른 예외 처리 + Debug.LogError($"Error in repeat execution for '{key}': {ex.Message}"); + if (cts.IsCancellationRequested) + { + break; + } + + try + { + await UniTask.Delay(info.RepeatInterval, cancellationToken: cts.Token); + } + catch (OperationCanceledException) + { + // 취소된 경우 무시하고 루프 종료 + break; + } + } + } + } + finally + { + lock (repeatTokenSources) // 스레드 안전성 확보 + { + if (repeatTokenSources.TryGetValue(key, out var currentCts) && currentCts == cts) + { + repeatTokenSources.Remove(key); + } + } + cts.Dispose(); // 여기서 최종적으로 Dispose 호출 + } + } + + /// + /// 반복 실행 중인 요청을 중지합니다. + /// + /// 중지할 요청의 키 + /// 요청 중지 작업을 나타내는 비동기 작업 + /// + /// 해당 키로 실행 중인 반복 작업이 없는 경우 아무 작업도 수행하지 않습니다. + /// 요청이 중지되었더라도 현재 실행 중인 작업이 완전히 종료될 때까지 대기합니다. + /// 이를 통해 작업 중단 후 자원이 안전하게 정리되는 것을 보장합니다. + /// + public async UniTask StopRepeat(string key) + { + CancellationTokenSource cts = null; + lock (repeatTokenSources) // 스레드 안전성 확보 + { + if (repeatTokenSources.TryGetValue(key, out cts) && !cts.IsCancellationRequested) + { + cts.Cancel(); + repeatTokenSources.Remove(key); + } + } + + // 진행 중인 요청이 완료될 때까지 대기 + if (cts != null) + { + while (true) + { + lock (requestInProgress) + { + if (!requestInProgress.ContainsKey(key) || !requestInProgress[key]) + { + break; + } + } + await UniTask.Delay(10); + } + } + } + + /// + /// 모든 반복 실행 중인 요청을 중지합니다. + /// + /// + /// 애플리케이션 종료 시 또는 모든 반복 작업을 일괄 중지해야 할 때 사용합니다. + /// 이 메서드는 비동기적으로 작동하지만 완료를 대기하지 않습니다. + /// 모든 작업이 완전히 종료될 때까지 기다려야 하는 경우, 각 키에 대해 개별적으로 StopRepeat를 호출하고 대기해야 합니다. + /// + public async UniTask StopAllRepeats() + { + foreach (var key in new List(repeatTokenSources.Keys)) + { + await StopRepeat(key); + } + } + + + /// + /// 현재 활성화된 요청 목록과 상태 정보를 반환합니다. + /// + /// 키와 요청 상태 정보를 포함하는 딕셔너리 + /// + /// 반환되는 딕셔너리는 등록된 모든 HTTP 요청에 대한 상태 정보를 제공합니다. + /// 각 요청에 대해 활성 상태, 반복 설정, 반복 횟수, 반복 간격을 확인할 수 있습니다. + /// + /// + /// + /// var httpPipeline = new HttpPipeLine(); + /// // 파이프라인에 요청 추가 후... + /// + /// // 모든 활성 요청 확인 + /// var activeRequests = httpPipeline.GetActiveRequests(); + /// foreach (var request in activeRequests) + /// { + /// Debug.Log($"요청 키: {request.Key}, 활성 상태: {request.Value.IsActive}, " + + /// $"반복 중: {request.Value.IsRepeating}, 반복 간격: {request.Value.RepeatInterval}ms"); + /// } + /// + /// + public Dictionary GetActiveRequests() + { + var result = new Dictionary(); + foreach (var key in infoList.Keys) + { + bool isRepeating = repeatTokenSources.ContainsKey(key); + result[key] = new HttpPipeLineRequestStatus + { + IsActive = isRepeating, + IsRepeating = isRepeating, + RepeatCount = isRepeating ? infoList[key].RepeatCount : 0, + RepeatInterval = isRepeating ? infoList[key].RepeatInterval : 0 + }; + } + return result; + } + + /// + /// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다. + /// + /// 이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다. + /// 를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다. + public void Dispose() + { + // 모든 반복 작업 중지 + StopAllRepeats().Forget(); + // 요청 상태 초기화 + requestInProgress.Clear(); + // 등록된 요청 정보 초기화 + infoList.Clear(); + // 취소 토큰 소스 정리 + foreach (var cts in repeatTokenSources.Values) + { + cts.Dispose(); + } + repeatTokenSources.Clear(); } } + + /// + /// HTTP 요청의 현재 상태 정보를 나타내는 클래스입니다. + /// + /// + /// 이 클래스는 HTTP 파이프라인에 등록된 요청의 활성 상태, 반복 설정, + /// 반복 횟수, 반복 간격에 관한 정보를 제공합니다. + /// + public class HttpPipeLineRequestStatus + { + /// + /// 요청이 현재 활성 상태인지 여부를 나타냅니다. + /// + public bool IsActive { get; set; } + /// + /// 요청이 반복 실행 중인지 여부를 나타냅니다. + /// + public bool IsRepeating { get; set; } + /// + /// 반복 설정된 횟수를 나타냅니다. 0은 무한 반복을 의미합니다. + /// + public int RepeatCount { get; set; } + /// + /// 반복 요청 간의 간격을 밀리초 단위로 나타냅니다. + /// + public int RepeatInterval { get; set; } + } } diff --git a/Assets/Scripts/UVC/Data/HttpPipeLineInfo.cs b/Assets/Scripts/UVC/Data/HttpPipeLineInfo.cs index d4120245..7c7fb204 100644 --- a/Assets/Scripts/UVC/Data/HttpPipeLineInfo.cs +++ b/Assets/Scripts/UVC/Data/HttpPipeLineInfo.cs @@ -5,47 +5,185 @@ using System.Collections.Generic; namespace UVC.Data { + /// + /// HTTP 요청 파이프라인 정보를 관리하는 클래스입니다. + /// 요청 URL, 메서드, 헤더, 본문과 같은 HTTP 요청 정보 및 + /// 반복 실행, 재시도, 데이터 매핑 등 파이프라인 동작을 구성합니다. + /// + /// + /// 이 클래스는 파이프라인 빌더 패턴을 사용하여 HTTP 요청 설정을 구성할 수 있도록 합니다. + /// 각 설정 메서드는 체이닝을 통해 유연하게 요청을 정의할 수 있습니다. + /// + /// + /// + /// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/data", "GET") + /// .setDataMapper(new DataMapper(dataMask)) + /// .setHandler(data => Console.WriteLine(data)) + /// .setRetry(5, 2000) + /// .setRepeat(true, 10, 5000); + /// + /// public class HttpPipeLineInfo { - public string url; - public string method; - public Dictionary? headers = null; - public string? body = null; - public Action? handler = null; - public bool repeat = false; // 반복 실행 여부 - public int repeatCount = 0; // 반복 횟수, 0은 무한 반복 - public int repeatInterval = 1000; // 반복 간격 (ms) + private string _url; + private string _method; + private Dictionary? _headers = null; + private string? _body = null; + private Action? _handler = null; + private bool _repeat = false; // 반복 실행 여부 + private int _repeatCount = 0; // 반복 횟수, 0은 무한 반복 + private int _repeatInterval = 1000; // 반복 간격 (ms) + private DataMapper? _dataMapper = null; // 데이터 매퍼 + private int _maxRetryCount = 3; + private int _retryDelay = 1000; // 밀리초 + private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부 - public DataMapper? dataMapper = null; // 데이터 매퍼 + /// + /// 요청을 보낼 URL 주소 + /// + public string Url => _url; + /// + /// HTTP 요청 메서드 (GET, POST 등) + /// + public string Method => _method; + + /// + /// HTTP 요청 헤더 + /// + public Dictionary? Headers => _headers; + + /// + /// HTTP 요청 본문 + /// + public string? Body => _body; + + /// + /// 요청 완료 후 호출될 핸들러 + /// + public Action? Handler => _handler; + + /// + /// 반복 실행 여부 + /// + public bool Repeat => _repeat; + + /// + /// 반복 횟수, 0은 무한 반복 + /// + public int RepeatCount => _repeatCount; + + /// + /// 반복 간격 (ms) + /// + public int RepeatInterval => _repeatInterval; + + /// + /// 데이터 매퍼 객체 + /// + public DataMapper? DataMapper => _dataMapper; + + /// + /// 최대 재시도 횟수 + /// + public int MaxRetryCount => _maxRetryCount; + + /// + /// 재시도 간격 (ms) + /// + public int RetryDelay => _retryDelay; + + /// + /// 업데이트된 데이터만 받을 여부 (true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다) + /// + public bool UpdatedDataOnly => _updatedDataOnly; + + /// + /// HttpPipeLineInfo 클래스의 새 인스턴스를 초기화합니다. + /// + /// HTTP 요청을 보낼 URL + /// HTTP 요청 메서드 (기본값: "post") + /// HTTP 요청 헤더 (선택사항) + /// HTTP 요청 본문 (선택사항) public HttpPipeLineInfo(string url, string method = "post", Dictionary? headers = null, string? body = null) { - this.url = url; - this.method = method; - this.headers = headers; - this.body = body; + _url = url; + _method = method; + _headers = headers; + _body = body; } + /// + /// HTTP 요청 응답을 처리할 데이터 매퍼를 설정합니다. + /// 데이터 매퍼는 HTTP 응답을 IDataObject로 변환하는 역할을 합니다. + /// + /// 사용할 데이터 매퍼 객체 + /// 현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용) public HttpPipeLineInfo setDataMapper(DataMapper dataMapper) { - this.dataMapper = dataMapper; + _dataMapper = dataMapper; return this; } - public HttpPipeLineInfo setHandler(Action handler) + /// + /// HTTP 요청이 완료된 후 호출될 핸들러를 설정합니다. + /// 변경 된 데이터는 IDataObject로 전달됩니다. + /// 변경 된 데이터가 없으면 호출 되지 않습니다. + /// + /// 응답 데이터를 처리할 콜백 함수 + /// 현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용) + public HttpPipeLineInfo setHandler(Action? handler) { - this.handler = handler; + _handler = handler; return this; } - public HttpPipeLineInfo setRepeat(bool repeat, int count = 0, int interval = 1000) + /// + /// HTTP 요청 실패 시 재시도 정책을 설정합니다. + /// + /// 최대 재시도 횟수 (기본값: 3) + /// 재시도 간 대기 시간(밀리초) (기본값: 1000) + /// 현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용) + public HttpPipeLineInfo setRetry(int maxRetryCount = 3, int retryDelay = 1000) { - this.repeat = repeat; - this.repeatCount = count; - this.repeatInterval = interval; + _maxRetryCount = maxRetryCount; + _retryDelay = retryDelay; return this; } + /// + /// HTTP 요청의 반복 실행 설정을 구성합니다. + /// + /// 반복 실행 여부 + /// 반복 횟수 (0은 무한 반복) (기본값: 0) + /// 반복 실행 간 대기 시간(밀리초) (기본값: 1000) + /// 변경된 데이터만 처리할지 여부 (기본값: true) + /// 현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용) + /// + /// 반복 요청 시 updatedDataOnly가 true인 경우, 서버 응답에서 데이터가 변경된 경우에만 핸들러가 호출됩니다. + /// 이는 불필요한 데이터 처리를 방지하고 성능을 향상시키는 데 도움이 됩니다. + /// + /// + /// + /// // 5초마다 10번 반복 요청, 변경된 데이터만 처리 + /// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/data", "GET") + /// .setHandler(data => ProcessData(data)) + /// .setRepeat(true, 10, 5000, true); + /// + /// // 3초마다 무한 반복, 모든 응답 데이터 처리 + /// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/status", "GET") + /// .setHandler(data => UpdateStatus(data)) + /// .setRepeat(true, 0, 3000, false); + /// + /// + public HttpPipeLineInfo setRepeat(bool repeat, int count = 0, int interval = 1000, bool updatedDataOnly = true) + { + _repeat = repeat; + _repeatCount = count; + _repeatInterval = interval; + _updatedDataOnly = updatedDataOnly; // true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다. + return this; + } } } diff --git a/Assets/Scripts/UVC/Data/IDataObject.cs b/Assets/Scripts/UVC/Data/IDataObject.cs index db64d3ed..95cac125 100644 --- a/Assets/Scripts/UVC/Data/IDataObject.cs +++ b/Assets/Scripts/UVC/Data/IDataObject.cs @@ -10,10 +10,31 @@ /// public interface IDataObject { + /// + /// 모든 프로퍼티를 변경된 것으로 표시합니다. + /// 전체 데이터가 갱신되었을 때 사용합니다. + /// public void InitData(); + /// + /// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다. + /// 변경된 키는 자동으로 추적됩니다. + /// + /// 비교할 DataObject public void UpdateDifferent(IDataObject other); + /// + /// 업데이트된 속성만 포함하는 새로운 DataObject를 반환합니다. + /// + /// 업데이트 된 항목만 가지고 있는 DataObject public IDataObject GetUpdatedObject(); + + /// + /// /// + /// 업데이트 된 속성의 수. + /// + /// 업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다. + public int UpdatedCount { get; } + } } diff --git a/Assets/Scripts/UVC/Data/MQTTPipeLine.cs b/Assets/Scripts/UVC/Data/MQTTPipeLine.cs index b28a0c72..9405a256 100644 --- a/Assets/Scripts/UVC/Data/MQTTPipeLine.cs +++ b/Assets/Scripts/UVC/Data/MQTTPipeLine.cs @@ -3,30 +3,76 @@ using Newtonsoft.Json.Linq; using SampleProject.Config; using System.Collections.Generic; -using UnityEngine.InputSystem; using UVC.network; namespace UVC.Data { + /// + /// MQTT 통신을 통해 데이터를 수신하고 처리하는 파이프라인을 관리하는 클래스입니다. + /// + /// + /// 이 클래스는 MQTT 브로커에 연결하여 등록된 토픽으로 들어오는 메시지를 수신하고, + /// 해당 메시지를 지정된 데이터 매퍼를 통해 변환한 후 핸들러에게 전달합니다. + /// 여러 MQTT 토픽을 동시에 관리하고 각각에 대한 처리 방식을 개별적으로 설정할 수 있습니다. + /// public class MQTTPipeLine { + /// + /// MQTT 브로커의 도메인 주소 + /// + private string domain; - private Dictionary infoList = new Dictionary(); + /// + /// MQTT 브로커의 포트 번호 + /// + public int port; - private MQTTService mqtt = new MQTTService(Constants.MQTT_DOMAIN, Constants.MQTT_PORT); + /// + /// 토픽별 파이프라인 정보를 저장하는 딕셔너리 + /// + private Dictionary infoList; + /// + /// MQTT 통신을 처리하는 서비스 객체 + /// + private MQTTService mqtt; + /// + /// MQTTPipeLine 인스턴스를 생성합니다. + /// + /// MQTT 브로커의 도메인 주소, 기본값은 "localhost"입니다. + /// MQTT 브로커의 포트 번호, 기본값은 1883입니다. + public MQTTPipeLine(string domain = "localhost", int port = 1883) + { + this.domain = string.IsNullOrEmpty(domain) ? Constants.MQTT_DOMAIN : domain; + this.port = port; + mqtt = new MQTTService(Constants.MQTT_DOMAIN, Constants.MQTT_PORT); + infoList = new Dictionary(); + } + + /// + /// 토픽에 대한 파이프라인 정보를 추가합니다. + /// + /// 추가할 MQTTPipeLineInfo 객체 + /// + /// 동일한 토픽에 대한 정보가 이미 존재하는 경우 덮어씁니다. + /// public void Add(MQTTPipeLineInfo info) { - if (!infoList.ContainsKey(info.topic)) + if (!infoList.ContainsKey(info.Topic)) { - infoList.Add(info.topic, info); + infoList.Add(info.Topic, info); } else { - infoList[info.topic] = info; + infoList[info.Topic] = info; } } + + /// + /// 지정된 토픽에 대한 파이프라인 정보를 제거합니다. + /// + /// 제거할 토픽 이름 public void Remove(string topic) { if (infoList.ContainsKey(topic)) @@ -35,6 +81,9 @@ namespace UVC.Data } } + /// + /// 파이프라인을 실행하여 MQTT 브로커에 연결하고 등록된 모든 토픽을 구독합니다. + /// public void Execute() { foreach (var topic in infoList.Keys) @@ -44,6 +93,16 @@ namespace UVC.Data mqtt.Connect(); } + /// + /// MQTT 토픽으로 메시지가 수신되었을 때 호출되는 콜백 메서드입니다. + /// + /// 수신된 메시지의 토픽 + /// 수신된 메시지 내용 + /// + /// 이 메서드는 수신된 메시지의 형식(JSON 객체 또는 배열)에 따라 적절한 파싱을 수행하고, + /// 등록된 데이터 매퍼를 통해 메시지를 변환한 후, 해당 토픽에 등록된 핸들러에게 전달합니다. + /// 'UpdatedDataOnly' 설정에 따라 데이터가 변경된 경우에만 핸들러를 호출할 수도 있습니다. + /// private void OnTopicMessage(string topic, string message) { if (infoList.ContainsKey(topic)) @@ -56,19 +115,49 @@ namespace UVC.Data if (message.StartsWith("{")) { JObject source = JObject.Parse(message); - if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source); + if (info.DataMapper != null) dataObject = info.DataMapper.Mapping(source); } else if (message.StartsWith("[")) { JArray source = JArray.Parse(message); - if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source); + if (info.DataMapper != null) dataObject = info.DataMapper.Mapping(source); } } - if (dataObject != null) dataObject = DataRepository.Instance.AddData(topic, dataObject); - - info.handler?.Invoke(dataObject); + if (dataObject != null) dataObject = DataRepository.Instance.AddData(topic, dataObject, info.UpdatedDataOnly); + // 갱신 된 데이터가 있는 경우 핸들러 호출 + if (info.UpdatedDataOnly) + { + if (dataObject != null && dataObject.UpdatedCount > 0) info.Handler?.Invoke(dataObject); + } + else + { + info.Handler?.Invoke(dataObject); + } } } + /// + /// 파이프라인을 중지하고 모든 토픽 구독을 해제한 후 MQTT 브로커와의 연결을 종료합니다. + /// + public void Stop() + { + foreach (var topic in infoList.Keys) + { + mqtt.RemoveTopicHandler(topic, OnTopicMessage); + } + mqtt.Disconnect(); + } + + /// + /// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다. + /// + /// 이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다. + /// 를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다. + public void Dispose() + { + mqtt.Disconnect(); + infoList.Clear(); + } + } } diff --git a/Assets/Scripts/UVC/Data/MQTTPipeLineInfo.cs b/Assets/Scripts/UVC/Data/MQTTPipeLineInfo.cs index 562bfc70..b16d39f8 100644 --- a/Assets/Scripts/UVC/Data/MQTTPipeLineInfo.cs +++ b/Assets/Scripts/UVC/Data/MQTTPipeLineInfo.cs @@ -4,26 +4,103 @@ using System; namespace UVC.Data { + /// + /// MQTT 파이프라인 정보를 관리하는 클래스입니다. + /// MQTT 토픽 구독, 메시지 핸들링 및 데이터 매핑에 필요한 설정을 제공합니다. + /// + /// + /// 이 클래스는 빌더 패턴을 사용하여 MQTT 구독 설정을 유연하게 구성할 수 있게 합니다. + /// 각 설정 메서드는 체이닝을 통해 파이프라인을 정의할 수 있도록 합니다. + /// + /// + /// + /// var pipelineInfo = new MQTTPipeLineInfo("device/status") + /// .setDataMapper(new DataMapper(dataMask)) + /// .setHandler(data => Console.WriteLine(data)); + /// + /// public class MQTTPipeLineInfo { - public string topic; // MQTT 토픽 - public Action? handler = null; // 메시지 핸들러 - public DataMapper? dataMapper = null; // 데이터 매퍼 + private string _topic; // MQTT 토픽 + private Action? _handler = null; // 메시지 핸들러 + private DataMapper? _dataMapper = null; // 데이터 매퍼 + private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부 - public MQTTPipeLineInfo(string topic) + /// + /// MQTT 토픽 + /// + public string Topic => _topic; + + /// + /// 메시지 핸들러 + /// + public Action? Handler => _handler; + + /// + /// 데이터 매퍼 + /// + public DataMapper? DataMapper => _dataMapper; + + /// + /// 업데이트된 데이터만 받을 여부 (true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다) + /// + public bool UpdatedDataOnly => _updatedDataOnly; + + /// + /// MQTTPipeLineInfo 클래스의 새 인스턴스를 초기화합니다. + /// + /// 구독할 MQTT 토픽 + /// 변경된 데이터만 처리할지 여부 (기본값: true) + /// + /// updatedDataOnly가 true인 경우, 이전 데이터와 동일한 메시지는 핸들러에 전달되지 않습니다. + /// 이는 불필요한 데이터 처리를 방지하고 성능을 향상시킵니다. + /// + public MQTTPipeLineInfo(string topic, bool updatedDataOnly = true) { - this.topic = topic; + _topic = topic; + _updatedDataOnly = updatedDataOnly; } + /// + /// MQTT 메시지를 수신했을 때 호출될 핸들러를 설정합니다. + /// + /// 메시지 데이터를 처리할 콜백 함수 + /// 현재 MQTTPipeLineInfo 인스턴스 (메서드 체이닝용) + /// + /// 핸들러는 메시지가 수신되고 DataMapper에 의해 변환된 후 호출됩니다. + /// UpdatedDataOnly 속성이 true인 경우, 데이터가 변경된 경우에만 호출됩니다. + /// public MQTTPipeLineInfo setHandler(Action handler) { - this.handler = handler; + _handler = handler; return this; } + /// + /// MQTT 메시지 데이터를 처리할 데이터 매퍼를 설정합니다. + /// + /// 사용할 데이터 매퍼 객체 + /// 현재 MQTTPipeLineInfo 인스턴스 (메서드 체이닝용) + /// + /// 데이터 매퍼는 수신된 MQTT 메시지(JSON 형식)를 IDataObject로 변환하는 역할을 합니다. + /// DataMask를 사용하여 특정 필드에 대한 타입 변환 및 필드 이름 매핑을 처리할 수 있습니다. + /// + /// + /// + /// // DataMask를 사용한 데이터 매퍼 설정 + /// var dataMask = new DataMask(); + /// dataMask["temperature"] = 0.0; // 실수 타입 지정 + /// dataMask["humidity"] = 0; // 정수 타입 지정 + /// dataMask["timestamp"] = ""; // 문자열 타입 지정 + /// + /// var pipelineInfo = new MQTTPipeLineInfo("sensor/data") + /// .setDataMapper(new DataMapper(dataMask)) + /// .setHandler(data => ProcessSensorData(data)); + /// + /// public MQTTPipeLineInfo setDataMapper(DataMapper dataMapper) { - this.dataMapper = dataMapper; + _dataMapper = dataMapper; return this; } diff --git a/Assets/Scripts/UVC/Network/MQTTService.cs b/Assets/Scripts/UVC/Network/MQTTService.cs index 1bf6e39d..7b78e814 100644 --- a/Assets/Scripts/UVC/Network/MQTTService.cs +++ b/Assets/Scripts/UVC/Network/MQTTService.cs @@ -363,5 +363,17 @@ namespace UVC.network .WithPayload(message) .BeginPublish(); } + + /// + /// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다. + /// + /// 이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다. + /// 를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다. + public void Dispose() + { + Disconnect(); + topicHandler.Clear(); + client = null; + } } } diff --git a/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs b/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs index 2bf19c00..94b0d73d 100644 --- a/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs +++ b/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs @@ -1,15 +1,15 @@ #nullable enable using Cysharp.Threading.Tasks; -using Newtonsoft.Json.Linq; using NUnit.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using UnityEngine; using UVC.Data; -using UVC.Network; namespace UVC.Tests.Data { @@ -29,6 +29,7 @@ namespace UVC.Tests.Data public void Setup() { pipeLine = new HttpPipeLine(); + pipeLine.UseMockup = true; // MockHttpRequester 사용 설정 // 테스트를 위한 DataRepository 초기화 ClearDataRepository(); } @@ -40,20 +41,27 @@ namespace UVC.Tests.Data /// 이 메서드는 클래스의 모든 테스트 메서드를 순차적으로 호출하고 /// 각 테스트의 성공 또는 실패 여부를 로그로 출력합니다. /// - public void TestAll() + public async UniTask TestAll() { Setup(); Debug.Log("===== HttpPipeLine 테스트 시작 ====="); - RunTest(nameof(Add_NewInfo_AddedSuccessfully), Add_NewInfo_AddedSuccessfully); RunTest(nameof(Add_ExistingInfo_UpdatesExistingEntry), Add_ExistingInfo_UpdatesExistingEntry); - RunTest(nameof(Remove_ExistingInfo_RemovedSuccessfully), Remove_ExistingInfo_RemovedSuccessfully); - RunTest(nameof(Remove_NonExistingInfo_DoesNothing), Remove_NonExistingInfo_DoesNothing); + await RunTestAsync(nameof(Remove_ExistingInfo_RemovedSuccessfullyAsync), Remove_ExistingInfo_RemovedSuccessfullyAsync); + await RunTestAsync(nameof(Remove_NonExistingInfo_DoesNothing), Remove_NonExistingInfo_DoesNothing); RunTest(nameof(Excute_WithNonExistingKey_DoesNothing), Excute_WithNonExistingKey_DoesNothing); - // 비동기 테스트는 RunTest로 실행하기 어려워 별도 처리 필요 - // 간단하게 메시지만 출력 - Debug.Log($"비동기 테스트: {nameof(Excute_WithJObjectResponse_ProcessesDataCorrectly)}와 {nameof(Excute_WithJArrayResponse_ProcessesDataCorrectly)}는 수동으로 실행하세요."); + await RunTestAsync(nameof(Excute_WithJObjectResponse_ProcessesDataCorrectly), Excute_WithJObjectResponse_ProcessesDataCorrectly); + await RunTestAsync(nameof(Excute_WithJArrayResponse_ProcessesDataCorrectly), Excute_WithJArrayResponse_ProcessesDataCorrectly); + await RunTestAsync(nameof(Test_Excute_AgvDataParsing), Test_Excute_AgvDataParsing); + await RunTestAsync(nameof(Test_Excute_AlarmDataParsing), Test_Excute_AlarmDataParsing); + await RunTestAsync(nameof(Test_Excute_MultipleDataTypes), Test_Excute_MultipleDataTypes); + await RunTestAsync(nameof(Test_Excute_CarrierDataParsing), Test_Excute_CarrierDataParsing); + await RunTestAsync(nameof(Test_Excute_BaseInfoDataParsing), Test_Excute_BaseInfoDataParsing); + await RunTestAsync(nameof(Test_Excute_WithRepeatExecution), Test_Excute_WithRepeatExecution); + await RunTestAsync(nameof(Test_StopRepeat_StopsExecutionCorrectly), Test_StopRepeat_StopsExecutionCorrectly); + await RunTestAsync(nameof(Test_MultipleRepeatingRequests_ManagedIndependently), Test_MultipleRepeatingRequests_ManagedIndependently); + await RunTestAsync(nameof(Test_RepeatWithCount_StopsAutomatically), Test_RepeatWithCount_StopsAutomatically); Debug.Log("===== HttpPipeLine 테스트 완료 ====="); } @@ -76,6 +84,20 @@ namespace UVC.Tests.Data } } + private async UniTask RunTestAsync(string testName, Func testAction) + { + try + { + Debug.Log($"테스트 시작: {testName}"); + await testAction(); + Debug.Log($"테스트 성공: {testName}"); + } + catch (Exception ex) + { + Debug.LogError($"테스트 실패: {testName}\n{ex.Message}\n{ex.StackTrace}"); + } + } + /// /// 새로운 HttpPipeLineInfo를 추가하는 테스트 /// @@ -119,14 +141,14 @@ namespace UVC.Tests.Data /// 존재하는 HttpPipeLineInfo를 제거하는 테스트 /// [Test] - public void Remove_ExistingInfo_RemovedSuccessfully() + public async UniTask Remove_ExistingInfo_RemovedSuccessfullyAsync() { // Arrange var info = new HttpPipeLineInfo("http://test.com"); pipeLine.Add("test", info); // Act - pipeLine.Remove("test"); + await pipeLine.RemoveAsync("test"); // Assert var infoList = GetInfoListField(); @@ -137,14 +159,14 @@ namespace UVC.Tests.Data /// 존재하지 않는 키에 대한 Remove 호출 테스트 /// [Test] - public void Remove_NonExistingInfo_DoesNothing() + public async UniTask Remove_NonExistingInfo_DoesNothing() { // Arrange var info = new HttpPipeLineInfo("http://test.com"); pipeLine.Add("test", info); // Act - 존재하지 않는 키 제거 시도 - pipeLine.Remove("nonexistent"); + await pipeLine.RemoveAsync("nonexistent"); // Assert - 기존 항목은 여전히 존재해야 함 var infoList = GetInfoListField(); @@ -194,7 +216,7 @@ namespace UVC.Tests.Data /// Excute 메소드에서 JObject 응답을 처리하는 기능 테스트 /// [Test] - public async Task Excute_WithJObjectResponse_ProcessesDataCorrectly() + public async UniTask Excute_WithJObjectResponse_ProcessesDataCorrectly() { // Arrange bool handlerCalled = false; @@ -225,49 +247,11 @@ namespace UVC.Tests.Data try { - // HttpRequester의 Request 메소드를 대체하는 대신 - // 메소드를 직접 호출하여 테스트에서 통제할 수 있게 함 - // (실제 구현은 Mock 프레임워크를 사용하는 것이 좋음) - - // Execute 메소드 구현은 비동기지만 async void라서 직접 테스트 불가능 - // 대신 private 필드에 접근하여 로직을 검증 + // MockHttpRequester에 테스트용 응답 설정 + MockHttpRequester.SetResponse("http://test.com", mockResponse); // Act - // 이 시점에서 실제 HTTP 요청 대신 mockResponse를 반환하도록 설정해야 함 - // 아래는 conceptual pseudo-code (실제 구현 불가) - // MockHttpRequesterHelper.PatchHttpRequester( - // (url, method, body, headers) => UniTask.FromResult(mockResponse)); - - // pipeLine.Excute("testKey"); - - // 대신 메소드 내부 로직을 수동으로 실행 - var key = "testKey"; - if (GetInfoListField().TryGetValue(key, out var pipeLineInfo)) - { - // HttpRequester.Request 호출하는 대신 가짜 응답 사용 - string result = mockResponse; - - IDataObject? dataObject = null; - if (!string.IsNullOrEmpty(result)) - { - result = result.Trim(); - if (result.StartsWith("{")) - { - JObject source = JObject.Parse(result); - if (pipeLineInfo.dataMapper != null) - dataObject = pipeLineInfo.dataMapper.Mapping(source); - } - } - - if (dataObject != null) - { - // AddData 대신 수동으로 처리 - pipeLineInfo.handler?.Invoke(dataObject); - } - } - - // Wait for async operations to complete - await Task.Delay(100); + await pipeLine.Excute("testKey"); // Assert Assert.IsTrue(handlerCalled, "핸들러가 호출되지 않았습니다."); @@ -277,9 +261,9 @@ namespace UVC.Tests.Data } finally { - // 테스트 후 복원 - // MockHttpRequesterHelper.RestoreHttpRequester(); - pipeLine.Remove("testKey"); + // 설정한 응답 정리 + MockHttpRequester.ClearResponses("http://test.com"); + await pipeLine.RemoveAsync("testKey"); } } @@ -287,7 +271,7 @@ namespace UVC.Tests.Data /// Excute 메소드에서 JArray 응답을 처리하는 기능 테스트 /// [Test] - public async Task Excute_WithJArrayResponse_ProcessesDataCorrectly() + public async UniTask Excute_WithJArrayResponse_ProcessesDataCorrectly() { // Arrange bool handlerCalled = false; @@ -298,11 +282,8 @@ namespace UVC.Tests.Data // 배열용 DataMask 설정 var dataMask = new DataMask { - ["0"] = new DataMask - { - ["name"] = "이름", - ["value"] = 0 - } + ["name"] = "이름", + ["value"] = 0 }; var dataMapper = new DataMapper(dataMask); @@ -320,33 +301,12 @@ namespace UVC.Tests.Data try { + // MockHttpRequester에 테스트용 응답 설정 + MockHttpRequester.SetResponse("http://test.com", mockResponse); + // Act - var key = "testArrayKey"; - if (GetInfoListField().TryGetValue(key, out var pipeLineInfo)) - { - // 가짜 응답 사용 - string result = mockResponse; + await pipeLine.Excute("testArrayKey"); - IDataObject? dataObject = null; - if (!string.IsNullOrEmpty(result)) - { - result = result.Trim(); - if (result.StartsWith("[")) - { - JArray source = JArray.Parse(result); - if (pipeLineInfo.dataMapper != null) - dataObject = pipeLineInfo.dataMapper.Mapping(source); - } - } - - if (dataObject != null) - { - pipeLineInfo.handler?.Invoke(dataObject); - } - } - - // Wait for async operations to complete - await Task.Delay(100); // Assert Assert.IsTrue(handlerCalled, "핸들러가 호출되지 않았습니다."); @@ -362,7 +322,9 @@ namespace UVC.Tests.Data } finally { - pipeLine.Remove("testArrayKey"); + // 설정한 응답 정리 + MockHttpRequester.ClearResponses("http://test.com"); + await pipeLine.RemoveAsync("testArrayKey"); } } @@ -375,116 +337,664 @@ namespace UVC.Tests.Data // Arrange - 의도적으로 아무 것도 설정하지 않음 // Act & Assert - 예외가 발생하지 않아야 함 - Assert.DoesNotThrow(() => pipeLine.Excute("nonExistingKey")); - } - } - - /// - /// HTTP 테스트를 위한 모의(Mock) HttpRequester 클래스입니다. - /// 실제 네트워크 요청을 하지 않고 테스트할 수 있게 합니다. - /// - public static class MockHttpRequester - { - // 테스트 응답 설정을 위한 딕셔너리 - private static readonly Dictionary mockResponses = new Dictionary(); - - /// - /// 테스트용 응답을 설정합니다. - /// - public static void SetMockResponse(string url, string response) - { - mockResponses[url] = response; + Assert.DoesNotThrow(() => pipeLine.Excute("nonExistingKey")); } /// - /// 모든 모의 응답을 제거합니다. + /// UVC.Tests.MockHttpRequester를 사용하여 AGV 데이터 파싱 테스트 /// - public static void ClearMockResponses() + [Test] + public async UniTask Test_Excute_AgvDataParsing() { - mockResponses.Clear(); - } + // Arrange + bool handlerCalled = false; + IDataObject? receivedData = null; + string agvUrl = "http://api.example.com/agv"; // MockHttpRequester는 url에 "agv"가 포함된 경우 AGV 관련 응답 반환 - /// - /// HTTP 요청을 모의로 실행하고 미리 설정된 응답을 반환합니다. - /// - public static Task Request(string url, string method, string body = null, - Dictionary headers = null) - { - if (mockResponses.TryGetValue(url, out string response)) + // 배열용 DataMask 설정 (AGV 데이터 구조에 맞춤) + var dataMask = new DataMask { - return Task.FromResult(response); - } + ["VHL_NAME"] = "차량명", + ["AGV_IDX"] = "인덱스", + ["B_INSTALL"] = "설치여부", + ["NODE_ID"] = "노드ID", + ["REAL_ID"] = "실제ID", + ["VHL_STATE"] = "상태", + ["BAY_LIST"] = "베이리스트" + }; - // 기본 빈 응답 - return Task.FromResult("{}"); - } + var dataMapper = new DataMapper(dataMask); - - - - - } - - /// - /// Excute 메소드 테스트를 위한 모의 HttpRequester - /// - class MockHttpRequesterHelper - { - // HttpRequester.Request 메소드를 대체할 delegate - public delegate UniTask RequestDelegate(string url, string method, string body, Dictionary headers); - - private static RequestDelegate originalRequestMethod; - private static bool isPatched = false; - - /// - /// HttpRequester.Request 메소드를 모의 구현으로 대체 - /// - public static void PatchHttpRequester(RequestDelegate mockRequestMethod) - { - // 원본 메소드 저장 (이미 패치되었다면 다시 저장하지 않음) - if (!isPatched) - { - var methodInfo = typeof(HttpRequester).GetMethod("Request", - new[] { typeof(string), typeof(string), typeof(string), typeof(Dictionary), typeof(bool) }); - - if (methodInfo != null) + // HttpPipeLineInfo 설정 + var info = new HttpPipeLineInfo(agvUrl, "get") + .setDataMapper(dataMapper) + .setHandler((data) => { - // 이미 정적 필드에 저장된 상태라면 원본을 다시 가져올 필요 없음 - originalRequestMethod = (RequestDelegate)Delegate.CreateDelegate( - typeof(RequestDelegate), methodInfo); - isPatched = true; - } - } + handlerCalled = true; + receivedData = data; + }); - // Reflection을 사용하여 HttpRequester.Request 메소드를 우리 모의 메소드로 대체 - // 주의: 실제 환경에서는 좀 더 정교한 모킹 프레임워크를 사용하는 것이 좋음 + pipeLine.Add("agvData", info); + + // Act + await pipeLine.Excute("agvData"); + + // Assert + Assert.IsTrue(handlerCalled, "핸들러가 호출되지 않았습니다."); + Assert.IsNotNull(receivedData, "데이터가 null입니다."); + Assert.IsTrue(receivedData is DataArray, "결과가 DataArray가 아닙니다."); + + var dataArray = receivedData as DataArray; + Assert.IsTrue(dataArray?.Count > 0, "데이터 배열이 비어 있습니다."); + + // 첫 번째 항목 확인 + Assert.IsNotNull(dataArray?[0].GetString("VHL_NAME")); + Assert.IsNotNull(dataArray?[0].GetString("AGV_IDX")); + Assert.IsNotNull(dataArray?[0].GetString("B_INSTALL")); } /// - /// HttpRequester.Request를 원래대로 복원 + /// UVC.Tests.MockHttpRequester를 사용하여 알람 데이터 파싱 테스트 /// - public static void RestoreHttpRequester() + [Test] + public async UniTask Test_Excute_AlarmDataParsing() { - // 실제 환경에서는 원본 메소드로 복원하는 로직이 필요 - isPatched = false; - } + // Arrange + bool handlerCalled = false; + IDataObject? receivedData = null; + string alarmUrl = "http://api.example.com/alarm"; // MockHttpRequester는 url에 "alarm"이 포함된 경우 알람 관련 응답 반환 - /// - /// 테스트용 모의 Request 메소드 - /// - public static UniTask MockRequest(string url, string method, string body, Dictionary headers, - string mockResponse) - { - // 테스트용 응답 반환 - if (typeof(T) == typeof(string)) + // 배열용 DataMask 설정 (알람 데이터 구조에 맞춤) + var dataMask = new DataMask { - return UniTask.FromResult((T)(object)mockResponse); + ["ID"] = "알람ID", + ["ALARM_TYPE"] = "알람타입", + ["LEVEL"] = "심각도", + ["STATE"] = "상태", + ["MESSAGE"] = "메시지", + ["CODE"] = "코드", + ["ICON"] = "아이콘", + ["SET_TIME"] = "발생시간" + }; + + var dataMapper = new DataMapper(dataMask); + + // HttpPipeLineInfo 설정 + var info = new HttpPipeLineInfo(alarmUrl, "get") + .setDataMapper(dataMapper) + .setHandler((data) => + { + handlerCalled = true; + receivedData = data; + }); + + pipeLine.Add("alarmData", info); + + try + { + // Act + await pipeLine.Excute("alarmData"); + + // Assert + Assert.IsTrue(handlerCalled, "핸들러가 호출되지 않았습니다."); + Assert.IsNotNull(receivedData, "데이터가 null입니다."); + Assert.IsTrue(receivedData is DataArray, "결과가 DataArray가 아닙니다."); + + var dataArray = receivedData as DataArray; + Assert.IsTrue(dataArray?.Count > 0, "데이터 배열이 비어 있습니다."); + + // 첫 번째 항목 확인 + Assert.IsNotNull(dataArray?[0].GetString("ID")); + Assert.IsNotNull(dataArray?[0].GetString("ALARM_TYPE")); + Assert.IsNotNull(dataArray?[0].GetString("LEVEL")); + Assert.IsNotNull(dataArray?[0].GetString("MESSAGE")); + Assert.IsNotNull(dataArray?[0].GetString("CODE")); + Assert.IsNotNull(dataArray?[0].GetString("ICON")); + Assert.IsNotNull(dataArray?[0].GetString("SET_TIME")); + } + finally + { + await pipeLine.RemoveAsync("alarmData"); + } + } + + /// + /// UVC.Tests.MockHttpRequester를 사용하여 여러 URL 유형에 대한 동시 테스트 + /// + [Test] + public async UniTask Test_Excute_MultipleDataTypes() + { + // Arrange + int handlerCallCount = 0; + Dictionary results = new Dictionary(); + + // 각 데이터 타입별 URL + Dictionary urls = new Dictionary + { + { "agv", "http://api.example.com/agv" }, + { "equipment", "http://api.example.com/equipment" }, + { "alarm", "http://api.example.com/alarm" } + }; + + // 각 데이터 타입에 대한 DataMask 설정 + Dictionary dataMasks = new Dictionary + { + { + "agv", new DataMask + { + ["0"] = new DataMask { ["VHL_NAME"] = "차량명", ["AGV_IDX"] = "인덱스" } + } + }, + { + "equipment", new DataMask + { + ["0"] = new DataMask { ["EQP_ID"] = "장비ID", ["KOR_EQP_NAME"] = "장비명" } + } + }, + { + "alarm", new DataMask + { + ["0"] = new DataMask { ["ID"] = "알람ID", ["ALARM_TYPE"] = "알람타입" } + } + } + }; + + // 각 데이터 타입별 HttpPipeLineInfo 설정 및 등록 + foreach (var item in urls) + { + string key = item.Key; + var info = new HttpPipeLineInfo(item.Value, "get") + .setDataMapper(new DataMapper(dataMasks[key])) + .setHandler((data) => + { + handlerCallCount++; + results[key] = data; + }); + + pipeLine.Add(key, info); + await pipeLine.Excute(key); } - // 다른 타입은 지원하지 않음 - 테스트에서 string만 사용 - throw new NotSupportedException("MockRequest only supports string responses"); + + // Assert + Assert.AreEqual(urls.Count, handlerCallCount, "모든 핸들러가 호출되지 않았습니다."); + Assert.AreEqual(urls.Count, results.Count, "모든 결과가 수집되지 않았습니다."); + + // 각 데이터 타입별 결과 확인 + foreach (var result in results) + { + Assert.IsNotNull(result.Value, $"{result.Key} 데이터가 null입니다."); + Assert.IsTrue(result.Value is DataArray, $"{result.Key} 결과가 DataArray가 아닙니다."); + + var dataArray = result.Value as DataArray; + Assert.IsTrue(dataArray?.Count > 0, $"{result.Key} 데이터 배열이 비어 있습니다."); + } + } + + /// + /// UVC.Tests.MockHttpRequester를 사용한 운반대(Carrier) 데이터 테스트 + /// + [Test] + public async UniTask Test_Excute_CarrierDataParsing() + { + // Arrange + bool handlerCalled = false; + IDataObject? receivedData = null; + string testUrl = "http://api.example.com/carrier"; // url에 "carrier"가 포함된 경우 캐리어 관련 응답 반환 + + // DataMask와 DataMapper 설정 + var dataMask = new DataMask + { + ["MAIN_CARR_ID"] = "캐리어ID", + ["SUB_CARR_ID"] = "서브ID", + ["CARR_SEQ"] = "순번", + ["CARR_USE"] = "사용상태" + }; + + var dataMapper = new DataMapper(dataMask); + + // HttpPipeLineInfo 설정 + var info = new HttpPipeLineInfo(testUrl, "get") + .setDataMapper(dataMapper) + .setHandler((data) => + { + handlerCalled = true; + receivedData = data; + }); + + pipeLine.Add("testCarrierData", info); + + // Act + await pipeLine.Excute("testCarrierData"); + + // Assert + Assert.IsTrue(handlerCalled, "핸들러가 호출되지 않았습니다."); + Assert.IsNotNull(receivedData, "응답 데이터가 null입니다."); + Assert.IsTrue(receivedData is DataArray, "응답이 DataArray가 아닙니다."); + + var dataArray = receivedData as DataArray; + Assert.IsTrue(dataArray?.Count > 0, "데이터 배열이 비어있습니다."); + + // 첫 번째 캐리어 데이터 확인 + Assert.IsNotNull(dataArray?[0].GetString("MAIN_CARR_ID"), "캐리어ID가 null입니다."); + Assert.IsNotNull(dataArray?[0].GetString("SUB_CARR_ID"), "서브ID가 null입니다."); + Assert.IsNotNull(dataArray?[0].GetString("CARR_SEQ"), "순번이 null입니다."); + Assert.IsNotNull(dataArray?[0].GetString("CARR_USE"), "사용상태가 null입니다."); + } + + /// + /// UVC.Tests.MockHttpRequester를 사용하여 BaseInfo 데이터 파싱 테스트 + /// + [Test] + public async UniTask Test_Excute_BaseInfoDataParsing() + { + // Arrange + bool handlerCalled = false; + IDataObject? receivedData = null; + string baseInfoUrl = "http://api.example.com/baseinfo"; // url에 "baseinfo"가 포함된 경우 기본 정보 응답 반환 + + // BaseInfo에서 테스트할 주요 섹션 (AGV, EQUIPMENT, ALARM 등)에 대한 DataMask 설정 + var dataMask = new DataMask + { + ["AGV"] = new DataMask + { + ["0"] = new DataMask + { + ["VHL_NAME"] = "차량명", + ["AGV_IDX"] = "인덱스", + ["B_INSTALL"] = "설치여부", + ["NODE_ID"] = "노드ID", + ["VHL_STATE"] = "상태" + } + }, + ["EQUIPMENT"] = new DataMask + { + ["0"] = new DataMask + { + ["EQP_ID"] = "장비ID", + ["KOR_EQP_NAME"] = "장비명", + ["STATE_ID"] = "상태", + ["NTW_STS"] = "네트워크상태" + } + }, + ["ALARM"] = new DataMask + { + ["0"] = new DataMask + { + ["ID"] = "알람ID", + ["ALARM_TYPE"] = "알람타입", + ["LEVEL"] = "심각도", + ["MESSAGE"] = "메시지" + } + } + }; + + var dataMapper = new DataMapper(dataMask); + + // HttpPipeLineInfo 설정 + var info = new HttpPipeLineInfo(baseInfoUrl, "get") + .setDataMapper(dataMapper) + .setHandler((data) => + { + handlerCalled = true; + receivedData = data; + }); + + pipeLine.Add("baseInfoData", info); + + try + { + // Act + await pipeLine.Excute("baseInfoData"); + + // Assert + Assert.IsTrue(handlerCalled, "핸들러가 호출되지 않았습니다."); + Assert.IsNotNull(receivedData, "데이터가 null입니다."); + Assert.IsTrue(receivedData is DataObject, "결과가 DataObject가 아닙니다."); + + var dataObject = receivedData as DataObject; + + // AGV 데이터 검증 + Assert.IsNotNull(dataObject?.Get("AGV"), "AGV 데이터가 없습니다."); + var agvData = dataObject?.Get("AGV") as DataArray; + Assert.IsTrue(agvData?.Count > 0, "AGV 데이터 배열이 비어 있습니다."); + Assert.IsNotNull(agvData?[0].GetString("VHL_NAME"), "VHL_NAME 필드가 없습니다."); + Assert.IsNotNull(agvData?[0].GetString("AGV_IDX"), "AGV_IDX 필드가 없습니다."); + Assert.IsNotNull(agvData?[0].GetString("B_INSTALL"), "B_INSTALL 필드가 없습니다."); + + // EQUIPMENT 데이터 검증 + Assert.IsNotNull(dataObject?.Get("EQUIPMENT"), "EQUIPMENT 데이터가 없습니다."); + var equipmentData = dataObject?.Get("EQUIPMENT") as DataArray; + Assert.IsTrue(equipmentData?.Count > 0, "EQUIPMENT 데이터 배열이 비어 있습니다."); + Assert.IsNotNull(equipmentData?[0].GetString("EQP_ID"), "EQP_ID 필드가 없습니다."); + Assert.IsNotNull(equipmentData?[0].GetString("KOR_EQP_NAME"), "KOR_EQP_NAME 필드가 없습니다."); + Assert.IsNotNull(equipmentData?[0].GetString("STATE_ID"), "STATE_ID 필드가 없습니다."); + + // ALARM 데이터 검증 + Assert.IsNotNull(dataObject?.Get("ALARM"), "ALARM 데이터가 없습니다."); + var alarmData = dataObject?.Get("ALARM") as DataArray; + Assert.IsTrue(alarmData?.Count > 0, "ALARM 데이터 배열이 비어 있습니다."); + Assert.IsNotNull(alarmData?[0].GetString("ID"), "ID 필드가 없습니다."); + Assert.IsNotNull(alarmData?[0].GetString("ALARM_TYPE"), "ALARM_TYPE 필드가 없습니다."); + Assert.IsNotNull(alarmData?[0].GetString("LEVEL"), "LEVEL 필드가 없습니다."); + Assert.IsNotNull(alarmData?[0].GetString("MESSAGE"), "MESSAGE 필드가 없습니다."); + } + finally + { + await pipeLine.RemoveAsync("baseInfoData"); + } + } + + /// + /// 반복 실행 기능을 테스트합니다. + /// + [Test] + public async UniTask Test_Excute_WithRepeatExecution() + { + // Arrange + int handlerCallCount = 0; + List receivedResponses = new List(); + string testUrl = "http://test.com/repeat"; + int expectedCallCount = 3; + int repeatInterval = 100; // 테스트를 빠르게 진행하기 위해 간격을 짧게 설정 + + // 여러 응답을 순차적으로 반환하기 위한 응답 데이터 설정 + string[] mockResponses = new string[] + { + @"{""id"": 1, ""status"": ""pending"", ""timestamp"": ""2025-06-09T10:00:00Z""}", + @"{""id"": 1, ""status"": ""processing"", ""timestamp"": ""2025-06-09T10:00:10Z""}", + @"{""id"": 1, ""status"": ""completed"", ""timestamp"": ""2025-06-09T10:00:20Z""}" + }; + + // Mock 응답 설정 + MockHttpRequester.SetResponse(testUrl, mockResponses[0]); + + // DataMask와 DataMapper 설정 + var dataMask = new DataMask(); + dataMask["id"] = 0; + dataMask["status"] = ""; + dataMask["timestamp"] = ""; + + var dataMapper = new DataMapper(dataMask); + + // 반복 실행 설정을 포함한 HttpPipeLineInfo 생성 + var info = new HttpPipeLineInfo(testUrl, "get") + .setDataMapper(dataMapper) + .setHandler(async (data) => + { + handlerCallCount++; + if (data is DataObject dataObject) + { + receivedResponses.Add(dataObject.GetString("status")); + } + + // 반복 실행 중단 + if (handlerCallCount >= expectedCallCount) + { + await pipeLine.StopRepeat("repeatTest"); + } + else + { + MockHttpRequester.SetResponse(testUrl, mockResponses[handlerCallCount]); + } + }) + .setRepeat(true, expectedCallCount, repeatInterval, false); + + pipeLine.UseMockup = true; + pipeLine.Add("repeatTest", info); + + try + { + // Act + await pipeLine.Excute("repeatTest"); + + // 반복 작업이 완료될 때까지 대기 + // (실제 상황에서는 이렇게 기다리지 않지만 테스트를 위해 필요) + await UniTask.Delay((repeatInterval * expectedCallCount) + 1000 * expectedCallCount); + + // Assert + Assert.AreEqual(expectedCallCount, handlerCallCount, "핸들러 호출 횟수가 예상과 다릅니다"); + + // 응답이 순차적으로 처리되었는지 확인 + Assert.AreEqual("pending", receivedResponses[0], "첫 번째 응답이 올바르지 않습니다"); + Assert.AreEqual("processing", receivedResponses[1], "두 번째 응답이 올바르지 않습니다"); + Assert.AreEqual("completed", receivedResponses[2], "세 번째 응답이 올바르지 않습니다"); + } + finally + { + // 테스트 정리 + await pipeLine.RemoveAsync("repeatTest"); + MockHttpRequester.ClearResponses(testUrl); + } + } + + /// + /// 실행 중인 반복 요청을 중지하는 기능을 테스트합니다. + /// + [Test] + public async UniTask Test_StopRepeat_StopsExecutionCorrectly() + { + // Arrange + int handlerCallCount = 0; + string testUrl = "http://test.com/repeat-stop"; + int repeatInterval = 100; + + // Mock 응답 설정 + string mockResponse = @"{""id"": 2, ""status"": ""running"", ""timestamp"": ""2025-06-09T11:00:00Z""}"; + MockHttpRequester.SetResponse(testUrl, mockResponse); + + // DataMask와 DataMapper 설정 + var dataMask = new DataMask(); + dataMask["id"] = 0; + dataMask["status"] = ""; + dataMask["timestamp"] = ""; + + var dataMapper = new DataMapper(dataMask); + + // 무한 반복 설정을 포함한 HttpPipeLineInfo 생성 + var info = new HttpPipeLineInfo(testUrl, "get") + .setDataMapper(dataMapper) + .setHandler((data) => { handlerCallCount++;}) + .setRepeat(true, 0, repeatInterval, false); // 무한 반복 (repeatCount = 0) + + pipeLine.UseMockup = true; + pipeLine.Add("infiniteRepeatTest", info); + + try + { + // Act + await pipeLine.Excute("infiniteRepeatTest"); + + // 몇 번의 반복 실행이 될 때까지 대기 + await UniTask.Delay(repeatInterval * 2 + 50); + + // 반복 중지 + await pipeLine.StopRepeat("infiniteRepeatTest"); + + // 현재 호출 횟수 기록 + int callCountBeforeStop = handlerCallCount; + + // 더 이상 호출이 발생하지 않는지 확인하기 위해 추가 대기 + await UniTask.Delay(repeatInterval * 2 + 50); + + // Assert + Assert.Greater(callCountBeforeStop, 0, "반복 호출이 발생하지 않았습니다"); + Assert.AreEqual(callCountBeforeStop, handlerCallCount, "반복 중지 후에도 호출이 계속 발생했습니다"); + } + finally + { + // 테스트 정리 + await pipeLine.RemoveAsync("infiniteRepeatTest"); + MockHttpRequester.ClearResponses(testUrl); + } + } + + /// + /// 여러 개의 반복 요청을 동시에 실행하고 개별적으로 중지하는 기능을 테스트합니다. + /// + [Test] + public async UniTask Test_MultipleRepeatingRequests_ManagedIndependently() + { + // Arrange + int handlerCallCount1 = 0; + int handlerCallCount2 = 0; + + string testUrl1 = "http://test.com/repeat1"; + string testUrl2 = "http://test.com/repeat2"; + + int repeatInterval1 = 100; + int repeatInterval2 = 150; + + // Mock 응답 설정 + string mockResponse1 = @"{""id"": 3, ""name"": ""작업1""}"; + string mockResponse2 = @"{""id"": 4, ""name"": ""작업2""}"; + + MockHttpRequester.SetResponse(testUrl1, mockResponse1); + MockHttpRequester.SetResponse(testUrl2, mockResponse2); + + // DataMask 설정 + var dataMask = new DataMask(); + dataMask["id"] = 0; + dataMask["name"] = ""; + + var dataMapper = new DataMapper(dataMask); + + // 두 개의 반복 요청 설정 + var info1 = new HttpPipeLineInfo(testUrl1, "get") + .setDataMapper(dataMapper) + .setHandler((data) => { handlerCallCount1++; }) + .setRepeat(true, 0, repeatInterval1, false); + + var info2 = new HttpPipeLineInfo(testUrl2, "get") + .setDataMapper(dataMapper) + .setHandler((data) => { handlerCallCount2++; }) + .setRepeat(true, 0, repeatInterval2, false); + + pipeLine.UseMockup = true; + pipeLine.Add("repeatTest1", info1); + pipeLine.Add("repeatTest2", info2); + + try + { + // Act + await pipeLine.Excute("repeatTest1"); + await pipeLine.Excute("repeatTest2"); + + // 두 요청 모두 몇 번 실행되도록 대기 + await UniTask.Delay(Math.Max(repeatInterval1, repeatInterval2) * 3 + 100 * 3 + 50);//100 네트워크 지연, 50 추가 여유 + + // 첫 번째 반복만 중지 + await pipeLine.StopRepeat("repeatTest1"); + + // 호출 횟수 기록 + int callCount1AfterStop = handlerCallCount1; + int callCount2BeforeSecondStop = handlerCallCount2; + + // 두 번째 반복이 계속 실행되는지 확인하기 위해 대기 + await UniTask.Delay(repeatInterval2 * 2 + 100 * 2 + 50); + // Assert + Assert.Greater(callCount1AfterStop, 0, "첫 번째 반복 요청 실행이 발생하지 않았습니다"); + Assert.Greater(handlerCallCount2, callCount2BeforeSecondStop, "두 번째 반복이 계속 실행되지 않았습니다"); + Assert.AreEqual(callCount1AfterStop, handlerCallCount1, "첫 번째 반복이 중지되지 않았습니다"); + // 두 번째 반복도 중지 + await pipeLine.StopRepeat("repeatTest2"); + // 호출 횟수 다시 기록 + int callCount2AfterStop = handlerCallCount2; + // 더 이상 호출이 없는지 확인하기 위해 대기 + await UniTask.Delay(Math.Max(repeatInterval1, repeatInterval2) * 2 + 100 * 2 + 50); + // 모든 호출이 중지되었는지 확인 + Assert.AreEqual(callCount1AfterStop, handlerCallCount1, "첫 번째 반복이 계속되었습니다"); + Assert.AreEqual(callCount2AfterStop, handlerCallCount2, "두 번째 반복이 계속되었습니다"); + } + finally + { + // 테스트 정리 + await pipeLine.RemoveAsync("repeatTest1"); + await pipeLine.RemoveAsync("repeatTest2"); + MockHttpRequester.ClearResponses(testUrl1); + MockHttpRequester.ClearResponses(testUrl2); + } + } + + /// + /// 지정된 횟수만큼 반복 실행 후 자동 중단되는 기능을 테스트합니다. + /// + [Test] + public async UniTask Test_RepeatWithCount_StopsAutomatically() + { + // Arrange + int handlerCallCount = 0; + List receivedData = new List(); + + string testUrl = "http://test.com/repeat-count"; + int repeatCount = 3; + int repeatInterval = 100; + + // Mock 응답 설정 + string mockResponse = @"{""id"": 5, ""message"": ""자동 중단 테스트""}"; + MockHttpRequester.SetResponse(testUrl, mockResponse); + + // DataMask 설정 + var dataMask = new DataMask(); + dataMask["id"] = 0; + dataMask["message"] = ""; + + var dataMapper = new DataMapper(dataMask); + + // 반복 횟수가 지정된 HttpPipeLineInfo 생성 + var info = new HttpPipeLineInfo(testUrl, "get") + .setDataMapper(dataMapper) + .setHandler((data) => + { + handlerCallCount++; + receivedData.Add(data); + }) + .setRepeat(true, repeatCount, repeatInterval, false); + + pipeLine.UseMockup = true; + pipeLine.Add("countedRepeatTest", info); + + try + { + // Act + await pipeLine.Excute("countedRepeatTest"); + + // 지정된 횟수의 반복이 완료될 때까지 충분히 대기 + await UniTask.Delay(repeatInterval * (repeatCount + 1) + 50 + 100 * (repeatCount + 1)); + + // Assert + Assert.AreEqual(repeatCount, handlerCallCount, "지정된 횟수만큼 반복 실행되지 않았습니다"); + Assert.AreEqual(repeatCount, receivedData.Count, "기대한 데이터 수와 다릅니다"); + + // 각 응답이 올바르게 처리되었는지 확인 + var data = receivedData[0]; + Assert.IsNotNull(data, "데이터가 null입니다"); + var dataObject = data as DataObject; + Assert.AreEqual(5, dataObject?.GetInt("id"), "ID가 올바르지 않습니다"); + Assert.AreEqual("자동 중단 테스트", dataObject?.GetString("message"), "메시지가 올바르지 않습니다"); + + // 자동으로 제거되었는지 확인 + var repeatTokenSources = GetRepeatTokenSourcesField(); + Assert.IsFalse(repeatTokenSources.ContainsKey("countedRepeatTest"), + "실행 완료 후 반복 토큰이 제거되지 않았습니다"); + } + finally + { + // 테스트 정리 + await pipeLine.RemoveAsync("countedRepeatTest"); + MockHttpRequester.ClearResponses(testUrl); + } + } + + /// + /// HttpPipeLine의 private repeatTokenSources 필드 가져오기 + /// + private Dictionary GetRepeatTokenSourcesField() + { + var fieldInfo = typeof(HttpPipeLine).GetField("repeatTokenSources", + BindingFlags.NonPublic | BindingFlags.Instance); + + return (Dictionary)fieldInfo.GetValue(pipeLine); } } - - } \ No newline at end of file diff --git a/Assets/Scripts/UVC/Tests/MockHttpRequester.cs b/Assets/Scripts/UVC/Tests/MockHttpRequester.cs new file mode 100644 index 00000000..6d779724 --- /dev/null +++ b/Assets/Scripts/UVC/Tests/MockHttpRequester.cs @@ -0,0 +1,154 @@ +#nullable enable + +using Cysharp.Threading.Tasks; +using System; +using System.Collections.Generic; +using UVC.Json; + +namespace UVC.Tests +{ + /// + /// HTTP 요청을 모의(mock)하기 위한 클래스로, 테스트 환경에서 실제 HTTP 요청 없이 미리 정의된 응답을 반환합니다. + /// + /// + /// 이 클래스는 테스트 목적으로 HttpRequester 클래스의 동작을 시뮬레이션합니다. + /// URL에 따라 다양한 미리 정의된 응답을 반환하거나 SetResponse 메서드를 통해 + /// 특정 URL에 대한 커스텀 응답을 설정할 수 있습니다. + /// + public class MockHttpRequester + { + /// + /// URL별 응답 데이터를 저장하는 딕셔너리입니다. + /// + private static Dictionary responseDic = new Dictionary(); + + /// + /// 특정 URL에 대한 응답을 설정합니다. + /// + /// 응답을 설정할 URL + /// 설정할 응답 문자열 (JSON 형식) + /// + /// 이미 설정된 URL에 대한 응답이 있다면 새 응답으로 업데이트됩니다. + /// + public static void SetResponse(string url, string response) + { + if (!responseDic.ContainsKey(url)) + { + responseDic[url] = response; + } + else + { + responseDic[url] = response; // Update existing response + } + } + + /// + /// 저장된 응답 데이터를 초기화합니다. + /// + /// 선택적으로 특정 URL에 대한 응답만 초기화하려면 URL을 지정합니다. null이면 모든 응답을 초기화합니다. + public static void ClearResponses(string? key = null) + { + if (key != null && responseDic.ContainsKey(key)) + { + responseDic.Remove(key); + return; + } + responseDic.Clear(); + } + + /// + /// HTTP 요청을 시뮬레이션하고 미리 정의된 응답을 반환합니다. + /// + /// 반환할 데이터 타입 + /// 요청 URL + /// HTTP 메서드 (GET, POST 등) + /// 요청 본문 (선택적) + /// 요청 헤더 (선택적) + /// 인증 사용 여부 (선택적) + /// 지정된 타입으로 변환된 응답 + /// + /// 이 메서드는 네트워크 지연을 시뮬레이션하기 위해 짧은 지연 시간 후에 응답을 반환합니다. + /// URL이 responseDic에 있으면 해당 사용자 정의 응답을 반환하고, 없으면 URL에 포함된 키워드에 따라 + /// 미리 정의된 응답을 반환합니다. + /// + public static async UniTask Request(string url, string method, string? body = null, Dictionary? header = null, bool useAuth = false) + { + await UniTask.Delay(100);// new Random().Next(100, 1000)); // Simulate network delay + if (responseDic.TryGetValue(url, out string response)) + { + response = responseDic[url]; + } + else + { + response = GetResponse(url); + } + + if (typeof(T) == typeof(string)) + { + return (T)(object)response; + } + else + { + return JsonHelper.FromJson(response); + } + } + + /// + /// URL 키워드에 따라 미리 정의된 응답을 반환합니다. + /// + /// 응답을 결정할 URL + /// URL 키워드에 맞는 미리 정의된 JSON 응답 + /// + /// URL에 특정 키워드(예: "agv", "alarm" 등)가 포함되어 있으면 그에 맞는 + /// 테스트 데이터를 반환합니다. 일치하는 키워드가 없으면 기본 응답을 반환합니다. + /// + private static string GetResponse(string url) + { + if (url.ToLower().Contains("agv")) + { + return responseAGV; + } + else if (url.ToLower().Contains("stoker_stack")) + { + return responseStokerStack; + } + else if (url.ToLower().Contains("stoker_crane")) + { + return responseStokerCrane; + } + else if (url.ToLower().Contains("carrier")) + { + return responseCarrier; + } + else if (url.ToLower().Contains("equipment")) + { + return responseEquipment; + } + else if (url.ToLower().Contains("alarm")) + { + return responseAlarm; + } + else if (url.ToLower().Contains("simulation_rank")) + { + return responseSimulationRank; + } + else if (url.ToLower().Contains("baseinfo")) + { + return responseBaseInfo; + } + else + { + return "{\"status\":\"unknown\"}"; + } + } + + private static string responseAGV = "[{\"VHL_NAME\":\"HFF09CNA8001\",\"AGV_IDX\":\"0\",\"B_INSTALL\":\"N\",\"NODE_ID\":\"0\",\"REAL_ID\":\"0\",\"VHL_STATE\":\"0\",\"BAY_LIST\":\"1;\",\"X\":\"0\",\"Y\":\"0\",\"MODE\":\"0\",\"BATT\":\"0\",\"SUB_GOAL\":\"0\",\"FINAL_GOAL\":\"0\",\"TIMESTAMP\":\"2025-03-08T15:34:27Z\",\"DEGREE\":\"0\",\"STOP_STATE\":\"0\",\"CMD_ID\":null,\"RESERVED_CMD\":null,\"ASSIGN_TIME\":null,\"TRANSFER_STATE\":null,\"SOURCE_REAL_ID\":null,\"DEST_REAL_ID\":null,\"CARRIER_LOCATION\":null,\"SOURCE_PORT\":null,\"DESTINATION_PORT\":null,\"RECEIVE_TIME\":null,\"CARRIER_ID\":null,\"BATCH_ID\":null,\"LOT_ID\":null,\"CARRIER_TIMESTAMP\":null,\"JOB_ID\":null,\"FROM\":\"NULL,NULL,NULL\",\"TO\":\"NULL,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-08T15:34:27Z\"},{\"VHL_NAME\":\"HFF09CNA8002\",\"AGV_IDX\":\"1\",\"B_INSTALL\":\"Y\",\"NODE_ID\":\"231\",\"REAL_ID\":\"231\",\"VHL_STATE\":\"11\",\"BAY_LIST\":\"7;\",\"X\":\"154136\",\"Y\":\"20166\",\"MODE\":\"1\",\"BATT\":\"38\",\"SUB_GOAL\":\"388\",\"FINAL_GOAL\":\"248\",\"TIMESTAMP\":\"2025-03-25T12:00:49.571Z\",\"DEGREE\":\"179.2\",\"STOP_STATE\":\"0\",\"CMD_ID\":\"2F35556_235_7061032854410412\",\"RESERVED_CMD\":null,\"ASSIGN_TIME\":\"2025-03-25T11:58:03Z\",\"TRANSFER_STATE\":\"2\",\"SOURCE_REAL_ID\":\"-1\",\"DEST_REAL_ID\":\"248\",\"CARRIER_LOCATION\":\"HFF09CNA8002_010101\",\"SOURCE_PORT\":\"HFF09AGN0400_UOP01\",\"DESTINATION_PORT\":\"HFF09INA0300_LIP01\",\"RECEIVE_TIME\":\"2025-03-25T11:58:03Z\",\"CARRIER_ID\":\"2F35556,2F35323,2F11113\",\"BATCH_ID\":null,\"LOT_ID\":null,\"CARRIER_TIMESTAMP\":\"2025-03-25T11:59:47Z\",\"JOB_ID\":\"2F35556_235_7061032854410412\",\"FROM\":\"HFF09AGN0400,HFF09AGN0400_UOP08,NULL\",\"TO\":\"HFF09INA0300,HFF09INA0300_LIP01,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-25T11:59:36Z\"},{\"VHL_NAME\":\"HFF09CNA8003\",\"AGV_IDX\":\"2\",\"B_INSTALL\":\"N\",\"NODE_ID\":\"0\",\"REAL_ID\":\"0\",\"VHL_STATE\":\"0\",\"BAY_LIST\":\"3;4;\",\"X\":\"0\",\"Y\":\"0\",\"MODE\":\"0\",\"BATT\":\"0\",\"SUB_GOAL\":\"0\",\"FINAL_GOAL\":\"0\",\"TIMESTAMP\":\"2025-03-25T12:00:50Z\",\"DEGREE\":\"0\",\"STOP_STATE\":\"0\",\"CMD_ID\":null,\"RESERVED_CMD\":null,\"ASSIGN_TIME\":null,\"TRANSFER_STATE\":null,\"SOURCE_REAL_ID\":null,\"DEST_REAL_ID\":null,\"CARRIER_LOCATION\":null,\"SOURCE_PORT\":null,\"DESTINATION_PORT\":null,\"RECEIVE_TIME\":null,\"CARRIER_ID\":null,\"BATCH_ID\":null,\"LOT_ID\":null,\"CARRIER_TIMESTAMP\":null,\"JOB_ID\":null,\"FROM\":\"NULL,NULL,NULL\",\"TO\":\"NULL,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-25T12:00:50Z\"}]"; + private static string responseStokerStack = "[{\"STOCKER_NAME\":\"HFF09AGC0100\",\"CAPACITY\":\"80.12\",\"MAXIMUM_CAPACITY\":\"166\",\"TRAY_CAPACITY\":\"79.72\",\"MAXIMUM_TRAY_CAPACITY\":\"498\",\"RACK_LOAD_COUNT\":\"133\",\"RACK_EMPTY_COUNT\":\"33\",\"RESERVATED_RETURN_COUNT\":\"0\",\"TRAY_COUNT\":\"397\",\"TRAY_REWORK_COUNT_AVG\":\"2\",\"TRAY_REWORK_COUNT_MAX\":\"17\",\"TRAY_REWORK_COUNT_MIN\":\"0\",\"RACK_DISABLE_COUNT\":\"0\",\"KOR_EQP_NAME\":\"H09L_냉각Aging_E01\",\"ENG_EQP_NAME\":\"H09L_CTAging_E01\",\"TIMESTAMP\":\"2025-03-25T12:00:51Z\",\"STEP\":[{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8016\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8025\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8029\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8027\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8042\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"0\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8014\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"}]},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"CAPACITY\":\"89.66\",\"MAXIMUM_CAPACITY\":\"406\",\"TRAY_CAPACITY\":\"88.34\",\"MAXIMUM_TRAY_CAPACITY\":\"1218\",\"RACK_LOAD_COUNT\":\"364\",\"RACK_EMPTY_COUNT\":\"42\",\"RESERVATED_RETURN_COUNT\":\"6\",\"TRAY_COUNT\":\"1076\",\"TRAY_REWORK_COUNT_AVG\":\"6\",\"TRAY_REWORK_COUNT_MAX\":\"64\",\"TRAY_REWORK_COUNT_MIN\":\"0\",\"RACK_DISABLE_COUNT\":\"0\",\"KOR_EQP_NAME\":\"H09L_고온Aging_E01\",\"ENG_EQP_NAME\":\"H09L_HTAging_E01\",\"TIMESTAMP\":\"2025-03-25T12:00:51Z\",\"STEP\":[{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8190\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8040\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8016\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8020\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8150\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8024\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8028\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"0\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8025\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8022\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8026\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8029\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"}]},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"CAPACITY\":\"95.43\",\"MAXIMUM_CAPACITY\":\"810\",\"TRAY_CAPACITY\":\"93.54\",\"MAXIMUM_TRAY_CAPACITY\":\"2430\",\"RACK_LOAD_COUNT\":\"773\",\"RACK_EMPTY_COUNT\":\"37\",\"RESERVATED_RETURN_COUNT\":\"7\",\"TRAY_COUNT\":\"2273\",\"TRAY_REWORK_COUNT_AVG\":\"6\",\"TRAY_REWORK_COUNT_MAX\":\"132\",\"TRAY_REWORK_COUNT_MIN\":\"0\",\"RACK_DISABLE_COUNT\":\"55\",\"KOR_EQP_NAME\":\"H09L_출하창고(Module Cell Aging)_E01\",\"ENG_EQP_NAME\":\"H09L_Module Cell Aging_E01\",\"TIMESTAMP\":\"2025-03-25T12:00:51Z\",\"STEP\":[{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8040\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"0\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8046\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8192\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8190\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8400\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8245\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8106\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8010\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8182\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8220\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8014\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8108\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8250\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8016\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8047\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"}]}]"; + private static string responseStokerCrane = "[{\"STOCKER_NAME\":\"HFF09AGN0101\",\"TIMESTAMP\":\"2025-03-25T12:00:29.92Z\",\"BANK\":\"0\",\"BAY\":\"37\",\"LEVEL\":\"11\",\"REALTIME_FACTOR\":\"57.59\",\"KOR_EQP_NAME\":\"상온Aging #01 S/C\",\"ENG_EQP_NAME\":\"NTAging #01 S/C\",\"HP_CARRIER\":\"2F21582\",\"HP_JOB_ID\":\"2F21582_589_7060449316279234\",\"HP_FROM\":\"HFF09CNV0500,HFF09CNV0500_UOP11,NULL\",\"HP_TO\":\"HFF09AGN0100,NULL,NULL\",\"OP_CARRIER\":null,\"OP_JOB_ID\":null,\"OP_FROM\":null,\"OP_TO\":null},{\"STOCKER_NAME\":\"HFF09CDS0201\",\"TIMESTAMP\":\"2025-03-25T12:00:50.162Z\",\"BANK\":\"0\",\"BAY\":\"12\",\"LEVEL\":\"3\",\"REALTIME_FACTOR\":\"79.7\",\"KOR_EQP_NAME\":\"충방전 Room #02 S/C\",\"ENG_EQP_NAME\":\"Charger Room #02 S/C\",\"HP_CARRIER\":\"2F24482\",\"HP_JOB_ID\":\"2F24482_156_7061057791208036\",\"HP_FROM\":\"HFF09CDS0200,NULL,0400505\",\"HP_TO\":\"HFF09CDS0200,HFF09CDS0200_UBP12,NULL\",\"OP_CARRIER\":\"2F51056\",\"OP_JOB_ID\":\"2F51056_263_7060924349168559\",\"OP_FROM\":\"HFF09CDS0200,NULL,0400404\",\"OP_TO\":\"HFF09CDS0200,HFF09CDS0200_UBP12,NULL\"},{\"STOCKER_NAME\":\"HFF11AGN0401\",\"TIMESTAMP\":\"2025-03-24T17:33:59.819Z\",\"BANK\":\"0\",\"BAY\":\"0\",\"LEVEL\":\"1\",\"REALTIME_FACTOR\":\"0\",\"KOR_EQP_NAME\":\"상온Aging #04 S/C\",\"ENG_EQP_NAME\":\"Norm. Temp.AG Stocker #04 S/C\",\"HP_CARRIER\":\"3F\",\"HP_JOB_ID\":null,\"HP_FROM\":null,\"HP_TO\":null,\"OP_CARRIER\":null,\"OP_JOB_ID\":null,\"OP_FROM\":null,\"OP_TO\":null}]"; + private static string responseCarrier = "[{\"MAIN_CARR_ID\":\"225263\",\"SUB_CARR_ID\":\"225263\",\"CARR_SEQ\":\"1\",\"CARR_USE\":\"EMPTY\",\"CURRENTLOCATION\":\"HFB11ECS1300\",\"CURRENTPORT\":\"HFB11ECS1300_LIP01\",\"CURRENTRACK\":null,\"MOVE_JOBID\":null,\"MOVESTATUS\":\"ARRIVED\",\"FINALTOOLID\":null,\"MOVEFLAG\":\"0\",\"PROD_ID\":null,\"FTY_NO\":null,\"WORK_TYPE\":null,\"MFG_TYPE\":null,\"PROD_DETAIL_CODE\":\"E3A\",\"STEP_ID\":null,\"NEXT_STEP_ID\":null,\"ASSIGN_LOT_QTY\":\"0\",\"FRMT_BATCH_ID\":null,\"CARR_SIZE_TYPE\":\"STACK3\",\"ABNM_VALUE\":\"0\",\"LINE_ID\":\"FM0I\",\"TIMESTAMP\":\"2024-12-10T13:14:05Z\",\"INPUT_QTY\":null,\"GOOD_QTY\":\"0\",\"BAD_QTY\":null,\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"NEXT_KOR_STEP_GROUP_NAME\":null,\"NEXT_ENG_STEP_GROUP_NAME\":null,\"LOT_ID\":null,\"CTH_REEL_ID\":null,\"ANODE_REEL_ID\":null,\"CARR_NO\":null,\"BATCH_GUBUN\":null,\"PROC_IN_TIME\":null,\"IN_CARR_QTY\":null,\"LAST_TKIN_TIME\":null,\"VHCL_ID\":null,\"FIRST_FRMT_INPUT_TIME\":null,\"PRGS_STS\":null,\"JOB_ID\":null,\"FROM_PORT\":\"NULL,NULL,NULL\",\"TO_PORT\":\"NULL,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":null},{\"MAIN_CARR_ID\":\"2F\",\"SUB_CARR_ID\":\"2F\",\"CARR_SEQ\":\"1\",\"CARR_USE\":\"EMPTY\",\"CURRENTLOCATION\":null,\"CURRENTPORT\":null,\"CURRENTRACK\":null,\"MOVE_JOBID\":null,\"MOVESTATUS\":\"ARRIVED\",\"FINALTOOLID\":\"HFF09AGN0300_ZONE01\",\"MOVEFLAG\":\"0\",\"PROD_ID\":null,\"FTY_NO\":null,\"WORK_TYPE\":null,\"MFG_TYPE\":null,\"PROD_DETAIL_CODE\":\"E3A\",\"STEP_ID\":null,\"NEXT_STEP_ID\":null,\"ASSIGN_LOT_QTY\":\"0\",\"FRMT_BATCH_ID\":null,\"CARR_SIZE_TYPE\":\"STACK3\",\"ABNM_VALUE\":\"0\",\"LINE_ID\":\"FM0I\",\"TIMESTAMP\":\"2025-03-24T15:40:22Z\",\"INPUT_QTY\":null,\"GOOD_QTY\":\"0\",\"BAD_QTY\":null,\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"NEXT_KOR_STEP_GROUP_NAME\":null,\"NEXT_ENG_STEP_GROUP_NAME\":null,\"LOT_ID\":null,\"CTH_REEL_ID\":null,\"ANODE_REEL_ID\":null,\"CARR_NO\":null,\"BATCH_GUBUN\":null,\"PROC_IN_TIME\":null,\"IN_CARR_QTY\":null,\"LAST_TKIN_TIME\":null,\"VHCL_ID\":null,\"FIRST_FRMT_INPUT_TIME\":null,\"PRGS_STS\":null,\"JOB_ID\":\"2F_470_6987558774994436\",\"FROM_PORT\":\"HFF09CNV0500,HFF09CNV0500_UOP12,NULL\",\"TO_PORT\":\"HFF09AGN0300,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-24T15:39:52Z\"},{\"MAIN_CARR_ID\":\"2F U{9\",\"SUB_CARR_ID\":\"2F U{9\",\"CARR_SEQ\":\"1\",\"CARR_USE\":\"EMPTY\",\"CURRENTLOCATION\":\"HFB09ICS0500\",\"CURRENTPORT\":\"HFB09ICS0500_LIP01\",\"CURRENTRACK\":null,\"MOVE_JOBID\":null,\"MOVESTATUS\":\"ARRIVED\",\"FINALTOOLID\":null,\"MOVEFLAG\":\"0\",\"PROD_ID\":null,\"FTY_NO\":null,\"WORK_TYPE\":null,\"MFG_TYPE\":null,\"PROD_DETAIL_CODE\":\"E3A\",\"STEP_ID\":null,\"NEXT_STEP_ID\":null,\"ASSIGN_LOT_QTY\":\"0\",\"FRMT_BATCH_ID\":null,\"CARR_SIZE_TYPE\":\"STACK3\",\"ABNM_VALUE\":\"0\",\"LINE_ID\":\"FM0I\",\"TIMESTAMP\":\"2025-02-02T11:48:55Z\",\"INPUT_QTY\":null,\"GOOD_QTY\":\"0\",\"BAD_QTY\":null,\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"NEXT_KOR_STEP_GROUP_NAME\":null,\"NEXT_ENG_STEP_GROUP_NAME\":null,\"LOT_ID\":null,\"CTH_REEL_ID\":null,\"ANODE_REEL_ID\":null,\"CARR_NO\":null,\"BATCH_GUBUN\":null,\"PROC_IN_TIME\":null,\"IN_CARR_QTY\":null,\"LAST_TKIN_TIME\":null,\"VHCL_ID\":null,\"FIRST_FRMT_INPUT_TIME\":null,\"PRGS_STS\":null,\"JOB_ID\":null,\"FROM_PORT\":\"NULL,NULL,NULL\",\"TO_PORT\":\"NULL,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":null},{\"MAIN_CARR_ID\":\"2F00001\",\"SUB_CARR_ID\":\"2F00001,2F23054,2F02234\",\"CARR_SEQ\":\"3\",\"CARR_USE\":\"FULL\",\"CURRENTLOCATION\":\"HFF09AGN0100_0200210\",\"CURRENTPORT\":null,\"CURRENTRACK\":\"0200210\",\"MOVE_JOBID\":null,\"MOVESTATUS\":\"ARRIVED\",\"FINALTOOLID\":null,\"MOVEFLAG\":\"0\",\"PROD_ID\":\"CP7024F111A\",\"FTY_NO\":\"70B0\",\"WORK_TYPE\":\"NM\",\"MFG_TYPE\":\"PP02\",\"PROD_DETAIL_CODE\":\"E3A\",\"STEP_ID\":\"8016\",\"NEXT_STEP_ID\":\"8106\",\"ASSIGN_LOT_QTY\":\"288\",\"FRMT_BATCH_ID\":\"H1J70SA253GK21\",\"CARR_SIZE_TYPE\":\"STACK3\",\"ABNM_VALUE\":\"0\",\"LINE_ID\":\"FM0I\",\"TIMESTAMP\":\"2025-03-24T15:51:07Z\",\"INPUT_QTY\":\"288\",\"GOOD_QTY\":\"288\",\"BAD_QTY\":null,\"KOR_STEP_GROUP_NAME\":\"상온 Aging4\",\"ENG_STEP_GROUP_NAME\":\"Normal Temp Aging4\",\"NEXT_KOR_STEP_GROUP_NAME\":\"IR/OCV4\",\"NEXT_ENG_STEP_GROUP_NAME\":\"IR/OCV4\",\"LOT_ID\":\"36092531867188BKDS1B\",\"CTH_REEL_ID\":\"H16CP25306A120D\",\"ANODE_REEL_ID\":\"H16AP25221A117A\",\"CARR_NO\":\"H1J70SA251DC13-5171\",\"BATCH_GUBUN\":\"PP02\",\"PROC_IN_TIME\":\"2025-03-18T15:36:30Z\",\"IN_CARR_QTY\":\"288\",\"LAST_TKIN_TIME\":\"2025-03-24T15:51:04Z\",\"VHCL_ID\":null,\"FIRST_FRMT_INPUT_TIME\":\"2025-03-18T15:36:32Z\",\"PRGS_STS\":\"PROC\",\"JOB_ID\":\"2F00001_663_6987788937076875\",\"FROM_PORT\":\"HFF09CNV0500,HFF09CNV0500_UOP11,NULL\",\"TO_PORT\":\"HFF09AGN0100,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-24T15:51:08Z\"}]"; + private static string responseEquipment = "[{\"LINE_ID\":\"FM0I\",\"KOR_LINE_NAME\":\"헝가리 화성 #09-1\",\"ENG_LINE_NAME\":\"HT #09-1\",\"EQP_ID\":\"HFF09CDC0100\",\"KOR_EQP_NAME\":\"충방전기 #01\",\"ENG_EQP_NAME\":\"Charger #01\",\"UP_DOWN_STS\":null,\"AUTO_MODE_STS\":null,\"STATE_ID\":\"IDLE\",\"NTW_STS\":\"CONNECT\",\"RECIPE_ID\":null,\"WO_ID\":null,\"EQP_TYPE\":\"CD\",\"TIMESTAMP\":\"2025-01-31T20:18:01Z\",\"OLD_KOR_EQP_NAME\":\"H09L_충방전기_L01_01(PC01)\",\"OLD_ENG_EQP_NAME\":\"H09L_Charger_L01_01(PC01)\"},{\"LINE_ID\":\"BX0I\",\"KOR_LINE_NAME\":\"헝가리 포장 #09-1\",\"ENG_LINE_NAME\":\"HB #09-1\",\"EQP_ID\":\"HFB09ICS0100\",\"KOR_EQP_NAME\":\"특성검사 #01\",\"ENG_EQP_NAME\":\"INSPECTION #01\",\"UP_DOWN_STS\":null,\"AUTO_MODE_STS\":null,\"STATE_ID\":\"IDLE\",\"NTW_STS\":\"CONNECT\",\"RECIPE_ID\":null,\"WO_ID\":null,\"EQP_TYPE\":\"N/A\",\"TIMESTAMP\":\"2025-02-08T14:04:45Z\",\"OLD_KOR_EQP_NAME\":\"H09L_특성검사_ECS_E01\",\"OLD_ENG_EQP_NAME\":\"H09L_INSPECTION_INLINE_ECS_E01\"},{\"LINE_ID\":\"FM0I\",\"KOR_LINE_NAME\":\"헝가리 화성 #09-1\",\"ENG_LINE_NAME\":\"HT #09-1\",\"EQP_ID\":\"HFF09HVI0100\",\"KOR_EQP_NAME\":\"HV #01\",\"ENG_EQP_NAME\":\"HV #01\",\"UP_DOWN_STS\":null,\"AUTO_MODE_STS\":\"AUTO\",\"STATE_ID\":\"DOWN\",\"NTW_STS\":\"CONNECT\",\"RECIPE_ID\":\"H1IBBBX22052301M\",\"WO_ID\":\"H1IBBBX22052301M\",\"EQP_TYPE\":null,\"TIMESTAMP\":\"2025-03-13T00:07:56Z\",\"OLD_KOR_EQP_NAME\":\"H09L_HV_E01_#01\",\"OLD_ENG_EQP_NAME\":\"H09L_HV_E01_#01\"}]"; + private static string responseAlarm = "[{\"ID\":\"1c7dd047-6f7d-4551-869c-bbe4fee6d4fe\",\"ALARM_TYPE\":\"logistics\",\"LEVEL\":\"HEAVY\",\"LOGISTIC\":\"1\",\"STATE\":\"SET\",\"MESSAGE\":\"Load PIO T1 Timeout (60s)\",\"CODE\":\"VHL_ALARM\",\"ICON\":\"AGV\",\"SET_TIME\":\"2025-03-25T12:00:50Z\",\"CLEAR_TIME\":null,\"TIMESTAMP\":\"2025-03-25T12:00:50Z\",\"MACHINENAME\":null,\"UNITNAME\":null,\"JOB_ID\":null,\"MAIN_CARR_ID\":null,\"FROM_EQP_ID\":null,\"FROM_UNIT_ID\":null,\"TO_EQP_ID\":null,\"TO_PORT_ID\":null,\"SHOPNAME\":\"S100\",\"UPDATE_TIME\":\"2025-03-25T12:00:50Z\",\"TRANSPORT_EQP_NAME\":null,\"TRANSPORT_UNIT_NAME\":null,\"FROM_EQP_NAME\":null,\"FROM_UNIT_NAME\":null,\"TO_EQP_NAME\":null,\"TO_UNIT_NAME\":null,\"TRANSPORT_EQP_ID\":\"HEA04ACS0100\",\"TRANSPORT_UNIT_ID\":\"HEC04RDA4069\",\"TO_UNIT_ID\":null,\"CONTINUE_TIME\":null,\"REFERENCE_ID\":\"73374eaf-2aae-4d83-9932-6104d25a758f\"},{\"ID\":\"7730f104-064e-40bc-8a3c-0cfd5471c59e\",\"ALARM_TYPE\":\"event\",\"LEVEL\":\"HEAVY\",\"LOGISTIC\":\"1\",\"STATE\":\"SET\",\"MESSAGE\":\"HFF11CDS0100_LBP05 Production Supply Logistics Delay\",\"CODE\":\"PROD_LOSS_IN\",\"ICON\":\"TR\",\"SET_TIME\":\"2025-03-25T12:00:48.699Z\",\"CLEAR_TIME\":null,\"TIMESTAMP\":\"2025-03-25T12:00:48.699Z\",\"MACHINENAME\":null,\"UNITNAME\":null,\"JOB_ID\":null,\"MAIN_CARR_ID\":null,\"FROM_EQP_ID\":null,\"FROM_UNIT_ID\":null,\"TO_EQP_ID\":null,\"TO_PORT_ID\":null,\"SHOPNAME\":\"S300\",\"UPDATE_TIME\":\"2025-03-25T12:00:48.699Z\",\"TRANSPORT_EQP_NAME\":\"충방전 Room #01\",\"TRANSPORT_UNIT_NAME\":\"충방전 AG #1호기 입고대 Track #1-4\",\"FROM_EQP_NAME\":null,\"FROM_UNIT_NAME\":null,\"TO_EQP_NAME\":null,\"TO_UNIT_NAME\":null,\"TRANSPORT_EQP_ID\":\"HFF11CDS0100\",\"TRANSPORT_UNIT_ID\":\"HFF11CDS0100_LBP05\",\"TO_UNIT_ID\":null,\"CONTINUE_TIME\":null,\"REFERENCE_ID\":null},{\"ID\":\"b20b107b-bdea-4b41-be6e-7f147b8fb6fa\",\"ALARM_TYPE\":\"event\",\"LEVEL\":\"HEAVY\",\"LOGISTIC\":\"1\",\"STATE\":\"SET\",\"MESSAGE\":\"HFF11CDS0100_LBP04 Production Supply Logistics Delay\",\"CODE\":\"PROD_LOSS_IN\",\"ICON\":\"TR\",\"SET_TIME\":\"2025-03-25T12:00:44.686Z\",\"CLEAR_TIME\":null,\"TIMESTAMP\":\"2025-03-25T12:00:44.686Z\",\"MACHINENAME\":null,\"UNITNAME\":null,\"JOB_ID\":null,\"MAIN_CARR_ID\":null,\"FROM_EQP_ID\":null,\"FROM_UNIT_ID\":null,\"TO_EQP_ID\":null,\"TO_PORT_ID\":null,\"SHOPNAME\":\"S300\",\"UPDATE_TIME\":\"2025-03-25T12:00:44.686Z\",\"TRANSPORT_EQP_NAME\":\"충방전 Room #01\",\"TRANSPORT_UNIT_NAME\":\"충방전 AG #1호기 입고대 Track #1-3(Unstack Position Down)\",\"FROM_EQP_NAME\":null,\"FROM_UNIT_NAME\":null,\"TO_EQP_NAME\":null,\"TO_UNIT_NAME\":null,\"TRANSPORT_EQP_ID\":\"HFF11CDS0100\",\"TRANSPORT_UNIT_ID\":\"HFF11CDS0100_LBP04\",\"TO_UNIT_ID\":null,\"CONTINUE_TIME\":null,\"REFERENCE_ID\":null}]"; + private static string responseSimulationRank = "[{\"type\":\"warehouseUtilization\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"#11,12 상온에이징 3호기\",\"targetCode\":\"HFF11AGN0300\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":99.52,\"predictValueUnit\":\"percent\",\"resultValue\":99.52,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 상온에이징 4호기\",\"targetCode\":\"HFF11AGN0400\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":98.652,\"predictValueUnit\":\"percent\",\"resultValue\":98.652,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":93.93,\"predictValueUnit\":\"percent\",\"resultValue\":93.93,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"#11,12 상온에이징 3호기\",\"targetCode\":\"HFF11AGN0300\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":99.52,\"predictValueUnit\":\"percent\",\"resultValue\":99.52,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 상온에이징 4호기\",\"targetCode\":\"HFF11AGN0400\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":98.652,\"predictValueUnit\":\"percent\",\"resultValue\":98.652,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":94.82,\"predictValueUnit\":\"percent\",\"resultValue\":94.82,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"#11,12 상온에이징 3호기\",\"targetCode\":\"HFF11AGN0300\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":99.52,\"predictValueUnit\":\"percent\",\"resultValue\":99.52,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 상온에이징 4호기\",\"targetCode\":\"HFF11AGN0400\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":98.652,\"predictValueUnit\":\"percent\",\"resultValue\":98.652,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 고온에이징\",\"targetCode\":\"HFF11AGH0100\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":93.842,\"predictValueUnit\":\"percent\",\"resultValue\":93.842,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"#11,12 상온에이징 3호기\",\"targetCode\":\"HFF11AGN0300\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":99.64,\"predictValueUnit\":\"percent\",\"resultValue\":99.64,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 상온에이징 4호기\",\"targetCode\":\"HFF11AGN0400\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":98.652,\"predictValueUnit\":\"percent\",\"resultValue\":98.652,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":96.32,\"predictValueUnit\":\"percent\",\"resultValue\":96.32,\"resultValueUnit\":\"percent\"}]}]},{\"type\":\"agvBottleneck\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"G층 후면\",\"targetCode\":\"GF_SOUTH\",\"pivot\":37.1,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":76.494,\"predictValueUnit\":\"minute\",\"resultValue\":76,\"resultValueUnit\":\"minute\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":10,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":23.681,\"predictValueUnit\":\"minute\",\"resultValue\":23,\"resultValueUnit\":\"minute\"},{\"targetName\":\"1층 전면\",\"targetCode\":\"1F_NORTH\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":21.783,\"predictValueUnit\":\"minute\",\"resultValue\":21,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":182.06,\"predictValueUnit\":\"minute\",\"resultValue\":182,\"resultValueUnit\":\"minute\"},{\"targetName\":\"G층 후면\",\"targetCode\":\"GF_SOUTH\",\"pivot\":37.1,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":53.191,\"predictValueUnit\":\"minute\",\"resultValue\":53,\"resultValueUnit\":\"minute\"},{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"TAPING\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":43.87,\"predictValueUnit\":\"minute\",\"resultValue\":43,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":140.345,\"predictValueUnit\":\"minute\",\"resultValue\":140,\"resultValueUnit\":\"minute\"},{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"TAPING\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":110.772,\"predictValueUnit\":\"minute\",\"resultValue\":110,\"resultValueUnit\":\"minute\"},{\"targetName\":\"G층 후면\",\"targetCode\":\"GF_SOUTH\",\"pivot\":37.1,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":51.445,\"predictValueUnit\":\"minute\",\"resultValue\":51,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":10,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":1380.243,\"predictValueUnit\":\"minute\",\"resultValue\":1380,\"resultValueUnit\":\"minute\"},{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":333.995,\"predictValueUnit\":\"minute\",\"resultValue\":333,\"resultValueUnit\":\"minute\"},{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"TAPING\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":130.852,\"predictValueUnit\":\"minute\",\"resultValue\":130,\"resultValueUnit\":\"minute\"}]}]},{\"type\":\"logisticsDelay\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"IROCV → 상온에이징\",\"targetCode\":\"IRV_AGN\",\"pivot\":38,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":822.254,\"predictValueUnit\":\"minute\",\"resultValue\":13,\"resultValueUnit\":\"minute\"},{\"targetName\":\"냉각에이징 → 상온에이징\",\"targetCode\":\"AGC_AGN\",\"pivot\":25,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":755.885,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"},{\"targetName\":\"조립 → 함침에이징\",\"targetCode\":\"ASS_AGN\",\"pivot\":20,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":737,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"IROCV → 상온에이징\",\"targetCode\":\"IRV_AGN\",\"pivot\":38,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":822.254,\"predictValueUnit\":\"minute\",\"resultValue\":13,\"resultValueUnit\":\"minute\"},{\"targetName\":\"냉각에이징 → 상온에이징\",\"targetCode\":\"AGC_AGN\",\"pivot\":25,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":755.885,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"},{\"targetName\":\"조립 → 함침에이징\",\"targetCode\":\"ASS_AGN\",\"pivot\":20,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":686.204,\"predictValueUnit\":\"minute\",\"resultValue\":11,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"조립 → 함침에이징\",\"targetCode\":\"ASS_AGN\",\"pivot\":20,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":997.049,\"predictValueUnit\":\"minute\",\"resultValue\":16,\"resultValueUnit\":\"minute\"},{\"targetName\":\"IROCV → 상온에이징\",\"targetCode\":\"IRV_AGN\",\"pivot\":38,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":822.91,\"predictValueUnit\":\"minute\",\"resultValue\":13,\"resultValueUnit\":\"minute\"},{\"targetName\":\"상온에이징 → IROCV\",\"targetCode\":\"AGN_IRV\",\"pivot\":112,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":765.716,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"IROCV → 상온에이징\",\"targetCode\":\"IRV_AGN\",\"pivot\":38,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":980.425,\"predictValueUnit\":\"minute\",\"resultValue\":16,\"resultValueUnit\":\"minute\"},{\"targetName\":\"상온에이징 → IROCV\",\"targetCode\":\"AGN_IRV\",\"pivot\":112,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":955.849,\"predictValueUnit\":\"minute\",\"resultValue\":15,\"resultValueUnit\":\"minute\"},{\"targetName\":\"냉각에이징 → 상온에이징\",\"targetCode\":\"AGC_AGN\",\"pivot\":25,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":755.286,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"}]}]},{\"type\":\"agvUtilization\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":38.691,\"predictValueUnit\":\"percent\",\"resultValue\":38.691,\"resultValueUnit\":\"percent\"},{\"targetName\":\"조립 출고\",\"targetCode\":\"ASSYATC\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":35.974,\"predictValueUnit\":\"percent\",\"resultValue\":35.974,\"resultValueUnit\":\"percent\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":32.385,\"predictValueUnit\":\"percent\",\"resultValue\":32.385,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"1F_TAPING\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":50.408,\"predictValueUnit\":\"percent\",\"resultValue\":50.408,\"resultValueUnit\":\"percent\"},{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":37.605,\"predictValueUnit\":\"percent\",\"resultValue\":37.605,\"resultValueUnit\":\"percent\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":30.48,\"predictValueUnit\":\"percent\",\"resultValue\":30.48,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"1F_TAPING\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":71.16,\"predictValueUnit\":\"percent\",\"resultValue\":71.16,\"resultValueUnit\":\"percent\"},{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":30.43,\"predictValueUnit\":\"percent\",\"resultValue\":30.43,\"resultValueUnit\":\"percent\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":28.377,\"predictValueUnit\":\"percent\",\"resultValue\":28.377,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"1F_TAPING\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":88.406,\"predictValueUnit\":\"percent\",\"resultValue\":88.406,\"resultValueUnit\":\"percent\"},{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":33.697,\"predictValueUnit\":\"percent\",\"resultValue\":33.697,\"resultValueUnit\":\"percent\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":32.705,\"predictValueUnit\":\"percent\",\"resultValue\":32.705,\"resultValueUnit\":\"percent\"}]}]},{\"type\":\"craneUtilization\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":81.02,\"predictValueUnit\":\"percent\",\"resultValue\":81.02,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 상온에이징 4호기\",\"targetCode\":\"HFF09AGN0401\",\"pivot\":90,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":71.631,\"predictValueUnit\":\"percent\",\"resultValue\":71.631,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 2호기\",\"targetCode\":\"HFF09PCH0200\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":68.93,\"predictValueUnit\":\"percent\",\"resultValue\":68.93,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":80.15,\"predictValueUnit\":\"percent\",\"resultValue\":80.15,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 2호기\",\"targetCode\":\"HFF09PCH0200\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":72.68,\"predictValueUnit\":\"percent\",\"resultValue\":72.68,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 상온에이징 4호기\",\"targetCode\":\"HFF09AGN0401\",\"pivot\":90,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":69.07,\"predictValueUnit\":\"percent\",\"resultValue\":69.07,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":81.22,\"predictValueUnit\":\"percent\",\"resultValue\":81.22,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 2호기\",\"targetCode\":\"HFF09PCH0200\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":69.72,\"predictValueUnit\":\"percent\",\"resultValue\":69.72,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 상온에이징 4호기\",\"targetCode\":\"HFF09AGN0401\",\"pivot\":90,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":65.222,\"predictValueUnit\":\"percent\",\"resultValue\":65.222,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":81.32,\"predictValueUnit\":\"percent\",\"resultValue\":81.32,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 2호기\",\"targetCode\":\"HFF09PCH0200\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":70.35,\"predictValueUnit\":\"percent\",\"resultValue\":70.35,\"resultValueUnit\":\"percent\"},{\"targetName\":\"버퍼에이징\",\"targetCode\":\"HFF09AGM0101\",\"pivot\":90,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":67.729,\"predictValueUnit\":\"percent\",\"resultValue\":67.729,\"resultValueUnit\":\"percent\"}]}]}]"; + private static string responseBaseInfo = "{\"AGV\":[{\"VHL_NAME\":\"HFF09CNA8001\",\"AGV_IDX\":\"0\",\"B_INSTALL\":\"N\",\"NODE_ID\":\"0\",\"REAL_ID\":\"0\",\"VHL_STATE\":\"0\",\"BAY_LIST\":\"1;\",\"X\":\"0\",\"Y\":\"0\",\"MODE\":\"0\",\"BATT\":\"0\",\"SUB_GOAL\":\"0\",\"FINAL_GOAL\":\"0\",\"TIMESTAMP\":\"2025-03-08T15:34:27Z\",\"DEGREE\":\"0\",\"STOP_STATE\":\"0\",\"CMD_ID\":null,\"RESERVED_CMD\":null,\"ASSIGN_TIME\":null,\"TRANSFER_STATE\":null,\"SOURCE_REAL_ID\":null,\"DEST_REAL_ID\":null,\"CARRIER_LOCATION\":null,\"SOURCE_PORT\":null,\"DESTINATION_PORT\":null,\"RECEIVE_TIME\":null,\"CARRIER_ID\":null,\"BATCH_ID\":null,\"LOT_ID\":null,\"CARRIER_TIMESTAMP\":null,\"JOB_ID\":null,\"FROM\":\"NULL,NULL,NULL\",\"TO\":\"NULL,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-08T15:34:27Z\"},{\"VHL_NAME\":\"HFF09CNA8002\",\"AGV_IDX\":\"1\",\"B_INSTALL\":\"Y\",\"NODE_ID\":\"231\",\"REAL_ID\":\"231\",\"VHL_STATE\":\"11\",\"BAY_LIST\":\"7;\",\"X\":\"154136\",\"Y\":\"20166\",\"MODE\":\"1\",\"BATT\":\"38\",\"SUB_GOAL\":\"388\",\"FINAL_GOAL\":\"248\",\"TIMESTAMP\":\"2025-03-25T12:00:49.571Z\",\"DEGREE\":\"179.2\",\"STOP_STATE\":\"0\",\"CMD_ID\":\"2F35556_235_7061032854410412\",\"RESERVED_CMD\":null,\"ASSIGN_TIME\":\"2025-03-25T11:58:03Z\",\"TRANSFER_STATE\":\"2\",\"SOURCE_REAL_ID\":\"-1\",\"DEST_REAL_ID\":\"248\",\"CARRIER_LOCATION\":\"HFF09CNA8002_010101\",\"SOURCE_PORT\":\"HFF09AGN0400_UOP01\",\"DESTINATION_PORT\":\"HFF09INA0300_LIP01\",\"RECEIVE_TIME\":\"2025-03-25T11:58:03Z\",\"CARRIER_ID\":\"2F35556,2F35323,2F11113\",\"BATCH_ID\":null,\"LOT_ID\":null,\"CARRIER_TIMESTAMP\":\"2025-03-25T11:59:47Z\",\"JOB_ID\":\"2F35556_235_7061032854410412\",\"FROM\":\"HFF09AGN0400,HFF09AGN0400_UOP08,NULL\",\"TO\":\"HFF09INA0300,HFF09INA0300_LIP01,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-25T11:59:36Z\"},{\"VHL_NAME\":\"HFF09CNA8003\",\"AGV_IDX\":\"2\",\"B_INSTALL\":\"N\",\"NODE_ID\":\"0\",\"REAL_ID\":\"0\",\"VHL_STATE\":\"0\",\"BAY_LIST\":\"3;4;\",\"X\":\"0\",\"Y\":\"0\",\"MODE\":\"0\",\"BATT\":\"0\",\"SUB_GOAL\":\"0\",\"FINAL_GOAL\":\"0\",\"TIMESTAMP\":\"2025-03-25T12:00:50Z\",\"DEGREE\":\"0\",\"STOP_STATE\":\"0\",\"CMD_ID\":null,\"RESERVED_CMD\":null,\"ASSIGN_TIME\":null,\"TRANSFER_STATE\":null,\"SOURCE_REAL_ID\":null,\"DEST_REAL_ID\":null,\"CARRIER_LOCATION\":null,\"SOURCE_PORT\":null,\"DESTINATION_PORT\":null,\"RECEIVE_TIME\":null,\"CARRIER_ID\":null,\"BATCH_ID\":null,\"LOT_ID\":null,\"CARRIER_TIMESTAMP\":null,\"JOB_ID\":null,\"FROM\":\"NULL,NULL,NULL\",\"TO\":\"NULL,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-25T12:00:50Z\"}], \"STOCKER_STACK\":[{\"STOCKER_NAME\":\"HFF09AGC0100\",\"CAPACITY\":\"80.12\",\"MAXIMUM_CAPACITY\":\"166\",\"TRAY_CAPACITY\":\"79.72\",\"MAXIMUM_TRAY_CAPACITY\":\"498\",\"RACK_LOAD_COUNT\":\"133\",\"RACK_EMPTY_COUNT\":\"33\",\"RESERVATED_RETURN_COUNT\":\"0\",\"TRAY_COUNT\":\"397\",\"TRAY_REWORK_COUNT_AVG\":\"2\",\"TRAY_REWORK_COUNT_MAX\":\"17\",\"TRAY_REWORK_COUNT_MIN\":\"0\",\"RACK_DISABLE_COUNT\":\"0\",\"KOR_EQP_NAME\":\"H09L_냉각Aging_E01\",\"ENG_EQP_NAME\":\"H09L_CTAging_E01\",\"TIMESTAMP\":\"2025-03-25T12:00:51Z\",\"STEP\":[{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8016\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8025\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8029\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8027\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8042\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"0\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGC0100\",\"STEP_ID\":\"8014\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"33\",\"TOTAL\":\"166\",\"STEP_CAPACITY\":\"19.88\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"}]},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"CAPACITY\":\"89.66\",\"MAXIMUM_CAPACITY\":\"406\",\"TRAY_CAPACITY\":\"88.34\",\"MAXIMUM_TRAY_CAPACITY\":\"1218\",\"RACK_LOAD_COUNT\":\"364\",\"RACK_EMPTY_COUNT\":\"42\",\"RESERVATED_RETURN_COUNT\":\"6\",\"TRAY_COUNT\":\"1076\",\"TRAY_REWORK_COUNT_AVG\":\"6\",\"TRAY_REWORK_COUNT_MAX\":\"64\",\"TRAY_REWORK_COUNT_MIN\":\"0\",\"RACK_DISABLE_COUNT\":\"0\",\"KOR_EQP_NAME\":\"H09L_고온Aging_E01\",\"ENG_EQP_NAME\":\"H09L_HTAging_E01\",\"TIMESTAMP\":\"2025-03-25T12:00:51Z\",\"STEP\":[{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8190\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8040\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8016\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8020\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8150\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8024\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8028\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"0\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8025\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8022\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8026\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGH0100\",\"STEP_ID\":\"8029\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"42\",\"TOTAL\":\"406\",\"STEP_CAPACITY\":\"10.34\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"}]},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"CAPACITY\":\"95.43\",\"MAXIMUM_CAPACITY\":\"810\",\"TRAY_CAPACITY\":\"93.54\",\"MAXIMUM_TRAY_CAPACITY\":\"2430\",\"RACK_LOAD_COUNT\":\"773\",\"RACK_EMPTY_COUNT\":\"37\",\"RESERVATED_RETURN_COUNT\":\"7\",\"TRAY_COUNT\":\"2273\",\"TRAY_REWORK_COUNT_AVG\":\"6\",\"TRAY_REWORK_COUNT_MAX\":\"132\",\"TRAY_REWORK_COUNT_MIN\":\"0\",\"RACK_DISABLE_COUNT\":\"55\",\"KOR_EQP_NAME\":\"H09L_출하창고(Module Cell Aging)_E01\",\"ENG_EQP_NAME\":\"H09L_Module Cell Aging_E01\",\"TIMESTAMP\":\"2025-03-25T12:00:51Z\",\"STEP\":[{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8040\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"0\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8046\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8192\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8190\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8400\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8245\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8106\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8010\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8182\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8220\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8014\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8108\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8250\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8016\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"},{\"STOCKER_NAME\":\"HFF09AGM0100\",\"STEP_ID\":\"8047\",\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"RACK_STEP_COUNT\":\"37\",\"TOTAL\":\"810\",\"STEP_CAPACITY\":\"4.57\",\"TIMESTAMP\":\"2025-03-25T13:00:51\"}]}], \"STOCKER_CRANE\":[{\"STOCKER_NAME\":\"HFF09AGN0101\",\"TIMESTAMP\":\"2025-03-25T12:00:29.92Z\",\"BANK\":\"0\",\"BAY\":\"37\",\"LEVEL\":\"11\",\"REALTIME_FACTOR\":\"57.59\",\"KOR_EQP_NAME\":\"상온Aging #01 S/C\",\"ENG_EQP_NAME\":\"NTAging #01 S/C\",\"HP_CARRIER\":\"2F21582\",\"HP_JOB_ID\":\"2F21582_589_7060449316279234\",\"HP_FROM\":\"HFF09CNV0500,HFF09CNV0500_UOP11,NULL\",\"HP_TO\":\"HFF09AGN0100,NULL,NULL\",\"OP_CARRIER\":null,\"OP_JOB_ID\":null,\"OP_FROM\":null,\"OP_TO\":null},{\"STOCKER_NAME\":\"HFF09CDS0201\",\"TIMESTAMP\":\"2025-03-25T12:00:50.162Z\",\"BANK\":\"0\",\"BAY\":\"12\",\"LEVEL\":\"3\",\"REALTIME_FACTOR\":\"79.7\",\"KOR_EQP_NAME\":\"충방전 Room #02 S/C\",\"ENG_EQP_NAME\":\"Charger Room #02 S/C\",\"HP_CARRIER\":\"2F24482\",\"HP_JOB_ID\":\"2F24482_156_7061057791208036\",\"HP_FROM\":\"HFF09CDS0200,NULL,0400505\",\"HP_TO\":\"HFF09CDS0200,HFF09CDS0200_UBP12,NULL\",\"OP_CARRIER\":\"2F51056\",\"OP_JOB_ID\":\"2F51056_263_7060924349168559\",\"OP_FROM\":\"HFF09CDS0200,NULL,0400404\",\"OP_TO\":\"HFF09CDS0200,HFF09CDS0200_UBP12,NULL\"},{\"STOCKER_NAME\":\"HFF11AGN0401\",\"TIMESTAMP\":\"2025-03-24T17:33:59.819Z\",\"BANK\":\"0\",\"BAY\":\"0\",\"LEVEL\":\"1\",\"REALTIME_FACTOR\":\"0\",\"KOR_EQP_NAME\":\"상온Aging #04 S/C\",\"ENG_EQP_NAME\":\"Norm. Temp.AG Stocker #04 S/C\",\"HP_CARRIER\":\"3F\",\"HP_JOB_ID\":null,\"HP_FROM\":null,\"HP_TO\":null,\"OP_CARRIER\":null,\"OP_JOB_ID\":null,\"OP_FROM\":null,\"OP_TO\":null}], \"CARRIER\":[{\"MAIN_CARR_ID\":\"225263\",\"SUB_CARR_ID\":\"225263\",\"CARR_SEQ\":\"1\",\"CARR_USE\":\"EMPTY\",\"CURRENTLOCATION\":\"HFB11ECS1300\",\"CURRENTPORT\":\"HFB11ECS1300_LIP01\",\"CURRENTRACK\":null,\"MOVE_JOBID\":null,\"MOVESTATUS\":\"ARRIVED\",\"FINALTOOLID\":null,\"MOVEFLAG\":\"0\",\"PROD_ID\":null,\"FTY_NO\":null,\"WORK_TYPE\":null,\"MFG_TYPE\":null,\"PROD_DETAIL_CODE\":\"E3A\",\"STEP_ID\":null,\"NEXT_STEP_ID\":null,\"ASSIGN_LOT_QTY\":\"0\",\"FRMT_BATCH_ID\":null,\"CARR_SIZE_TYPE\":\"STACK3\",\"ABNM_VALUE\":\"0\",\"LINE_ID\":\"FM0I\",\"TIMESTAMP\":\"2024-12-10T13:14:05Z\",\"INPUT_QTY\":null,\"GOOD_QTY\":\"0\",\"BAD_QTY\":null,\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"NEXT_KOR_STEP_GROUP_NAME\":null,\"NEXT_ENG_STEP_GROUP_NAME\":null,\"LOT_ID\":null,\"CTH_REEL_ID\":null,\"ANODE_REEL_ID\":null,\"CARR_NO\":null,\"BATCH_GUBUN\":null,\"PROC_IN_TIME\":null,\"IN_CARR_QTY\":null,\"LAST_TKIN_TIME\":null,\"VHCL_ID\":null,\"FIRST_FRMT_INPUT_TIME\":null,\"PRGS_STS\":null,\"JOB_ID\":null,\"FROM_PORT\":\"NULL,NULL,NULL\",\"TO_PORT\":\"NULL,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":null},{\"MAIN_CARR_ID\":\"2F\",\"SUB_CARR_ID\":\"2F\",\"CARR_SEQ\":\"1\",\"CARR_USE\":\"EMPTY\",\"CURRENTLOCATION\":null,\"CURRENTPORT\":null,\"CURRENTRACK\":null,\"MOVE_JOBID\":null,\"MOVESTATUS\":\"ARRIVED\",\"FINALTOOLID\":\"HFF09AGN0300_ZONE01\",\"MOVEFLAG\":\"0\",\"PROD_ID\":null,\"FTY_NO\":null,\"WORK_TYPE\":null,\"MFG_TYPE\":null,\"PROD_DETAIL_CODE\":\"E3A\",\"STEP_ID\":null,\"NEXT_STEP_ID\":null,\"ASSIGN_LOT_QTY\":\"0\",\"FRMT_BATCH_ID\":null,\"CARR_SIZE_TYPE\":\"STACK3\",\"ABNM_VALUE\":\"0\",\"LINE_ID\":\"FM0I\",\"TIMESTAMP\":\"2025-03-24T15:40:22Z\",\"INPUT_QTY\":null,\"GOOD_QTY\":\"0\",\"BAD_QTY\":null,\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"NEXT_KOR_STEP_GROUP_NAME\":null,\"NEXT_ENG_STEP_GROUP_NAME\":null,\"LOT_ID\":null,\"CTH_REEL_ID\":null,\"ANODE_REEL_ID\":null,\"CARR_NO\":null,\"BATCH_GUBUN\":null,\"PROC_IN_TIME\":null,\"IN_CARR_QTY\":null,\"LAST_TKIN_TIME\":null,\"VHCL_ID\":null,\"FIRST_FRMT_INPUT_TIME\":null,\"PRGS_STS\":null,\"JOB_ID\":\"2F_470_6987558774994436\",\"FROM_PORT\":\"HFF09CNV0500,HFF09CNV0500_UOP12,NULL\",\"TO_PORT\":\"HFF09AGN0300,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-24T15:39:52Z\"},{\"MAIN_CARR_ID\":\"2F U{9\",\"SUB_CARR_ID\":\"2F U{9\",\"CARR_SEQ\":\"1\",\"CARR_USE\":\"EMPTY\",\"CURRENTLOCATION\":\"HFB09ICS0500\",\"CURRENTPORT\":\"HFB09ICS0500_LIP01\",\"CURRENTRACK\":null,\"MOVE_JOBID\":null,\"MOVESTATUS\":\"ARRIVED\",\"FINALTOOLID\":null,\"MOVEFLAG\":\"0\",\"PROD_ID\":null,\"FTY_NO\":null,\"WORK_TYPE\":null,\"MFG_TYPE\":null,\"PROD_DETAIL_CODE\":\"E3A\",\"STEP_ID\":null,\"NEXT_STEP_ID\":null,\"ASSIGN_LOT_QTY\":\"0\",\"FRMT_BATCH_ID\":null,\"CARR_SIZE_TYPE\":\"STACK3\",\"ABNM_VALUE\":\"0\",\"LINE_ID\":\"FM0I\",\"TIMESTAMP\":\"2025-02-02T11:48:55Z\",\"INPUT_QTY\":null,\"GOOD_QTY\":\"0\",\"BAD_QTY\":null,\"KOR_STEP_GROUP_NAME\":null,\"ENG_STEP_GROUP_NAME\":null,\"NEXT_KOR_STEP_GROUP_NAME\":null,\"NEXT_ENG_STEP_GROUP_NAME\":null,\"LOT_ID\":null,\"CTH_REEL_ID\":null,\"ANODE_REEL_ID\":null,\"CARR_NO\":null,\"BATCH_GUBUN\":null,\"PROC_IN_TIME\":null,\"IN_CARR_QTY\":null,\"LAST_TKIN_TIME\":null,\"VHCL_ID\":null,\"FIRST_FRMT_INPUT_TIME\":null,\"PRGS_STS\":null,\"JOB_ID\":null,\"FROM_PORT\":\"NULL,NULL,NULL\",\"TO_PORT\":\"NULL,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":null},{\"MAIN_CARR_ID\":\"2F00001\",\"SUB_CARR_ID\":\"2F00001,2F23054,2F02234\",\"CARR_SEQ\":\"3\",\"CARR_USE\":\"FULL\",\"CURRENTLOCATION\":\"HFF09AGN0100_0200210\",\"CURRENTPORT\":null,\"CURRENTRACK\":\"0200210\",\"MOVE_JOBID\":null,\"MOVESTATUS\":\"ARRIVED\",\"FINALTOOLID\":null,\"MOVEFLAG\":\"0\",\"PROD_ID\":\"CP7024F111A\",\"FTY_NO\":\"70B0\",\"WORK_TYPE\":\"NM\",\"MFG_TYPE\":\"PP02\",\"PROD_DETAIL_CODE\":\"E3A\",\"STEP_ID\":\"8016\",\"NEXT_STEP_ID\":\"8106\",\"ASSIGN_LOT_QTY\":\"288\",\"FRMT_BATCH_ID\":\"H1J70SA253GK21\",\"CARR_SIZE_TYPE\":\"STACK3\",\"ABNM_VALUE\":\"0\",\"LINE_ID\":\"FM0I\",\"TIMESTAMP\":\"2025-03-24T15:51:07Z\",\"INPUT_QTY\":\"288\",\"GOOD_QTY\":\"288\",\"BAD_QTY\":null,\"KOR_STEP_GROUP_NAME\":\"상온 Aging4\",\"ENG_STEP_GROUP_NAME\":\"Normal Temp Aging4\",\"NEXT_KOR_STEP_GROUP_NAME\":\"IR/OCV4\",\"NEXT_ENG_STEP_GROUP_NAME\":\"IR/OCV4\",\"LOT_ID\":\"36092531867188BKDS1B\",\"CTH_REEL_ID\":\"H16CP25306A120D\",\"ANODE_REEL_ID\":\"H16AP25221A117A\",\"CARR_NO\":\"H1J70SA251DC13-5171\",\"BATCH_GUBUN\":\"PP02\",\"PROC_IN_TIME\":\"2025-03-18T15:36:30Z\",\"IN_CARR_QTY\":\"288\",\"LAST_TKIN_TIME\":\"2025-03-24T15:51:04Z\",\"VHCL_ID\":null,\"FIRST_FRMT_INPUT_TIME\":\"2025-03-18T15:36:32Z\",\"PRGS_STS\":\"PROC\",\"JOB_ID\":\"2F00001_663_6987788937076875\",\"FROM_PORT\":\"HFF09CNV0500,HFF09CNV0500_UOP11,NULL\",\"TO_PORT\":\"HFF09AGN0100,NULL,NULL\",\"TRANSPORT_JOB_TIMESTAMP\":\"2025-03-24T15:51:08Z\"}], \"EQUIPMENT\":[{\"LINE_ID\":\"FM0I\",\"KOR_LINE_NAME\":\"헝가리 화성 #09-1\",\"ENG_LINE_NAME\":\"HT #09-1\",\"EQP_ID\":\"HFF09CDC0100\",\"KOR_EQP_NAME\":\"충방전기 #01\",\"ENG_EQP_NAME\":\"Charger #01\",\"UP_DOWN_STS\":null,\"AUTO_MODE_STS\":null,\"STATE_ID\":\"IDLE\",\"NTW_STS\":\"CONNECT\",\"RECIPE_ID\":null,\"WO_ID\":null,\"EQP_TYPE\":\"CD\",\"TIMESTAMP\":\"2025-01-31T20:18:01Z\",\"OLD_KOR_EQP_NAME\":\"H09L_충방전기_L01_01(PC01)\",\"OLD_ENG_EQP_NAME\":\"H09L_Charger_L01_01(PC01)\"},{\"LINE_ID\":\"BX0I\",\"KOR_LINE_NAME\":\"헝가리 포장 #09-1\",\"ENG_LINE_NAME\":\"HB #09-1\",\"EQP_ID\":\"HFB09ICS0100\",\"KOR_EQP_NAME\":\"특성검사 #01\",\"ENG_EQP_NAME\":\"INSPECTION #01\",\"UP_DOWN_STS\":null,\"AUTO_MODE_STS\":null,\"STATE_ID\":\"IDLE\",\"NTW_STS\":\"CONNECT\",\"RECIPE_ID\":null,\"WO_ID\":null,\"EQP_TYPE\":\"N/A\",\"TIMESTAMP\":\"2025-02-08T14:04:45Z\",\"OLD_KOR_EQP_NAME\":\"H09L_특성검사_ECS_E01\",\"OLD_ENG_EQP_NAME\":\"H09L_INSPECTION_INLINE_ECS_E01\"},{\"LINE_ID\":\"FM0I\",\"KOR_LINE_NAME\":\"헝가리 화성 #09-1\",\"ENG_LINE_NAME\":\"HT #09-1\",\"EQP_ID\":\"HFF09HVI0100\",\"KOR_EQP_NAME\":\"HV #01\",\"ENG_EQP_NAME\":\"HV #01\",\"UP_DOWN_STS\":null,\"AUTO_MODE_STS\":\"AUTO\",\"STATE_ID\":\"DOWN\",\"NTW_STS\":\"CONNECT\",\"RECIPE_ID\":\"H1IBBBX22052301M\",\"WO_ID\":\"H1IBBBX22052301M\",\"EQP_TYPE\":null,\"TIMESTAMP\":\"2025-03-13T00:07:56Z\",\"OLD_KOR_EQP_NAME\":\"H09L_HV_E01_#01\",\"OLD_ENG_EQP_NAME\":\"H09L_HV_E01_#01\"}], \"ALARM\":[{\"ID\":\"1c7dd047-6f7d-4551-869c-bbe4fee6d4fe\",\"ALARM_TYPE\":\"logistics\",\"LEVEL\":\"HEAVY\",\"LOGISTIC\":\"1\",\"STATE\":\"SET\",\"MESSAGE\":\"Load PIO T1 Timeout (60s)\",\"CODE\":\"VHL_ALARM\",\"ICON\":\"AGV\",\"SET_TIME\":\"2025-03-25T12:00:50Z\",\"CLEAR_TIME\":null,\"TIMESTAMP\":\"2025-03-25T12:00:50Z\",\"MACHINENAME\":null,\"UNITNAME\":null,\"JOB_ID\":null,\"MAIN_CARR_ID\":null,\"FROM_EQP_ID\":null,\"FROM_UNIT_ID\":null,\"TO_EQP_ID\":null,\"TO_PORT_ID\":null,\"SHOPNAME\":\"S100\",\"UPDATE_TIME\":\"2025-03-25T12:00:50Z\",\"TRANSPORT_EQP_NAME\":null,\"TRANSPORT_UNIT_NAME\":null,\"FROM_EQP_NAME\":null,\"FROM_UNIT_NAME\":null,\"TO_EQP_NAME\":null,\"TO_UNIT_NAME\":null,\"TRANSPORT_EQP_ID\":\"HEA04ACS0100\",\"TRANSPORT_UNIT_ID\":\"HEC04RDA4069\",\"TO_UNIT_ID\":null,\"CONTINUE_TIME\":null,\"REFERENCE_ID\":\"73374eaf-2aae-4d83-9932-6104d25a758f\"},{\"ID\":\"7730f104-064e-40bc-8a3c-0cfd5471c59e\",\"ALARM_TYPE\":\"event\",\"LEVEL\":\"HEAVY\",\"LOGISTIC\":\"1\",\"STATE\":\"SET\",\"MESSAGE\":\"HFF11CDS0100_LBP05 Production Supply Logistics Delay\",\"CODE\":\"PROD_LOSS_IN\",\"ICON\":\"TR\",\"SET_TIME\":\"2025-03-25T12:00:48.699Z\",\"CLEAR_TIME\":null,\"TIMESTAMP\":\"2025-03-25T12:00:48.699Z\",\"MACHINENAME\":null,\"UNITNAME\":null,\"JOB_ID\":null,\"MAIN_CARR_ID\":null,\"FROM_EQP_ID\":null,\"FROM_UNIT_ID\":null,\"TO_EQP_ID\":null,\"TO_PORT_ID\":null,\"SHOPNAME\":\"S300\",\"UPDATE_TIME\":\"2025-03-25T12:00:48.699Z\",\"TRANSPORT_EQP_NAME\":\"충방전 Room #01\",\"TRANSPORT_UNIT_NAME\":\"충방전 AG #1호기 입고대 Track #1-4\",\"FROM_EQP_NAME\":null,\"FROM_UNIT_NAME\":null,\"TO_EQP_NAME\":null,\"TO_UNIT_NAME\":null,\"TRANSPORT_EQP_ID\":\"HFF11CDS0100\",\"TRANSPORT_UNIT_ID\":\"HFF11CDS0100_LBP05\",\"TO_UNIT_ID\":null,\"CONTINUE_TIME\":null,\"REFERENCE_ID\":null},{\"ID\":\"b20b107b-bdea-4b41-be6e-7f147b8fb6fa\",\"ALARM_TYPE\":\"event\",\"LEVEL\":\"HEAVY\",\"LOGISTIC\":\"1\",\"STATE\":\"SET\",\"MESSAGE\":\"HFF11CDS0100_LBP04 Production Supply Logistics Delay\",\"CODE\":\"PROD_LOSS_IN\",\"ICON\":\"TR\",\"SET_TIME\":\"2025-03-25T12:00:44.686Z\",\"CLEAR_TIME\":null,\"TIMESTAMP\":\"2025-03-25T12:00:44.686Z\",\"MACHINENAME\":null,\"UNITNAME\":null,\"JOB_ID\":null,\"MAIN_CARR_ID\":null,\"FROM_EQP_ID\":null,\"FROM_UNIT_ID\":null,\"TO_EQP_ID\":null,\"TO_PORT_ID\":null,\"SHOPNAME\":\"S300\",\"UPDATE_TIME\":\"2025-03-25T12:00:44.686Z\",\"TRANSPORT_EQP_NAME\":\"충방전 Room #01\",\"TRANSPORT_UNIT_NAME\":\"충방전 AG #1호기 입고대 Track #1-3(Unstack Position Down)\",\"FROM_EQP_NAME\":null,\"FROM_UNIT_NAME\":null,\"TO_EQP_NAME\":null,\"TO_UNIT_NAME\":null,\"TRANSPORT_EQP_ID\":\"HFF11CDS0100\",\"TRANSPORT_UNIT_ID\":\"HFF11CDS0100_LBP04\",\"TO_UNIT_ID\":null,\"CONTINUE_TIME\":null,\"REFERENCE_ID\":null}], \"SIMULATION_RANK\":[{\"type\":\"warehouseUtilization\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"#11,12 상온에이징 3호기\",\"targetCode\":\"HFF11AGN0300\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":99.52,\"predictValueUnit\":\"percent\",\"resultValue\":99.52,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 상온에이징 4호기\",\"targetCode\":\"HFF11AGN0400\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":98.652,\"predictValueUnit\":\"percent\",\"resultValue\":98.652,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":93.93,\"predictValueUnit\":\"percent\",\"resultValue\":93.93,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"#11,12 상온에이징 3호기\",\"targetCode\":\"HFF11AGN0300\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":99.52,\"predictValueUnit\":\"percent\",\"resultValue\":99.52,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 상온에이징 4호기\",\"targetCode\":\"HFF11AGN0400\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":98.652,\"predictValueUnit\":\"percent\",\"resultValue\":98.652,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":94.82,\"predictValueUnit\":\"percent\",\"resultValue\":94.82,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"#11,12 상온에이징 3호기\",\"targetCode\":\"HFF11AGN0300\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":99.52,\"predictValueUnit\":\"percent\",\"resultValue\":99.52,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 상온에이징 4호기\",\"targetCode\":\"HFF11AGN0400\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":98.652,\"predictValueUnit\":\"percent\",\"resultValue\":98.652,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 고온에이징\",\"targetCode\":\"HFF11AGH0100\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":93.842,\"predictValueUnit\":\"percent\",\"resultValue\":93.842,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"#11,12 상온에이징 3호기\",\"targetCode\":\"HFF11AGN0300\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":99.64,\"predictValueUnit\":\"percent\",\"resultValue\":99.64,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#11,12 상온에이징 4호기\",\"targetCode\":\"HFF11AGN0400\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":98.652,\"predictValueUnit\":\"percent\",\"resultValue\":98.652,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":99,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":96.32,\"predictValueUnit\":\"percent\",\"resultValue\":96.32,\"resultValueUnit\":\"percent\"}]}]},{\"type\":\"agvBottleneck\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"G층 후면\",\"targetCode\":\"GF_SOUTH\",\"pivot\":37.1,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":76.494,\"predictValueUnit\":\"minute\",\"resultValue\":76,\"resultValueUnit\":\"minute\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":10,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":23.681,\"predictValueUnit\":\"minute\",\"resultValue\":23,\"resultValueUnit\":\"minute\"},{\"targetName\":\"1층 전면\",\"targetCode\":\"1F_NORTH\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":21.783,\"predictValueUnit\":\"minute\",\"resultValue\":21,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":182.06,\"predictValueUnit\":\"minute\",\"resultValue\":182,\"resultValueUnit\":\"minute\"},{\"targetName\":\"G층 후면\",\"targetCode\":\"GF_SOUTH\",\"pivot\":37.1,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":53.191,\"predictValueUnit\":\"minute\",\"resultValue\":53,\"resultValueUnit\":\"minute\"},{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"TAPING\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":43.87,\"predictValueUnit\":\"minute\",\"resultValue\":43,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":140.345,\"predictValueUnit\":\"minute\",\"resultValue\":140,\"resultValueUnit\":\"minute\"},{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"TAPING\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":110.772,\"predictValueUnit\":\"minute\",\"resultValue\":110,\"resultValueUnit\":\"minute\"},{\"targetName\":\"G층 후면\",\"targetCode\":\"GF_SOUTH\",\"pivot\":37.1,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":51.445,\"predictValueUnit\":\"minute\",\"resultValue\":51,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":10,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":1380.243,\"predictValueUnit\":\"minute\",\"resultValue\":1380,\"resultValueUnit\":\"minute\"},{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":333.995,\"predictValueUnit\":\"minute\",\"resultValue\":333,\"resultValueUnit\":\"minute\"},{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"TAPING\",\"pivot\":200,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":130.852,\"predictValueUnit\":\"minute\",\"resultValue\":130,\"resultValueUnit\":\"minute\"}]}]},{\"type\":\"logisticsDelay\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"IROCV → 상온에이징\",\"targetCode\":\"IRV_AGN\",\"pivot\":38,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":822.254,\"predictValueUnit\":\"minute\",\"resultValue\":13,\"resultValueUnit\":\"minute\"},{\"targetName\":\"냉각에이징 → 상온에이징\",\"targetCode\":\"AGC_AGN\",\"pivot\":25,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":755.885,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"},{\"targetName\":\"조립 → 함침에이징\",\"targetCode\":\"ASS_AGN\",\"pivot\":20,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":737,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"IROCV → 상온에이징\",\"targetCode\":\"IRV_AGN\",\"pivot\":38,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":822.254,\"predictValueUnit\":\"minute\",\"resultValue\":13,\"resultValueUnit\":\"minute\"},{\"targetName\":\"냉각에이징 → 상온에이징\",\"targetCode\":\"AGC_AGN\",\"pivot\":25,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":755.885,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"},{\"targetName\":\"조립 → 함침에이징\",\"targetCode\":\"ASS_AGN\",\"pivot\":20,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":686.204,\"predictValueUnit\":\"minute\",\"resultValue\":11,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"조립 → 함침에이징\",\"targetCode\":\"ASS_AGN\",\"pivot\":20,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":997.049,\"predictValueUnit\":\"minute\",\"resultValue\":16,\"resultValueUnit\":\"minute\"},{\"targetName\":\"IROCV → 상온에이징\",\"targetCode\":\"IRV_AGN\",\"pivot\":38,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":822.91,\"predictValueUnit\":\"minute\",\"resultValue\":13,\"resultValueUnit\":\"minute\"},{\"targetName\":\"상온에이징 → IROCV\",\"targetCode\":\"AGN_IRV\",\"pivot\":112,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":765.716,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"IROCV → 상온에이징\",\"targetCode\":\"IRV_AGN\",\"pivot\":38,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":980.425,\"predictValueUnit\":\"minute\",\"resultValue\":16,\"resultValueUnit\":\"minute\"},{\"targetName\":\"상온에이징 → IROCV\",\"targetCode\":\"AGN_IRV\",\"pivot\":112,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":955.849,\"predictValueUnit\":\"minute\",\"resultValue\":15,\"resultValueUnit\":\"minute\"},{\"targetName\":\"냉각에이징 → 상온에이징\",\"targetCode\":\"AGC_AGN\",\"pivot\":25,\"pivotUnit\":\"minute\",\"maxPivot\":1000,\"predictValue\":755.286,\"predictValueUnit\":\"minute\",\"resultValue\":12,\"resultValueUnit\":\"minute\"}]}]},{\"type\":\"agvUtilization\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":38.691,\"predictValueUnit\":\"percent\",\"resultValue\":38.691,\"resultValueUnit\":\"percent\"},{\"targetName\":\"조립 출고\",\"targetCode\":\"ASSYATC\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":35.974,\"predictValueUnit\":\"percent\",\"resultValue\":35.974,\"resultValueUnit\":\"percent\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":32.385,\"predictValueUnit\":\"percent\",\"resultValue\":32.385,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"1F_TAPING\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":50.408,\"predictValueUnit\":\"percent\",\"resultValue\":50.408,\"resultValueUnit\":\"percent\"},{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":37.605,\"predictValueUnit\":\"percent\",\"resultValue\":37.605,\"resultValueUnit\":\"percent\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":30.48,\"predictValueUnit\":\"percent\",\"resultValue\":30.48,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"1F_TAPING\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":71.16,\"predictValueUnit\":\"percent\",\"resultValue\":71.16,\"resultValueUnit\":\"percent\"},{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":30.43,\"predictValueUnit\":\"percent\",\"resultValue\":30.43,\"resultValueUnit\":\"percent\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":28.377,\"predictValueUnit\":\"percent\",\"resultValue\":28.377,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"테이핑 특성검사\",\"targetCode\":\"1F_TAPING\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":88.406,\"predictValueUnit\":\"percent\",\"resultValue\":88.406,\"resultValueUnit\":\"percent\"},{\"targetName\":\"1층 후면\",\"targetCode\":\"1F_SOUTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":33.697,\"predictValueUnit\":\"percent\",\"resultValue\":33.697,\"resultValueUnit\":\"percent\"},{\"targetName\":\"G층 전면\",\"targetCode\":\"GF_NORTH\",\"pivot\":85,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":32.705,\"predictValueUnit\":\"percent\",\"resultValue\":32.705,\"resultValueUnit\":\"percent\"}]}]},{\"type\":\"craneUtilization\",\"values\":[{\"timePivotName\":\"10분 뒤\",\"timePivotValue\":10,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":81.02,\"predictValueUnit\":\"percent\",\"resultValue\":81.02,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 상온에이징 4호기\",\"targetCode\":\"HFF09AGN0401\",\"pivot\":90,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":71.631,\"predictValueUnit\":\"percent\",\"resultValue\":71.631,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 2호기\",\"targetCode\":\"HFF09PCH0200\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":68.93,\"predictValueUnit\":\"percent\",\"resultValue\":68.93,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"30분 뒤\",\"timePivotValue\":30,\"timePivotUnit\":\"minute\",\"pivotValues\":[{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":80.15,\"predictValueUnit\":\"percent\",\"resultValue\":80.15,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 2호기\",\"targetCode\":\"HFF09PCH0200\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":72.68,\"predictValueUnit\":\"percent\",\"resultValue\":72.68,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 상온에이징 4호기\",\"targetCode\":\"HFF09AGN0401\",\"pivot\":90,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":69.07,\"predictValueUnit\":\"percent\",\"resultValue\":69.07,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"1시간 뒤\",\"timePivotValue\":1,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":81.22,\"predictValueUnit\":\"percent\",\"resultValue\":81.22,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 2호기\",\"targetCode\":\"HFF09PCH0200\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":69.72,\"predictValueUnit\":\"percent\",\"resultValue\":69.72,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 상온에이징 4호기\",\"targetCode\":\"HFF09AGN0401\",\"pivot\":90,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":65.222,\"predictValueUnit\":\"percent\",\"resultValue\":65.222,\"resultValueUnit\":\"percent\"}]},{\"timePivotName\":\"2시간 뒤\",\"timePivotValue\":2,\"timePivotUnit\":\"hour\",\"pivotValues\":[{\"targetName\":\"#9,10 HVC 1호기\",\"targetCode\":\"HFF09PCH0100\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":81.32,\"predictValueUnit\":\"percent\",\"resultValue\":81.32,\"resultValueUnit\":\"percent\"},{\"targetName\":\"#9,10 HVC 2호기\",\"targetCode\":\"HFF09PCH0200\",\"pivot\":95,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":70.35,\"predictValueUnit\":\"percent\",\"resultValue\":70.35,\"resultValueUnit\":\"percent\"},{\"targetName\":\"버퍼에이징\",\"targetCode\":\"HFF09AGM0101\",\"pivot\":90,\"pivotUnit\":\"percent\",\"maxPivot\":1000,\"predictValue\":67.729,\"predictValueUnit\":\"percent\",\"resultValue\":67.729,\"resultValueUnit\":\"percent\"}]}]}]}"; + } +} diff --git a/Assets/Scripts/UVC/Tests/MockHttpRequester.cs.meta b/Assets/Scripts/UVC/Tests/MockHttpRequester.cs.meta new file mode 100644 index 00000000..47d3f82f --- /dev/null +++ b/Assets/Scripts/UVC/Tests/MockHttpRequester.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0826ba2b5da8fad48aff9e3542115630 \ No newline at end of file