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