pipeline 스레드 적용
This commit is contained in:
@@ -133,142 +133,6 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 2480dc463c63bf945a9488183ffe66d0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1 &695708929815469789
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7457226219413938759}
|
||||
- component: {fileID: 4416386235926923171}
|
||||
- component: {fileID: 7184852704645535964}
|
||||
m_Layer: 5
|
||||
m_Name: ValueText
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7457226219413938759
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 695708929815469789}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4802890858156259540}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 45, y: -7.5}
|
||||
m_SizeDelta: {x: -100, y: -25}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4416386235926923171
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 695708929815469789}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &7184852704645535964
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 695708929815469789}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: New Text
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: 08cebd004d97ca742ac80400f37f4eed, type: 2}
|
||||
m_sharedMaterial: {fileID: 4860575619018115804, guid: 08cebd004d97ca742ac80400f37f4eed, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4278190080
|
||||
m_fontColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 10
|
||||
m_fontSizeBase: 10
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 1
|
||||
m_VerticalAlignment: 256
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 4
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &1267781917027001920
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -453,7 +317,6 @@ RectTransform:
|
||||
- {fileID: 2993317773174661490}
|
||||
- {fileID: 6308980257678838100}
|
||||
- {fileID: 6685372230643413407}
|
||||
- {fileID: 7457226219413938759}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
@@ -481,10 +344,10 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 620e52b408949c340adef1110323cb7c, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
labelText: {fileID: 4202672738064507978}
|
||||
valueText: {fileID: 7184852704645535964}
|
||||
text: {fileID: 4202672738064507978}
|
||||
closeButton: {fileID: 5925304901667948221}
|
||||
screenOffset: {x: 0, y: 0}
|
||||
screenOffset: {x: 10, y: 10}
|
||||
menuBarHeight: 70
|
||||
--- !u!1 &7346391167643616437
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -497,7 +360,7 @@ GameObject:
|
||||
- component: {fileID: 9166711732528497215}
|
||||
- component: {fileID: 4202672738064507978}
|
||||
m_Layer: 5
|
||||
m_Name: 'LabelText '
|
||||
m_Name: 'text '
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
@@ -519,8 +382,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: -52.5, y: -7.5}
|
||||
m_SizeDelta: {x: -115, y: -25}
|
||||
m_AnchoredPosition: {x: 0, y: -5}
|
||||
m_SizeDelta: {x: -20, y: -30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &9166711732528497215
|
||||
CanvasRenderer:
|
||||
|
||||
@@ -438,6 +438,7 @@ MonoBehaviour:
|
||||
panSpeed: 20
|
||||
rotationSpeed: 300
|
||||
zoomSpeed: 10
|
||||
maxPanDelta: 50
|
||||
--- !u!1 &410087039
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1137,6 +1138,10 @@ PrefabInstance:
|
||||
propertyPath: m_Name
|
||||
value: Toolbox
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8263263242933281924, guid: 43766caa723360d4c97983845e6749fe, type: 3}
|
||||
propertyPath: m_Color.a
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 8849628700159893901, guid: 43766caa723360d4c97983845e6749fe, type: 3}
|
||||
propertyPath: m_Pivot.x
|
||||
value: 0
|
||||
@@ -1845,6 +1850,10 @@ PrefabInstance:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 2030316712}
|
||||
m_Modifications:
|
||||
- target: {fileID: 3914245719037572856, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: menuBarHeight
|
||||
value: 70
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3914245719037572856, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: worldOffset.x
|
||||
value: 60
|
||||
@@ -1855,35 +1864,35 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3914245719037572856, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: screenOffset.x
|
||||
value: 0
|
||||
value: 10
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3914245719037572856, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: screenOffset.y
|
||||
value: 0
|
||||
value: 10
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4802890858156259540, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: m_Pivot.x
|
||||
value: 0.5
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4802890858156259540, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: m_Pivot.y
|
||||
value: 0.5
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4802890858156259540, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: m_AnchorMax.x
|
||||
value: 0.5
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4802890858156259540, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 0.5
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4802890858156259540, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: m_AnchorMin.x
|
||||
value: 0.5
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4802890858156259540, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: m_AnchorMin.y
|
||||
value: 0.5
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4802890858156259540, guid: 036e56b4c097fc8409ffced10ff53562, type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using UnityEngine;
|
||||
using UVC.Core;
|
||||
using UVC.Data;
|
||||
using UVC.Tests;
|
||||
|
||||
namespace SampleProject
|
||||
{
|
||||
|
||||
@@ -314,6 +314,17 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 문자열을 배정밀도 부동 소수점 숫자로 변환합니다.
|
||||
/// </summary>
|
||||
/// <param name="v">변환할 숫자의 문자열 표현입니다.</param>
|
||||
/// <returns>입력 문자열에서 구문 분석된 배정밀도 부동 소수점 숫자 또는 변환이 실패하면 <c>0.0</c>을 반환합니다.
|
||||
///</returns>
|
||||
public double GetLong(string v)
|
||||
{
|
||||
return GetDouble(v, 0.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 DateTime으로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
@@ -511,7 +522,6 @@ namespace UVC.Data
|
||||
{
|
||||
return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -78,7 +79,7 @@ namespace UVC.Data
|
||||
obj.UpdateDifferent(dataObject);
|
||||
var newDataObject = dataObject;
|
||||
if (updatedDataOnly) newDataObject = obj.GetUpdatedObject();
|
||||
NotifyDataUpdate(key, newDataObject);
|
||||
UniTask.Post(() => NotifyDataUpdate(key, newDataObject));
|
||||
return newDataObject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 유효성 검사 규칙의 목록입니다.
|
||||
/// 각 사전은 속성 이름(string)과 해당 속성의 검증 함수(Func<object, bool>)를 포함합니다.
|
||||
/// 각 사전은 속성 이름(string)과 해당 속성의 검증 함수(Func<object, bool>)를 포함합니다.
|
||||
/// </summary>
|
||||
private List<Dictionary<string, Func<object, bool>>> validaters = new List<Dictionary<string, Func<object, bool>>>();
|
||||
private List<Dictionary<string, Func<object?, bool>>> validaters = new List<Dictionary<string, Func<object?, bool>>>();
|
||||
|
||||
/// <summary>
|
||||
/// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다.
|
||||
@@ -91,10 +91,10 @@ namespace UVC.Data
|
||||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void AddValidator(string propertyName, Func<object, bool> validator)
|
||||
public void AddValidator(string propertyName, Func<object?, bool> validator)
|
||||
{
|
||||
// 유효성 검사기를 추가합니다.
|
||||
validaters.Add(new Dictionary<string, Func<object, bool>> { { propertyName, validator } });
|
||||
validaters.Add(new Dictionary<string, Func<object?, bool>> { { propertyName, validator } });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -207,7 +207,7 @@ namespace UVC.Data
|
||||
continue; // 속성이 없으면 검증을 건너뜀
|
||||
}
|
||||
// 속성 값에 대한 검증을 수행
|
||||
if (!kvp.Value(data[kvp.Key]))
|
||||
if (!kvp.Value(ConvertJTokenToObject(data[kvp.Key])))
|
||||
{
|
||||
// 검증에 실패하면 false를 반환
|
||||
return false;
|
||||
@@ -354,5 +354,32 @@ namespace UVC.Data
|
||||
return GetValidData(sourceArray);
|
||||
}
|
||||
}
|
||||
|
||||
private object? ConvertJTokenToObject(JToken? token)
|
||||
{
|
||||
if(token == null || token.Type == JTokenType.Null)
|
||||
{
|
||||
return null; // null 처리
|
||||
}
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.String:
|
||||
return token.ToString();
|
||||
case JTokenType.Integer:
|
||||
return token.ToObject<int>();
|
||||
case JTokenType.Float:
|
||||
return token.ToObject<float>();
|
||||
case JTokenType.Boolean:
|
||||
return token.ToObject<bool>();
|
||||
//case JTokenType.Object:
|
||||
// return new DataObject((JObject)token);
|
||||
//case JTokenType.Array:
|
||||
// JArray array = (JArray)token;
|
||||
// return new DataArray(array);
|
||||
default:
|
||||
return token.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace UVC.Data
|
||||
/// 모든 HTTP 요청 처리는 백그라운드 스레드에서 수행되며, 핸들러만 메인 스레드에서 호출됩니다.
|
||||
/// </remarks>
|
||||
/// <exception cref="KeyNotFoundException">지정된 키가 등록되어 있지 않은 경우</exception>
|
||||
public async UniTask Excute(string key, bool switchToMainThread = false)
|
||||
public async UniTask Excute(string key)
|
||||
{
|
||||
if (!infoList.ContainsKey(key))
|
||||
{
|
||||
@@ -179,6 +179,7 @@ namespace UVC.Data
|
||||
{
|
||||
// 단일 실행 로직 호출
|
||||
await ExecuteSingle(key, info);
|
||||
await UniTask.SwitchToMainThread();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -187,15 +188,11 @@ namespace UVC.Data
|
||||
StartRepeat(key).Forget();
|
||||
}
|
||||
}
|
||||
finally
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ExecuteSingle 또는 StartRepeat.Forget() 후 메인 스레드로 복귀하는 것은 선택 사항
|
||||
// 여기서는 원래 실행 컨텍스트로 돌아가기 위해 메인 스레드로 전환
|
||||
// 만약 계속해서 백그라운드 스레드에서 실행하려면 이 코드를 제거할 수 있음
|
||||
if (switchToMainThread)
|
||||
{
|
||||
await UniTask.SwitchToMainThread();
|
||||
}
|
||||
// 예외가 발생한 경우에도 메인 스레드로 복귀
|
||||
await UniTask.SwitchToMainThread();
|
||||
throw; // 예외 재발생
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,13 +233,13 @@ namespace UVC.Data
|
||||
try
|
||||
{
|
||||
string result = string.Empty;
|
||||
if (!UseMockup)
|
||||
if (UseMockup)
|
||||
{
|
||||
result = await HttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
|
||||
result = await MockHttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await MockHttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
|
||||
result = await HttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
|
||||
}
|
||||
|
||||
// 응답 처리 전에 다시 취소 요청 확인
|
||||
@@ -267,10 +264,8 @@ namespace UVC.Data
|
||||
string errorMessage = responseResult.Message!;
|
||||
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke(errorMessage);
|
||||
});
|
||||
await UniTask.SwitchToMainThread();
|
||||
info.FailHandler.Invoke(errorMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -293,10 +288,8 @@ namespace UVC.Data
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
});
|
||||
await UniTask.SwitchToMainThread();
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -310,10 +303,8 @@ namespace UVC.Data
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
});
|
||||
await UniTask.SwitchToMainThread();
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -350,10 +341,8 @@ namespace UVC.Data
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
});
|
||||
await UniTask.SwitchToMainThread();
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -368,10 +357,8 @@ namespace UVC.Data
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
});
|
||||
await UniTask.SwitchToMainThread();
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -441,15 +428,14 @@ namespace UVC.Data
|
||||
// 로컬 변수로 복사하여 클로저에서 안전하게 사용
|
||||
var handlerData = dataObject;
|
||||
// UI 스레드에서 성공 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.SuccessHandler.Invoke(handlerData);
|
||||
});
|
||||
await UniTask.SwitchToMainThread();
|
||||
info.SuccessHandler.Invoke(handlerData);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (dataObject != null)
|
||||
{
|
||||
if (info.SuccessHandler != null)
|
||||
@@ -457,10 +443,8 @@ namespace UVC.Data
|
||||
// 로컬 변수로 복사하여 클로저에서 안전하게 사용
|
||||
var handlerData = dataObject;
|
||||
// UI 스레드에서 성공 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.SuccessHandler.Invoke(handlerData);
|
||||
});
|
||||
await UniTask.SwitchToMainThread();
|
||||
info.SuccessHandler.Invoke(handlerData);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -468,10 +452,8 @@ namespace UVC.Data
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is Null");
|
||||
});
|
||||
await UniTask.SwitchToMainThread();
|
||||
info.FailHandler.Invoke("Data is Null");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +513,6 @@ namespace UVC.Data
|
||||
if (!infoList.ContainsKey(key))
|
||||
{
|
||||
throw new KeyNotFoundException($"No HTTP request found with key '{key}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
HttpPipeLineInfo info = infoList[key];
|
||||
@@ -619,7 +600,7 @@ namespace UVC.Data
|
||||
/// </remarks>
|
||||
public async UniTask StopRepeat(string key)
|
||||
{
|
||||
CancellationTokenSource cts = null;
|
||||
CancellationTokenSource? cts = null;
|
||||
lock (repeatTokenSources) // 스레드 안전성 확보
|
||||
{
|
||||
if (repeatTokenSources.TryGetValue(key, out cts) && !cts.IsCancellationRequested)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#nullable enable
|
||||
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SampleProject.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UVC.Log;
|
||||
using UVC.network;
|
||||
using UVC.Tests;
|
||||
@@ -189,69 +191,74 @@ namespace UVC.Data
|
||||
/// 이 메서드는 수신된 메시지의 형식(JSON 객체 또는 배열)에 따라 적절한 파싱을 수행하고,
|
||||
/// 등록된 데이터 매퍼를 통해 메시지를 변환한 후, 해당 토픽에 등록된 핸들러에게 전달합니다.
|
||||
/// 'UpdatedDataOnly' 설정에 따라 데이터가 변경된 경우에만 핸들러를 호출할 수도 있습니다.
|
||||
/// 메시지 처리는 백그라운드 스레드에서 수행되며, 핸들러는 메인 스레드에서 호출됩니다.
|
||||
/// </remarks>
|
||||
private void OnTopicMessage(string topic, string message)
|
||||
{
|
||||
if (infoList.ContainsKey(topic))
|
||||
// 메시지 처리를 백그라운드 스레드에서 실행하여 메인 스레드 부하를 줄입니다.
|
||||
UniTask.RunOnThreadPool(() =>
|
||||
{
|
||||
MQTTPipeLineInfo info = infoList[topic];
|
||||
IDataObject? dataObject = null;
|
||||
|
||||
message = message.Trim();
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
if (infoList.ContainsKey(topic))
|
||||
{
|
||||
try
|
||||
MQTTPipeLineInfo info = infoList[topic];
|
||||
IDataObject? dataObject = null;
|
||||
|
||||
message = message.Trim();
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
if (message.StartsWith("{"))
|
||||
try
|
||||
{
|
||||
JObject source = JObject.Parse(message);
|
||||
if (info.Validator != null && !info.Validator.IsValid(source))
|
||||
if (message.StartsWith("{"))
|
||||
{
|
||||
return; // 유효성 검사 실패 시 핸들러 호출을 중단
|
||||
}
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Map(source);
|
||||
}
|
||||
else if (message.StartsWith("["))
|
||||
{
|
||||
JArray source = JArray.Parse(message);
|
||||
if (info.Validator != null)
|
||||
{
|
||||
JArray? validSource = info.Validator.GetValidData(source);
|
||||
if (validSource != null && validSource.Count > 0)
|
||||
{
|
||||
// 유효한 데이터가 있는 경우에만 매핑
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Map(validSource);
|
||||
}
|
||||
else
|
||||
JObject source = JObject.Parse(message);
|
||||
if (info.Validator != null && !info.Validator.IsValid(source))
|
||||
{
|
||||
return; // 유효성 검사 실패 시 핸들러 호출을 중단
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Map(source);
|
||||
}
|
||||
}
|
||||
else if (message.StartsWith("["))
|
||||
{
|
||||
JArray source = JArray.Parse(message);
|
||||
if (info.Validator != null)
|
||||
{
|
||||
JArray? validSource = info.Validator.GetValidData(source);
|
||||
if (validSource != null && validSource.Count > 0)
|
||||
{
|
||||
// 유효한 데이터가 있는 경우에만 매핑
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Map(validSource);
|
||||
}
|
||||
else
|
||||
{
|
||||
return; // 유효성 검사 실패 시 핸들러 호출을 중단
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Map(source);
|
||||
}
|
||||
}
|
||||
|
||||
if (dataObject != null) dataObject = DataRepository.Instance.AddData(topic, dataObject, info.UpdatedDataOnly);
|
||||
if (dataObject != null) dataObject = DataRepository.Instance.AddData(topic, dataObject, info.UpdatedDataOnly);
|
||||
|
||||
// 갱신 된 데이터가 있는 경우 핸들러 호출
|
||||
if (info.UpdatedDataOnly)
|
||||
{
|
||||
if (dataObject != null && dataObject.UpdatedCount > 0) info.Handler?.Invoke(dataObject);
|
||||
// 핸들러 호출이 필요한지 확인
|
||||
bool shouldInvoke = !info.UpdatedDataOnly || (dataObject != null && dataObject.UpdatedCount > 0);
|
||||
|
||||
if (shouldInvoke)
|
||||
{
|
||||
// 핸들러를 메인 스레드에서 안전하게 호출
|
||||
UniTask.Post(() => info.Handler?.Invoke(dataObject));
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
info.Handler?.Invoke(dataObject);
|
||||
// 예외 발생 시 로깅 또는 처리
|
||||
// 예외 로깅도 메인 스레드에서 처리하여 Unity API 호출에 대한 스레드 안정성 확보
|
||||
UniTask.Post(() => ULog.Error($"Error processing message for topic '{topic}': {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 예외 발생 시 로깅 또는 처리
|
||||
ULog.Error($"Error processing message for topic '{topic}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UVC.Data;
|
||||
|
||||
namespace UVC.Factory.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 씬에 표시되는 개별 AGV(무인 운반차)를 제어하는 클래스입니다.
|
||||
/// FactoryObject를 상속받아, AGV의 데이터 처리, 3D 모델의 이동 및 회전, 정보 표시 기능을 구현합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 AGVManager에 의해 동적으로 생성되고 관리됩니다.
|
||||
/// AGVManager로부터 실시간 데이터를 받아 ProcessData 메서드에서 처리하고,
|
||||
/// Unity의 Update 메서드에서 매 프레임마다 부드러운 시각적 이동을 구현합니다.
|
||||
/// </remarks>
|
||||
public class AGV : FactoryObject
|
||||
{
|
||||
// 서버에서 받은 좌표(예: 밀리미터 단위)를 Unity 씬의 단위(미터)로 변환하기 위한 스케일 값입니다.
|
||||
// 예를 들어, 서버 좌표 1000이 Unity에서 1미터가 되려면 0.001f로 설정합니다.
|
||||
private float scaleFactor = 0.0005f; // Unity에서 사용하는 단위로 변환하기 위한 스케일 팩터
|
||||
|
||||
// 목표 위치와 회전 값
|
||||
// 데이터로부터 수신한 AGV의 목표 위치와 목표 회전값입니다.
|
||||
// AGV는 현재 위치에서 이 목표 지점을 향해 부드럽게 움직입니다.
|
||||
private Vector3 targetPosition;
|
||||
private Quaternion targetRotation;
|
||||
|
||||
// 움직임과 회전의 부드러움을 조절할 속도 변수
|
||||
// Unity 인스펙터 창에서 실시간으로 값을 조절하며 테스트할 수 있습니다.
|
||||
// 움직임과 회전의 부드러움을 조절할 속도 변수입니다.
|
||||
// Unity 인스펙터 창에서 실시간으로 값을 조절하며 최적의 움직임을 찾을 수 있습니다.
|
||||
[Tooltip("목표 지점까지의 이동 속도를 조절합니다.")]
|
||||
public float moveSpeed = 1.0f;
|
||||
|
||||
@@ -25,11 +35,16 @@ namespace UVC.Factory.Component
|
||||
[Tooltip("이 거리(미터)를 초과하면 보간 없이 즉시 위치를 변경합니다.")]
|
||||
public float teleportDistanceThreshold = 5.0f; // 5미터 이상 차이나면 순간이동
|
||||
|
||||
/// <summary>
|
||||
/// AGV 객체가 생성될 때 처음 한 번 호출되는 초기화 메서드입니다.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
// 시작 시에는 현재 위치를 목표 위치로 설정하여 의도치 않은 움직임을 방지합니다.
|
||||
targetPosition = transform.position;
|
||||
targetRotation = transform.rotation;
|
||||
|
||||
// 사용자가 AGV를 클릭했을 때 정보창에 표시될 데이터 항목과 순서를 정의합니다.
|
||||
DataOrderedMask = new List<string>
|
||||
{
|
||||
"VHL_NAME",
|
||||
@@ -43,26 +58,32 @@ namespace UVC.Factory.Component
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 내부 상태를 업데이트하고 정렬된 마스크에 정의된 특정 키에 대한 작업을 수행하여 제공된 데이터 객체를 처리합니다.
|
||||
// </summary>
|
||||
/// <remarks>이 메서드는 초기화되지 않은 경우 내부 데이터 상태를 업데이트하고, 제공된
|
||||
/// <see cref="DataOrderedMask"/>에 정의된 키를 반복하여 제공된
|
||||
/// <paramref name="newData"/> 객체의 일치하는 항목에 대한 작업을 수행합니다. <paramref name="newData"/>에 처리에 필요한 키가 포함되어 있는지
|
||||
/// 확인합니다.</remarks>
|
||||
/// <param name="newData">처리할 데이터 객체입니다. null이 아니어야 하며 정렬된 마스크와 관련된 키-값 쌍을 포함해야 합니다.
|
||||
/// AGVManager로부터 새로운 데이터를 받았을 때 호출되는 핵심 메서드입니다.
|
||||
/// 받은 데이터를 기반으로 AGV의 내부 상태와 목표 위치를 갱신합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 FactoryObject의 추상 메서드를 재정의한 것입니다.
|
||||
/// AGV의 위치, 회전, 상태 등 모든 동적인 정보는 이 메서드를 통해 업데이트됩니다.
|
||||
/// </remarks>
|
||||
/// <param name="newData">AGV의 최신 정보가 담긴 데이터 객체입니다.</param>
|
||||
protected override void ProcessData(DataObject newData)
|
||||
{
|
||||
// 처음 데이터를 받는 경우 (data가 null일 때)
|
||||
if (data == null)
|
||||
{
|
||||
// 새 데이터로 위치와 회전을 즉시 설정합니다.
|
||||
UpdatePositionAndRotation(newData);
|
||||
// 받은 데이터를 내부 데이터 저장소에 저장합니다.
|
||||
data = newData;
|
||||
// 시작 시에는 현재 위치를 목표 위치로 설정하여 움직이지 않도록 합니다.
|
||||
targetPosition = transform.position;
|
||||
targetRotation = transform.rotation;
|
||||
}
|
||||
else
|
||||
else // 이미 데이터가 있는 경우 (업데이트)
|
||||
{
|
||||
// 새 데이터를 기반으로 목표 위치와 회전을 갱신합니다.
|
||||
UpdatePositionAndRotation(newData);
|
||||
// 기존 데이터(data)에 새로운 데이터(newData)의 내용을 덮어씁니다.
|
||||
foreach (var keyValue in newData)
|
||||
{
|
||||
if (data.ContainsKey(keyValue.Key))
|
||||
@@ -73,8 +94,13 @@ namespace UVC.Factory.Component
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 객체로부터 위치(X, Y) 및 각도(DEGREE) 값을 읽어와 AGV의 목표 위치와 회전을 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="newData">위치와 각도 정보가 포함된 데이터 객체입니다.</param>
|
||||
private void UpdatePositionAndRotation(DataObject newData)
|
||||
{
|
||||
// 처음 데이터를 받는 경우, 받은 데이터로 즉시 위치를 설정합니다.
|
||||
if (data == null)
|
||||
{
|
||||
float x = newData.GetFloat("X") * scaleFactor;
|
||||
@@ -83,11 +109,13 @@ namespace UVC.Factory.Component
|
||||
transform.position = new Vector3(x, 0, y);
|
||||
transform.rotation = rotation;
|
||||
}
|
||||
else
|
||||
else // 이후 업데이트의 경우
|
||||
{
|
||||
// 새 데이터로부터 목표 위치와 회전값을 계산합니다.
|
||||
float x = data.GetFloat("X") * scaleFactor;
|
||||
float y = data.GetFloat("Y") * scaleFactor;
|
||||
Quaternion rotation = Quaternion.Euler(0, data.GetFloat("DEGREE"), 0);
|
||||
|
||||
float newX = newData.GetFloat("X") * scaleFactor;
|
||||
float newY = newData.GetFloat("Y") * scaleFactor;
|
||||
Quaternion newRotation = Quaternion.Euler(0, newData.GetFloat("DEGREE"), 0);
|
||||
@@ -116,10 +144,16 @@ namespace UVC.Factory.Component
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unity에 의해 매 프레임마다 호출되는 메서드입니다.
|
||||
/// AGV의 현재 위치/회전을 목표 위치/회전으로 부드럽게 이동시키는 시각적 처리를 담당합니다.
|
||||
/// </summary>
|
||||
void Update()
|
||||
{
|
||||
// 현재 위치가 목표 위치와 다를 경우에만 이동 로직을 실행합니다.
|
||||
if (transform.position != targetPosition)
|
||||
{
|
||||
// 목표 지점과의 거리가 매우 가까우면 (0.01미터 미만) 그냥 목표 위치로 설정하여 미세한 떨림을 방지합니다.
|
||||
if (Vector3.Distance(transform.position, targetPosition) < 0.1f)
|
||||
{
|
||||
// 현재 위치와 목표 위치 사이의 거리가 임계값을 초과하면 순간이동합니다.
|
||||
@@ -127,20 +161,23 @@ namespace UVC.Factory.Component
|
||||
}
|
||||
else
|
||||
{
|
||||
// 매 프레임마다 현재 위치에서 목표 위치로 부드럽게 이동시킵니다.
|
||||
// Vector3.Lerp를 사용하여 현재 위치에서 목표 위치로 부드럽게 이동시킵니다.
|
||||
// Time.deltaTime * moveSpeed는 프레임 속도에 관계없이 일정한 속도를 보장합니다.
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * moveSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 회전이 목표 회전과 다를 경우에만 회전 로직을 실행합니다.
|
||||
if (transform.rotation != targetRotation)
|
||||
{
|
||||
// 목표 회전과의 각도 차이가 매우 작으면 (0.1도 미만) 그냥 목표 회전으로 설정합니다.
|
||||
if (Quaternion.Angle(transform.rotation, targetRotation) < 0.1f)
|
||||
{
|
||||
transform.rotation = targetRotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 매 프레임마다 현재 회전에서 목표 회전으로 부드럽게 회전시킵니다.
|
||||
// Quaternion.Slerp를 사용하여 현재 회전에서 목표 회전으로 부드럽게 회전시킵니다.
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotationSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,77 @@ using UVC.Data;
|
||||
|
||||
namespace UVC.Factory.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 씬에 존재하는 모든 AGV(Automated Guided Vehicle, 무인 운반차) 객체를 총괄 관리하는 싱글톤 클래스입니다.
|
||||
/// 외부 데이터 소스(예: MQTT)로부터 AGV의 실시간 데이터를 수신하여, 씬에 AGV를 동적으로 생성, 업데이트, 제거하는 역할을 담당합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 매니저는 `SingletonScene`을 상속받아 구현되었으므로, `AGVManager.Instance`를 통해 씬의 어디에서든 쉽게 접근할 수 있습니다.
|
||||
/// 씬이 초기화될 때(`OnSceneInitialized`) MQTT 파이프라인에 연결하여 'AGV' 토픽의 데이터를 구독하기 시작합니다.
|
||||
/// 수신된 데이터(`DataArray`)를 분석하여 새로 추가된 AGV, 제거된 AGV, 정보가 변경된 AGV를 감지하고 각각에 맞는 처리를 비동기적으로 수행합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// 이 매니저는 자동으로 작동하므로 다른 스크립트에서 직접 호출할 일은 거의 없습니다.
|
||||
/// 대신 이 매니저가 어떻게 AGV 객체를 생성하고 데이터를 전달하는지, 그리고 AGV 객체는 그 데이터를 어떻게 처리하는지를 이해하는 것이 중요합니다.
|
||||
///
|
||||
/// 1. **데이터 수신**: 서버로부터 다음과 같은 JSON 데이터 배열을 수신했다고 가정합니다.
|
||||
/// (실제로는 DataArray 객체로 변환되어 전달됩니다.)
|
||||
/// ```json
|
||||
/// [
|
||||
/// { "VHL_NAME": "AGV-01", "X": 105, "Y": 210, "DEGREE": 90.0, "VHL_STATE": "RUN" },
|
||||
/// { "VHL_NAME": "AGV-02", "X": 150, "Y": 300, "DEGREE": 180.0, "VHL_STATE": "IDLE" }
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// 2. **매니저의 동작**: `OnUpdateDataAsync` 메서드가 이 데이터를 받아 처리합니다.
|
||||
/// - `agvList`에 "AGV-01"이 없으면, `CreateAGV`를 호출하여 프리팹으로부터 새로운 AGV 게임 오브젝트를 생성합니다.
|
||||
/// - 생성된 AGV 객체의 `UpdateData` 메서드를 호출하여 위치, 각도 등의 데이터를 전달합니다.
|
||||
/// - 만약 기존에 있던 "AGV-01"의 데이터가 변경된 것이라면(ModifiedList), `agvList`에서 해당 AGV를 찾아 `UpdateData`만 호출합니다.
|
||||
///
|
||||
/// 3. **AGV 객체의 반응**: AGV.cs 에서는 `UpdateData` 메서드를 통해 받은 데이터로 자신의 상태를 갱신합니다.
|
||||
/// (AGV.cs 파일에 포함될 수 있는 예시 코드)
|
||||
/// <code>
|
||||
/// public class AGV : FactoryObject
|
||||
/// {
|
||||
/// // ... (moveSpeed, rotationSpeed 등 변수 선언)
|
||||
///
|
||||
/// protected override void ProcessData(DataObject newData)
|
||||
/// {
|
||||
/// // AGVManager로부터 전달받은 데이터에 위치(X, Y)나 각도(DEGREE) 정보가 있는지 확인
|
||||
/// if (newData.ContainsKey("X") && newData.ContainsKey("Y") && newData.ContainsKey("DEGREE"))
|
||||
/// {
|
||||
/// // 데이터로부터 목표 위치와 회전값 계산
|
||||
/// float x = newData.GetFloat("X");
|
||||
/// float y = newData.GetFloat("Y");
|
||||
/// float degree = newData.GetFloat("DEGREE");
|
||||
///
|
||||
/// // Unity 월드 좌표계에 맞게 변환 (예시: Z를 Y로 사용, 스케일 조정)
|
||||
/// Vector3 targetPosition = new Vector3(x * 0.01f, 0, y * 0.01f);
|
||||
/// Quaternion targetRotation = Quaternion.Euler(0, degree, 0);
|
||||
///
|
||||
/// // 부드러운 이동 및 회전을 위해 Lerp 또는 다른 방법을 사용하여 현재 위치/회전을 업데이트
|
||||
/// // (이 로직은 보통 Unity의 Update 메서드에서 처리됩니다)
|
||||
/// this.targetPosition = targetPosition;
|
||||
/// this.targetRotation = targetRotation;
|
||||
/// }
|
||||
///
|
||||
/// // 상태(VHL_STATE)에 따라 AGV의 색상이나 애니메이션 변경
|
||||
/// if (newData.ContainsKey("VHL_STATE"))
|
||||
/// {
|
||||
/// string status = newData.GetString("VHL_STATE");
|
||||
/// // ... (상태에 따른 시각적 처리)
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class AGVManager : SingletonScene<AGVManager>
|
||||
{
|
||||
|
||||
private readonly string prefabPath = "Prefabs/SampleProject/Factory/AGV";
|
||||
private GameObject? prefab;
|
||||
|
||||
|
||||
|
||||
private List<AGV> agvList;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// AGVManager의 초기화 메서드입니다.
|
||||
/// Awake 메서드에서 호출되며, MonoBehaviour가 생성될 때 한 번만 실행됩니다.
|
||||
@@ -32,6 +91,10 @@ namespace UVC.Factory.Component
|
||||
SceneMain.Instance.Initialized += OnSceneInitialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 씬이 완전히 초기화된 후 호출됩니다.
|
||||
/// AGV 데이터를 수신하기 위한 MQTT 파이프라인을 설정합니다.
|
||||
/// </summary>
|
||||
private void OnSceneInitialized()
|
||||
{
|
||||
//데이터를 어떤 형식으로 받을지 정의합니다.
|
||||
@@ -53,33 +116,50 @@ namespace UVC.Factory.Component
|
||||
dataMask["JOB_ID"] = "";
|
||||
dataMask["TIMESTAMP"] = DateTime.Now;
|
||||
|
||||
// MQTT 파이프라인 정보를 생성합니다.
|
||||
// 'AGV' 토픽을 구독하고, 받은 데이터는 위에서 정의한 dataMask로 매핑하며,
|
||||
// 데이터 유효성 검사를 위해 DataValidator를 설정합니다.
|
||||
// 데이터가 업데이트되면 OnUpdateData 메서드를 호출하여 처리합니다.
|
||||
|
||||
DataValidator validator = new DataValidator();
|
||||
validator.AddValidator("JOB_ID", value => value != null);
|
||||
|
||||
var pipelineInfo = new MQTTPipeLineInfo("AGV")
|
||||
.setDataMapper(new DataMapper(dataMask))
|
||||
.setHandler(OnUpdateData);
|
||||
.setValidator(validator)
|
||||
.setHandler(OnUpdateData);
|
||||
|
||||
// MQTTPipeLine에 파이프라인 정보를 추가합니다.
|
||||
// 생성한 파이프라인 정보를 전역 MQTT 파이프라인에 추가합니다.
|
||||
AppMain.Instance.MQTTPipeLine.Add(pipelineInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 수신 시 호출되는 공개 핸들러입니다.
|
||||
/// 비동기 처리 메서드인 OnUpdateDataAsync를 호출합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">수신된 데이터 객체 (일반적으로 DataArray 형태)</param>
|
||||
public void OnUpdateData(IDataObject? data)
|
||||
{
|
||||
OnUpdateDataAsync(data).Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 데이터와 비교하여 변경된 부분만 전달 됩니다.
|
||||
/// 수신된 AGV 데이터 배열을 비동기적으로 처리하여 씬에 반영합니다.
|
||||
/// 추가, 제거, 수정된 AGV 데이터를 각각 구분하여 처리합니다.
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="data">수신된 데이터 (DataArray)</param>
|
||||
public async UniTask OnUpdateDataAsync(IDataObject? data)
|
||||
{
|
||||
if (data == null) return;
|
||||
DataArray? arr = data as DataArray;
|
||||
if (arr == null || arr.Count == 0) return;
|
||||
|
||||
// 데이터 배열에서 추가, 제거, 수정된 항목 리스트를 가져옵니다.
|
||||
var AddedItems = arr.AddedItems;
|
||||
var RemovedItems = arr.RemovedItems;
|
||||
var ModifiedList = arr.ModifiedList;
|
||||
|
||||
// 새로 추가된 AGV 처리
|
||||
foreach (var item in AddedItems.ToList())
|
||||
{
|
||||
AGV? agv = await CreateAGV(item);
|
||||
@@ -90,6 +170,7 @@ namespace UVC.Factory.Component
|
||||
}
|
||||
}
|
||||
|
||||
// 제거된 AGV 처리
|
||||
foreach (var item in RemovedItems.ToList())
|
||||
{
|
||||
AGV agv = agvList.Find(x => x.Info != null && x.Info.Name == item.GetString("VHL_NAME"));
|
||||
@@ -100,6 +181,7 @@ namespace UVC.Factory.Component
|
||||
}
|
||||
}
|
||||
|
||||
// 정보가 수정된 AGV 처리
|
||||
foreach (var item in ModifiedList.ToList())
|
||||
{
|
||||
AGV agv = agvList.Find(x => x.Info != null && x.Info.Name == item.GetString("VHL_NAME"));
|
||||
@@ -111,6 +193,11 @@ namespace UVC.Factory.Component
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 프리팹을 사용하여 새로운 AGV 게임 오브젝트를 생성하고 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">신규 AGV의 정보가 담긴 DataObject</param>
|
||||
/// <returns>생성 및 초기화된 AGV 컴포넌트, 실패 시 null</returns>
|
||||
private async UniTask<AGV?> CreateAGV(DataObject data)
|
||||
{
|
||||
if (prefab == null)
|
||||
@@ -129,6 +216,7 @@ namespace UVC.Factory.Component
|
||||
return null;
|
||||
}
|
||||
var agv = prefabInstance.GetComponent<AGV>();
|
||||
// AGV의 고정 정보(Info)를 설정합니다. 이 정보는 FactoryObjectManager 등에서 사용될 수 있습니다.
|
||||
agv.Info = new FactoryObjectInfo(
|
||||
data.GetString("VHL_NAME"),
|
||||
data.GetString("NODE_ID"),
|
||||
@@ -139,6 +227,10 @@ namespace UVC.Factory.Component
|
||||
return agv;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGVManager가 파괴될 때 호출됩니다.
|
||||
/// MQTT 파이프라인에서 'AGV' 핸들러를 제거하여 메모리 누수를 방지합니다.
|
||||
/// </summary>
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
@@ -9,11 +9,115 @@ using UVC.UI.Info;
|
||||
namespace UVC.Factory.Component
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// 팩토리 내의 상호작용 가능한 3D 객체를 위한 추상 기본 클래스입니다.
|
||||
/// 이 클래스는 Unity GameObject를 데이터(DataObject)와 연결하고, 사용자 상호작용(예: 클릭)을 처리하며,
|
||||
/// 데이터 변경에 따라 객체의 상태(예: 색상, 애니메이션)를 업데이트하는 기능을 제공합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 팩토리 객체는 고유한 정보(Info)와 동적 데이터(data)를 가집니다.
|
||||
/// 사용자가 객체를 클릭하면 OnPointerClick 이벤트가 발생하여 InfoWindow에 관련 데이터를 표시할 수 있습니다.
|
||||
/// ProcessData 메서드를 재정의하여 데이터 변경 시 특정 로직을 수행하도록 구현해야 합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// 다음은 FactoryObject를 상속받아 'MachineObject'라는 구체적인 클래스를 만드는 예제입니다.
|
||||
/// 이 예제에서는 데이터로 받은 'status' 값에 따라 머신의 색상을 변경합니다.
|
||||
/// <code>
|
||||
/// using UnityEngine;
|
||||
/// using UVC.Data;
|
||||
/// using UVC.Factory.Component;
|
||||
///
|
||||
/// // FactoryObject를 상속받는 MachineObject 클래스 정의
|
||||
/// public class MachineObject : FactoryObject
|
||||
/// {
|
||||
/// private Renderer objectRenderer;
|
||||
///
|
||||
/// private void Awake()
|
||||
/// {
|
||||
/// // 색상을 변경할 렌더러 컴포넌트를 미리 찾아둡니다.
|
||||
/// objectRenderer = GetComponent<Renderer>();
|
||||
/// }
|
||||
///
|
||||
/// // ProcessData 메서드를 재정의하여 데이터 처리 로직을 구현합니다.
|
||||
/// protected override void ProcessData(DataObject newData)
|
||||
/// {
|
||||
/// // 'status' 키가 데이터에 포함되어 있는지 확인합니다.
|
||||
/// if (newData.ContainsKey("status"))
|
||||
/// {
|
||||
/// // 'status' 값을 문자열로 가져옵니다.
|
||||
/// string status = newData.GetString("status", "off");
|
||||
///
|
||||
/// // 상태 값에 따라 머티리얼의 색상을 변경합니다.
|
||||
/// switch (status)
|
||||
/// {
|
||||
/// case "running":
|
||||
/// objectRenderer.material.color = Color.green;
|
||||
/// break;
|
||||
/// case "warning":
|
||||
/// objectRenderer.material.color = Color.yellow;
|
||||
/// break;
|
||||
/// case "error":
|
||||
/// objectRenderer.material.color = Color.red;
|
||||
/// break;
|
||||
/// default:
|
||||
/// objectRenderer.material.color = Color.gray;
|
||||
/// break;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // 아래는 MachineObject를 생성하고 데이터를 업데이트하는 예시입니다.
|
||||
/// public class MachineManager : MonoBehaviour
|
||||
/// {
|
||||
/// void Start()
|
||||
/// {
|
||||
/// // 1. 게임 오브젝트를 생성하고 MachineObject 컴포넌트를 추가합니다.
|
||||
/// GameObject machineGo = new GameObject("Drilling Machine");
|
||||
/// machineGo.AddComponent<MeshFilter>(); // 렌더링을 위한 기본 컴포넌트
|
||||
/// machineGo.AddComponent<MeshRenderer>();
|
||||
/// machineGo.AddComponent<BoxCollider>(); // 클릭 이벤트를 위한 콜라이더
|
||||
/// MachineObject machine = machineGo.AddComponent<MachineObject>();
|
||||
///
|
||||
/// // 2. 객체 정보(Info)를 설정합니다.
|
||||
/// machine.Info = new FactoryObjectInfo
|
||||
/// {
|
||||
/// Id = "MC-001",
|
||||
/// Name = "Drilling Machine"
|
||||
/// };
|
||||
///
|
||||
/// // 3. 초기 데이터를 생성하고 UpdateData를 통해 전달합니다.
|
||||
/// // 이 시점에 ProcessData가 호출되어 머신 색상이 녹색으로 변경됩니다.
|
||||
/// var initialData = new DataObject
|
||||
/// {
|
||||
/// { "status", "running" },
|
||||
/// { "temperature", 85.5f },
|
||||
/// { "operator", "Admin" }
|
||||
/// };
|
||||
/// machine.UpdateData(initialData);
|
||||
///
|
||||
/// // 4. 5초 후 데이터 변경을 시뮬레이션합니다.
|
||||
/// // 상태가 'error'로 변경되면 ProcessData가 다시 호출되어 색상이 빨간색으로 바뀝니다.
|
||||
/// StartCoroutine(SimulateError(machine));
|
||||
/// }
|
||||
///
|
||||
/// private System.Collections.IEnumerator SimulateError(MachineObject machine)
|
||||
/// {
|
||||
/// yield return new WaitForSeconds(5);
|
||||
///
|
||||
/// var errorData = new DataObject { { "status", "error" } };
|
||||
/// machine.UpdateData(errorData);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public abstract class FactoryObject : InteractiveObject
|
||||
{
|
||||
protected FactoryObjectInfo? info;
|
||||
|
||||
/// <summary>
|
||||
/// 팩토리 객체의 고유한 식별 정보(ID, 이름 등)를 가져오거나 설정합니다.
|
||||
/// 이 정보는 객체를 관리하고 UI에 표시하는 데 사용됩니다.
|
||||
/// </summary>
|
||||
public FactoryObjectInfo? Info
|
||||
{
|
||||
get => info;
|
||||
@@ -33,7 +137,8 @@ namespace UVC.Factory.Component
|
||||
|
||||
protected List<string>? dataOrderedMask;
|
||||
/// <summary>
|
||||
/// InfoWindow에 표시할 데이터의 순서와 항목을 지정하는 마스크입니다.
|
||||
/// 객체 클릭 시 정보창(InfoWindow)에 표시될 데이터의 순서와 항목을 지정하는 마스크입니다.
|
||||
/// 이 리스트에 포함된 키의 데이터만 순서대로 표시됩니다. null이거나 비어있으면 모든 데이터를 표시합니다.
|
||||
/// </summary>
|
||||
public List<string>? DataOrderedMask
|
||||
{
|
||||
@@ -54,10 +159,11 @@ namespace UVC.Factory.Component
|
||||
/// <summary>
|
||||
/// 포인터 클릭 이벤트를 처리하고 관련 데이터가 포함된 정보 창을 표시합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 메서드는 정보 창이 현재 표시되어 있는지, 그리고
|
||||
/// 유효한 데이터가 있는지 확인합니다. 데이터가 마스크를 사용하여 정렬된 경우 마스크된 데이터만 표시되고, 그렇지 않은 경우
|
||||
/// 사용 가능한 모든 데이터가 표시됩니다. 정보 창은 현재
|
||||
/// 변환을 기준으로 배치됩니다.</remarks>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 `InteractiveObject`로부터 상속받아 재정의되었습니다.
|
||||
/// 객체에 유효한 데이터가 있을 경우, `InfoWindow`를 통해 사용자에게 데이터를 보여줍니다.
|
||||
/// `DataOrderedMask`가 설정되어 있으면 해당 순서대로, 그렇지 않으면 모든 데이터를 표시합니다.
|
||||
/// </remarks>
|
||||
/// <param name="eventData">포인터 클릭과 관련된 이벤트 데이터입니다.</param>
|
||||
public override void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
@@ -85,9 +191,11 @@ namespace UVC.Factory.Component
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 변경된 데이터만 업데이트합니다.
|
||||
/// 외부로부터 받은 새로운 데이터로 객체의 상태를 업데이트합니다.
|
||||
/// 이 메서드는 내부적으로 `ProcessData`를 호출하여 실제 데이터 처리 로직을 수행합니다.
|
||||
/// MQTTPipeLineInfo.updatedDataOnly가 true인 경우, 데이터가 변경된 경우에만 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="newData"></param>
|
||||
/// <param name="newData">업데이트할 새로운 데이터가 포함된 IDataObject 객체입니다.</param>
|
||||
public void UpdateData(IDataObject? newData)
|
||||
{
|
||||
if(newData == null) return;
|
||||
@@ -100,19 +208,29 @@ namespace UVC.Factory.Component
|
||||
/// 지정된 데이터 객체를 처리합니다. 이 메서드는 파생 클래스에서 재정의되어
|
||||
/// 사용자 지정 데이터 처리 로직을 구현하도록 설계되었습니다.
|
||||
/// </summary>
|
||||
/// <remarks>파생 클래스는 <paramref name="newData"/> 매개변수에 대한 특정 처리 동작을 제공하기 위해
|
||||
/// 이 메서드를 재정의해야 합니다.
|
||||
/// 사용하기 전에 매개변수의 유효성을 검사해야 합니다.</remarks>
|
||||
/// <remarks>
|
||||
/// `UpdateData`가 호출될 때 실행됩니다. 파생 클래스에서는 이 메서드를 재정의하여
|
||||
/// 데이터 값에 따라 객체의 색상, 애니메이션, 동작 등을 변경하는 코드를 작성해야 합니다.
|
||||
/// </remarks>
|
||||
/// <param name="newData">처리할 데이터 객체입니다. null일 수 없습니다.</param>
|
||||
protected virtual void ProcessData(DataObject newData) {}
|
||||
|
||||
/// <summary>
|
||||
/// 객체의 위치를 가져옵니다. 월드 좌표 또는 로컬 좌표로 반환할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <param name="world">true이면 월드 좌표, false이면 부모 기준의 로컬 좌표를 반환합니다.</param>
|
||||
/// <returns>객체의 Vector3 위치 값입니다.</returns>
|
||||
public Vector3 GetPosition(bool world = false)
|
||||
{
|
||||
if (!world) return transform.position;
|
||||
return transform.TransformPoint(transform.position);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 객체의 진입점 위치를 가져옵니다. 주로 다른 객체(예: AGV)가 이 객체로 접근할 때 목표 지점으로 사용됩니다.
|
||||
/// </summary>
|
||||
/// <param name="world">true이면 월드 좌표, false이면 부모 기준의 로컬 좌표를 반환합니다.</param>
|
||||
/// <returns>객체의 진입점 Vector3 위치 값입니다.</returns>
|
||||
public Vector3 GetEntrancePosition(bool world = false)
|
||||
{
|
||||
if (!world) return transform.position;
|
||||
|
||||
@@ -1,32 +1,84 @@
|
||||
namespace UVC.Factory.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 팩토리 내의 각 객체(FactoryObject)에 대한 고정적인 식별 정보를 저장하는 데이터 클래스입니다.
|
||||
/// 이 클래스는 객체의 이름, ID, 위치, 구역, 층과 같이 한 번 설정된 후에는 거의 변경되지 않는 정적 데이터를 담는 데 사용됩니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 정보는 FactoryObject가 생성될 때 할당되며, FactoryObjectManager를 통해 시스템 전체에서 객체를 식별하고 관리하는 데 사용됩니다.
|
||||
/// 예를 들어, 특정 ID를 가진 기계를 찾거나 특정 구역에 있는 모든 센서를 필터링하는 등의 작업에 활용될 수 있습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// 다음은 `FactoryObjectInfo`를 생성하고 `FactoryObject`에 할당하는 간단한 예제입니다.
|
||||
/// <code>
|
||||
/// using UnityEngine;
|
||||
/// using UVC.Factory.Component;
|
||||
///
|
||||
/// public class FactorySetup : MonoBehaviour
|
||||
/// {
|
||||
/// void Start()
|
||||
/// {
|
||||
/// // 1. 씬에 새로운 게임 오브젝트를 생성하고, 사용자 정의 팩토리 객체 컴포넌트(예: MachineObject)를 추가합니다.
|
||||
/// GameObject machineGo = new GameObject("CNC Machine");
|
||||
/// MachineObject machine = machineGo.AddComponent<MachineObject>(); // MachineObject는 FactoryObject를 상속받는 클래스라고 가정합니다.
|
||||
///
|
||||
/// // 2. 이 기계에 대한 식별 정보를 담는 FactoryObjectInfo 인스턴스를 생성합니다.
|
||||
/// var machineInfo = new FactoryObjectInfo(
|
||||
/// name: "CNC-Machine-05",
|
||||
/// id: "MC-005",
|
||||
/// position: "Row 3, Column 2",
|
||||
/// area: "Machining Area 1",
|
||||
/// floor: "1F"
|
||||
/// );
|
||||
///
|
||||
/// // 3. 생성된 정보 객체를 MachineObject의 Info 속성에 할당합니다.
|
||||
/// // 이 과정을 통해 3D 모델(GameObject)과 메타데이터(FactoryObjectInfo)가 연결됩니다.
|
||||
/// machine.Info = machineInfo;
|
||||
///
|
||||
/// // 이제 이 객체는 FactoryObjectManager에 등록되고, ID나 이름으로 검색할 수 있게 됩니다.
|
||||
/// Debug.Log($"새로운 기계 등록: {machine.Info.ToString()}");
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // MachineObject는 다음과 같이 FactoryObject를 상속하여 구현할 수 있습니다.
|
||||
/// public class MachineObject : FactoryObject { /* ... */ }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class FactoryObjectInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 이름
|
||||
/// 객체의 이름입니다. UI에 표시되거나 사람이 식별할 수 있는 이름으로 사용됩니다. (예: "Main Conveyor Belt")
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 아이디
|
||||
/// 객체의 고유 식별자(ID)입니다. 시스템에서 객체를 유일하게 구분하는 데 사용됩니다. (예: "CVB-001")
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 위치
|
||||
/// 팩토리 내에서 객체의 물리적인 위치나 좌표를 설명합니다. (예: "A-12")
|
||||
/// </summary>
|
||||
public string Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 구역
|
||||
/// 객체가 속한 구역이나 공정을 나타냅니다. (예: "조립 라인 1")
|
||||
/// </summary>
|
||||
public string Area { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 층
|
||||
/// 객체가 위치한 건물의 층을 나타냅니다. (예: "1F", "B2")
|
||||
/// </summary>
|
||||
public string Floor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 모든 속성을 초기화하는 생성자입니다.
|
||||
/// </summary>
|
||||
/// <param name="name">객체 이름</param>
|
||||
/// <param name="id">고유 ID</param>
|
||||
/// <param name="position">위치 정보</param>
|
||||
/// <param name="area">구역 정보</param>
|
||||
/// <param name="floor">층 정보</param>
|
||||
public FactoryObjectInfo(string name, string id, string position, string area, string floor)
|
||||
{
|
||||
Name = name;
|
||||
@@ -36,12 +88,21 @@
|
||||
Floor = floor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 FactoryObjectInfo 객체와 모든 속성 값이 동일한지 비교합니다.
|
||||
/// </summary>
|
||||
/// <param name="other">비교할 다른 FactoryObjectInfo 객체입니다.</param>
|
||||
/// <returns>모든 속성이 같으면 true, 그렇지 않으면 false를 반환합니다.</returns>
|
||||
public bool Equals(FactoryObjectInfo other)
|
||||
{
|
||||
if (other == null) return false;
|
||||
return Name == other.Name && Id == other.Id && Position == other.Position && Area == other.Area && Floor == other.Floor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 객체의 정보를 요약된 문자열 형태로 반환합니다. 디버깅이나 로깅에 유용합니다.
|
||||
/// </summary>
|
||||
/// <returns>객체의 모든 속성 정보를 포함하는 문자열입니다.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Name:{Name},Id:{Id},Position:{Position},Area:{Area},Floor:{Floor}";
|
||||
|
||||
@@ -5,10 +5,83 @@ using UVC.Core;
|
||||
|
||||
namespace UVC.Factory.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 씬에 존재하는 모든 FactoryObject 인스턴스를 관리하는 싱글톤 클래스입니다.
|
||||
/// 이 매니저를 통해 팩토리 객체를 등록, 등록 해제 및 검색할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SingletonScene을 상속받아 구현되었으므로, `FactoryObjectManager.Instance`를 통해 씬의 어디에서든 쉽게 접근할 수 있습니다.
|
||||
/// FactoryObject는 Info 속성이 설정될 때 자동으로 이 매니저에 등록되며, 파괴될 때 자동으로 등록 해제됩니다.
|
||||
/// 따라서 개발자가 직접 Register/Unregister 메서드를 호출할 일은 거의 없습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// 다음은 FactoryObjectManager를 사용하여 씬에 있는 특정 기계를 찾고 제어하는 예제입니다.
|
||||
/// <code>
|
||||
/// using UnityEngine;
|
||||
/// using UVC.Factory.Component;
|
||||
/// using UVC.Data;
|
||||
///
|
||||
/// public class ControlPanel : MonoBehaviour
|
||||
/// {
|
||||
/// void Start()
|
||||
/// {
|
||||
/// // 가정: 씬 어딘가에 이미 FactoryObject(예: MachineObject)가 생성되어 있고,
|
||||
/// // Info 속성이 설정되어 FactoryObjectManager에 등록된 상태입니다.
|
||||
/// //
|
||||
/// // var machine = new MachineObject();
|
||||
/// // machine.Info = new FactoryObjectInfo("Drill-01", "MC-1024", ...);
|
||||
/// }
|
||||
///
|
||||
/// // UI 버튼 클릭 시 호출될 함수
|
||||
/// public void OnFindAndStartMachineButtonClick()
|
||||
/// {
|
||||
/// // 1. ID를 사용하여 FactoryObjectManager에서 특정 기계를 찾습니다.
|
||||
/// FactoryObject machine = FactoryObjectManager.Instance.FindById("MC-1024");
|
||||
///
|
||||
/// if (machine != null)
|
||||
/// {
|
||||
/// Debug.Log($"기계를 찾았습니다: {machine.Info.Name}");
|
||||
///
|
||||
/// // 2. 찾은 기계의 데이터를 업데이트하여 가동시킵니다.
|
||||
/// var command = new DataObject { { "status", "running" } };
|
||||
/// machine.UpdateData(command);
|
||||
/// }
|
||||
/// else
|
||||
/// {
|
||||
/// Debug.LogWarning("ID 'MC-1024'에 해당하는 기계를 찾을 수 없습니다.");
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // 특정 구역의 모든 객체를 찾는 예시
|
||||
/// public void OnFindAllObjectsInArea_A_ButtonClick()
|
||||
/// {
|
||||
/// // "Area-A" 구역에 있는 모든 객체를 찾습니다.
|
||||
/// List<FactoryObject> objectsInArea = FactoryObjectManager.Instance.FindByArea("Area-A");
|
||||
///
|
||||
/// Debug.Log($"'Area-A' 구역에서 {objectsInArea.Count}개의 객체를 찾았습니다.");
|
||||
///
|
||||
/// foreach (var obj in objectsInArea)
|
||||
/// {
|
||||
/// Debug.Log($"- 객체 이름: {obj.Info.Name}, ID: {obj.Info.Id}");
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class FactoryObjectManager : SingletonScene<FactoryObjectManager>
|
||||
{
|
||||
/// <summary>
|
||||
/// 등록된 모든 FactoryObject를 저장하는 딕셔너리입니다.
|
||||
/// Key는 객체의 식별 정보인 FactoryObjectInfo이고, Value는 FactoryObject 인스턴스입니다.
|
||||
/// </summary>
|
||||
public Dictionary<FactoryObjectInfo, FactoryObject> FactoryObjects { get; private set; } = new Dictionary<FactoryObjectInfo, FactoryObject>();
|
||||
|
||||
/// <summary>
|
||||
/// 새로운 FactoryObject를 매니저에 등록합니다.
|
||||
/// 일반적으로 FactoryObject의 Info 속성이 설정될 때 자동으로 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="factoryObject">등록할 FactoryObject 인스턴스입니다.</param>
|
||||
/// <exception cref="ArgumentNullException">factoryObject 또는 factoryObject.Info가 null일 때 발생합니다.</exception>
|
||||
public void RegisterFactoryObject(FactoryObject factoryObject)
|
||||
{
|
||||
if (factoryObject == null || factoryObject.Info == null)
|
||||
@@ -21,6 +94,11 @@ namespace UVC.Factory.Component
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매니저에서 FactoryObject의 등록을 해제합니다.
|
||||
/// 일반적으로 FactoryObject가 파괴될 때(OnDestroy) 자동으로 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="factoryObjectInfo">등록 해제할 객체의 FactoryObjectInfo입니다.</param>
|
||||
public void UnregisterFactoryObject(FactoryObjectInfo factoryObjectInfo)
|
||||
{
|
||||
if (factoryObjectInfo == null)
|
||||
@@ -31,6 +109,12 @@ namespace UVC.Factory.Component
|
||||
FactoryObjects.Remove(factoryObjectInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FactoryObjectInfo를 사용하여 등록된 FactoryObject 인스턴스를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="factoryObjectInfo">찾고자 하는 객체의 FactoryObjectInfo입니다.</param>
|
||||
/// <returns>발견된 FactoryObject 인스턴스. 없으면 null을 반환합니다.</returns>
|
||||
/// <exception cref="ArgumentNullException">factoryObjectInfo가 null일 때 발생합니다.</exception>
|
||||
public FactoryObject? GetFactoryObject(FactoryObjectInfo factoryObjectInfo)
|
||||
{
|
||||
if (factoryObjectInfo == null)
|
||||
@@ -41,6 +125,11 @@ namespace UVC.Factory.Component
|
||||
return factoryObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 객체의 이름(Name)으로 FactoryObject를 검색합니다. 대소문자를 구분하지 않습니다.
|
||||
/// </summary>
|
||||
/// <param name="name">찾고자 하는 객체의 이름입니다.</param>
|
||||
/// <returns>처음으로 발견된 일치하는 FactoryObject. 없으면 null을 반환합니다.</returns>
|
||||
public FactoryObject? FindByName(string name)
|
||||
{
|
||||
foreach (var kvp in FactoryObjects)
|
||||
@@ -53,6 +142,11 @@ namespace UVC.Factory.Component
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 객체의 고유 ID로 FactoryObject를 검색합니다. 대소문자를 구분하지 않습니다.
|
||||
/// </summary>
|
||||
/// <param name="id">찾고자 하는 객체의 ID입니다.</param>
|
||||
/// <returns>처음으로 발견된 일치하는 FactoryObject. 없으면 null을 반환합니다.</returns>
|
||||
public FactoryObject? FindById(string id)
|
||||
{
|
||||
foreach (var kvp in FactoryObjects)
|
||||
@@ -65,6 +159,11 @@ namespace UVC.Factory.Component
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 객체의 위치(Position) 정보로 FactoryObject를 검색합니다. 대소문자를 구분하지 않습니다.
|
||||
/// </summary>
|
||||
/// <param name="position">찾고자 하는 객체의 위치 정보입니다.</param>
|
||||
/// <returns>처음으로 발견된 일치하는 FactoryObject. 없으면 null을 반환합니다.</returns>
|
||||
public FactoryObject? FindByPosition(string position)
|
||||
{
|
||||
foreach (var kvp in FactoryObjects)
|
||||
@@ -77,6 +176,11 @@ namespace UVC.Factory.Component
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 구역(Area)에 속한 모든 FactoryObject를 검색합니다. 대소문자를 구분하지 않습니다.
|
||||
/// </summary>
|
||||
/// <param name="area">찾고자 하는 구역의 이름입니다.</param>
|
||||
/// <returns>발견된 모든 FactoryObject의 리스트. 없으면 빈 리스트를 반환합니다.</returns>
|
||||
public List<FactoryObject> FindByArea(string area)
|
||||
{
|
||||
List<FactoryObject> foundObjects = new List<FactoryObject>();
|
||||
@@ -90,6 +194,11 @@ namespace UVC.Factory.Component
|
||||
return foundObjects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 층(Floor)에 속한 모든 FactoryObject를 검색합니다. 대소문자를 구분하지 않습니다.
|
||||
/// </summary>
|
||||
/// <param name="floor">찾고자 하는 층의 이름입니다.</param>
|
||||
/// <returns>발견된 모든 FactoryObject의 리스트. 없으면 빈 리스트를 반환합니다.</returns>
|
||||
public List<FactoryObject> FindByFloor(string floor)
|
||||
{
|
||||
List<FactoryObject> foundObjects = new List<FactoryObject>();
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace UVC.Tests.Data
|
||||
public class HttpPipeLineTests
|
||||
{
|
||||
// 테스트에 사용할 HttpPipeLine 인스턴스
|
||||
private HttpPipeLine pipeLine;
|
||||
private HttpPipeLine? pipeLine;
|
||||
|
||||
/// <summary>
|
||||
/// 각 테스트 실행 전에 호출되는 설정 메서드입니다.
|
||||
@@ -62,14 +62,20 @@ namespace UVC.Tests.Data
|
||||
//await RunTestAsync(nameof(Test_RepeatWithCount_StopsAutomatically), Test_RepeatWithCount_StopsAutomatically);
|
||||
|
||||
// HttpResponseMask 테스트 추가
|
||||
Debug.Log("===== HttpResponseMask 테스트 시작 =====");
|
||||
RunTest(nameof(HttpResponseMask_Apply_SuccessfulResponse_ReturnsSuccessWithData), HttpResponseMask_Apply_SuccessfulResponse_ReturnsSuccessWithData);
|
||||
RunTest(nameof(HttpResponseMask_Apply_FailedResponse_WrongSuccessValue_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_WrongSuccessValue_ReturnsFailWithMessage);
|
||||
RunTest(nameof(HttpResponseMask_Apply_FailedResponse_MissingSuccessKey_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_MissingSuccessKey_ReturnsFailWithMessage);
|
||||
RunTest(nameof(HttpResponseMask_Apply_FailedResponse_MissingDataKey_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_MissingDataKey_ReturnsFailWithMessage);
|
||||
RunTest(nameof(HttpResponseMask_Apply_SuccessfulResponse_CustomKeys_ReturnsSuccessWithData), HttpResponseMask_Apply_SuccessfulResponse_CustomKeys_ReturnsSuccessWithData);
|
||||
RunTest(nameof(HttpResponseMask_Apply_InvalidJson_ThrowsException), HttpResponseMask_Apply_InvalidJson_ThrowsException);
|
||||
Debug.Log("===== HttpResponseMask 테스트 완료 =====");
|
||||
//Debug.Log("===== HttpResponseMask 테스트 시작 =====");
|
||||
//RunTest(nameof(HttpResponseMask_Apply_SuccessfulResponse_ReturnsSuccessWithData), HttpResponseMask_Apply_SuccessfulResponse_ReturnsSuccessWithData);
|
||||
//RunTest(nameof(HttpResponseMask_Apply_FailedResponse_WrongSuccessValue_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_WrongSuccessValue_ReturnsFailWithMessage);
|
||||
//RunTest(nameof(HttpResponseMask_Apply_FailedResponse_MissingSuccessKey_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_MissingSuccessKey_ReturnsFailWithMessage);
|
||||
//RunTest(nameof(HttpResponseMask_Apply_FailedResponse_MissingDataKey_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_MissingDataKey_ReturnsFailWithMessage);
|
||||
//RunTest(nameof(HttpResponseMask_Apply_SuccessfulResponse_CustomKeys_ReturnsSuccessWithData), HttpResponseMask_Apply_SuccessfulResponse_CustomKeys_ReturnsSuccessWithData);
|
||||
//RunTest(nameof(HttpResponseMask_Apply_InvalidJson_ThrowsException), HttpResponseMask_Apply_InvalidJson_ThrowsException);
|
||||
//Debug.Log("===== HttpResponseMask 테스트 완료 =====");
|
||||
|
||||
Debug.Log("===== DataValidator 테스트 시작 =====");
|
||||
await RunTestAsync(nameof(Test_Excute_WithValidData_ValidatorPasses), Test_Excute_WithValidData_ValidatorPasses);
|
||||
await RunTestAsync(nameof(Test_Excute_WithInvalidData_ValidatorFails), Test_Excute_WithInvalidData_ValidatorFails);
|
||||
await RunTestAsync(nameof(Test_Excute_WithArrayAndValidator_FiltersInvalidData), Test_Excute_WithArrayAndValidator_FiltersInvalidData);
|
||||
Debug.Log("===== DataValidator 테스트 완료 =====");
|
||||
|
||||
Debug.Log("===== HttpPipeLine 테스트 완료 =====");
|
||||
}
|
||||
@@ -245,6 +251,7 @@ namespace UVC.Tests.Data
|
||||
.setDataMapper(dataMapper)
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
Debug.Log("핸들러 호출됨");
|
||||
handlerCalled = true;
|
||||
if (data is DataObject dataObject)
|
||||
{
|
||||
@@ -1127,5 +1134,166 @@ namespace UVC.Tests.Data
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DataValidator Tests
|
||||
|
||||
/// <summary>
|
||||
/// DataValidator를 사용하여 유효한 데이터를 성공적으로 처리하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async UniTask Test_Excute_WithValidData_ValidatorPasses()
|
||||
{
|
||||
// Arrange
|
||||
bool handlerCalled = false;
|
||||
IDataObject? receivedData = null;
|
||||
string testUrl = "http://test.com/validator-pass";
|
||||
var mockResponse = @"{""message"": ""Success"", ""data"": {""id"": 1, ""status"": ""active""}}";
|
||||
MockHttpRequester.SetResponse(testUrl, mockResponse);
|
||||
|
||||
var dataMapper = new DataMapper(new DataMask { ["id"] = 0, ["status"] = "" });
|
||||
|
||||
// "status" 필드가 "active"인 경우에만 유효하도록 설정
|
||||
var validator = new DataValidator();
|
||||
validator.AddValidator("status", value => {
|
||||
return value is string s && s == "active";
|
||||
});
|
||||
|
||||
var info = new HttpPipeLineInfo(testUrl)
|
||||
.setDataMapper(dataMapper)
|
||||
.setValidator(validator)
|
||||
.setSuccessHandler(data =>
|
||||
{
|
||||
handlerCalled = true;
|
||||
receivedData = data;
|
||||
})
|
||||
.setFailHandler((message) =>
|
||||
{
|
||||
Debug.LogError("Fail message: " + message);
|
||||
});
|
||||
|
||||
pipeLine.Add("validatorPassTest", info);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await pipeLine.Excute("validatorPassTest");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(handlerCalled, "유효성 검사를 통과했으므로 핸들러가 호출되어야 합니다.");
|
||||
Assert.IsNotNull(receivedData, "데이터가 핸들러로 전달되어야 합니다.");
|
||||
Assert.AreEqual("active", (receivedData as DataObject)?.GetString("status"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await pipeLine.RemoveAsync("validatorPassTest");
|
||||
MockHttpRequester.ClearResponses(testUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataValidator를 사용하여 유효하지 않은 데이터를 필터링하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async UniTask Test_Excute_WithInvalidData_ValidatorFails()
|
||||
{
|
||||
// Arrange
|
||||
bool handlerCalled = false;
|
||||
string testUrl = "http://test.com/validator-fail";
|
||||
var mockResponse = @"{""message"": ""Success"", ""data"": {""id"": 2, ""status"": ""inactive""}}";
|
||||
MockHttpRequester.SetResponse(testUrl, mockResponse);
|
||||
|
||||
var dataMapper = new DataMapper(new DataMask { ["id"] = 0, ["status"] = "" });
|
||||
|
||||
// "status" 필드가 "active"인 경우에만 유효하도록 설정
|
||||
var validator = new DataValidator();
|
||||
validator.AddValidator("status", value => value is string s && s == "active");
|
||||
|
||||
var info = new HttpPipeLineInfo(testUrl)
|
||||
.setDataMapper(dataMapper)
|
||||
.setValidator(validator)
|
||||
.setSuccessHandler(data =>
|
||||
{
|
||||
handlerCalled = true; // 이 핸들러는 호출되지 않아야 함
|
||||
});
|
||||
|
||||
pipeLine.Add("validatorFailTest", info);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await pipeLine.Excute("validatorFailTest");
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(handlerCalled, "유효성 검사에 실패했으므로 핸들러가 호출되지 않아야 합니다.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await pipeLine.RemoveAsync("validatorFailTest");
|
||||
MockHttpRequester.ClearResponses(testUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataValidator가 배열 데이터에서 유효한 항목만 필터링하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async UniTask Test_Excute_WithArrayAndValidator_FiltersInvalidData()
|
||||
{
|
||||
// Arrange
|
||||
bool handlerCalled = false;
|
||||
IDataObject? receivedData = null;
|
||||
string testUrl = "http://test.com/validator-array";
|
||||
var mockResponse = @"{
|
||||
""message"": ""Success"",
|
||||
""data"": [
|
||||
{""id"": 1, ""value"": 10},
|
||||
{""id"": 2, ""value"": 20},
|
||||
{""id"": 3, ""value"": 5}
|
||||
]
|
||||
}";
|
||||
MockHttpRequester.SetResponse(testUrl, mockResponse);
|
||||
|
||||
var dataMapper = new DataMapper(new DataMask { ["id"] = 0, ["value"] = 0 });
|
||||
|
||||
// "value"가 15보다 큰 항목만 유효하도록 설정
|
||||
var validator = new DataValidator();
|
||||
validator.AddValidator("value", value => {
|
||||
return value is int v && v > 15;
|
||||
});
|
||||
|
||||
var info = new HttpPipeLineInfo(testUrl)
|
||||
.setDataMapper(dataMapper)
|
||||
.setValidator(validator)
|
||||
.setSuccessHandler(data =>
|
||||
{
|
||||
handlerCalled = true;
|
||||
receivedData = data;
|
||||
});
|
||||
|
||||
pipeLine.Add("validatorArrayTest", info);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await pipeLine.Excute("validatorArrayTest");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(handlerCalled, "핸들러가 호출되어야 합니다.");
|
||||
Assert.IsNotNull(receivedData, "데이터가 핸들러로 전달되어야 합니다.");
|
||||
Assert.IsTrue(receivedData is DataArray, "결과는 DataArray여야 합니다.");
|
||||
|
||||
var dataArray = receivedData as DataArray;
|
||||
Assert.AreEqual(1, dataArray.Count, "유효한 항목은 1개여야 합니다.");
|
||||
Assert.AreEqual(20, dataArray[0].GetInt("value"), "필터링된 데이터의 값이 올바르지 않습니다.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await pipeLine.RemoveAsync("validatorArrayTest");
|
||||
MockHttpRequester.ClearResponses(testUrl);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UVC.Data;
|
||||
using UVC.Log;
|
||||
@@ -51,7 +52,11 @@ namespace UVC.Tests.Data
|
||||
//RunTest(nameof(OnTopicMessage_ValidJsonObject_CallsHandler), OnTopicMessage_ValidJsonObject_CallsHandler);
|
||||
//RunTest(nameof(OnTopicMessage_JsonArray_CallsHandler), OnTopicMessage_JsonArray_CallsHandler);
|
||||
//RunTest(nameof(OnTopicMessage_EmptyMessage_DoesNotCallHandler), OnTopicMessage_EmptyMessage_DoesNotCallHandler);
|
||||
RunTest(nameof(OnTopicMessage_InvalidJson_DoesNotCallHandler), OnTopicMessage_InvalidJson_DoesNotCallHandler);
|
||||
//RunTest(nameof(OnTopicMessage_InvalidJson_DoesNotCallHandler), OnTopicMessage_InvalidJson_DoesNotCallHandler);
|
||||
|
||||
//await RunTestAsync(nameof(OnTopicMessage_WithValidData_ValidatorPassesAsync), OnTopicMessage_WithValidData_ValidatorPassesAsync);
|
||||
//await RunTestAsync(nameof(OnTopicMessage_WithInvalidData_ValidatorFailsAsync), OnTopicMessage_WithInvalidData_ValidatorFailsAsync);
|
||||
await RunTestAsync(nameof(OnTopicMessage_WithArrayAndValidator_FiltersInvalidDataAsync), OnTopicMessage_WithArrayAndValidator_FiltersInvalidDataAsync);
|
||||
Debug.Log("===== MQTTPipeLine 테스트 완료 =====");
|
||||
}
|
||||
|
||||
@@ -568,6 +573,133 @@ namespace UVC.Tests.Data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region DataValidator Tests
|
||||
|
||||
/// <summary>
|
||||
/// DataValidator를 사용하여 유효한 데이터를 성공적으로 처리하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async UniTask OnTopicMessage_WithValidData_ValidatorPassesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var testPipeLine = new TestMQTTPipeLine();
|
||||
var handler = new TestDataHandler();
|
||||
var dataMapper = new DataMapper(new DataMask { ["id"] = 0, ["status"] = "" });
|
||||
|
||||
// "status" 필드가 "active"인 경우에만 유효하도록 설정
|
||||
var validator = new DataValidator();
|
||||
validator.AddValidator("status", value => {
|
||||
Debug.Log($"Validator called with value: {value}, {value is string s2 && s2 == "active"}");
|
||||
return value is string s && s == "active";
|
||||
});
|
||||
|
||||
var pipelineInfo = new MQTTPipeLineInfo("test_topic")
|
||||
.setDataMapper(dataMapper)
|
||||
.setValidator(validator)
|
||||
.setHandler(handler.HandleData);
|
||||
|
||||
testPipeLine.Add(pipelineInfo);
|
||||
|
||||
var validMessage = @"{""id"": 1, ""status"": ""active""}";
|
||||
|
||||
// Act
|
||||
testPipeLine.TestOnTopicMessage("test_topic", validMessage);
|
||||
|
||||
await UniTask.Delay(2000); // 핸들러가 호출되기 전에 잠시 대기
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, handler.CallCount, "유효성 검사를 통과했으므로 핸들러가 호출되어야 합니다.");
|
||||
Assert.IsNotNull(handler.LastDataObject, "데이터가 핸들러로 전달되어야 합니다.");
|
||||
Assert.AreEqual("active", (handler.LastDataObject as DataObject)?.GetString("status"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataValidator를 사용하여 유효하지 않은 데이터를 필터링하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async UniTask OnTopicMessage_WithInvalidData_ValidatorFailsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var testPipeLine = new TestMQTTPipeLine();
|
||||
var handler = new TestDataHandler();
|
||||
var dataMapper = new DataMapper(new DataMask { ["id"] = 0, ["status"] = "" });
|
||||
|
||||
// "status" 필드가 "active"인 경우에만 유효하도록 설정
|
||||
var validator = new DataValidator();
|
||||
validator.AddValidator("status", value => {
|
||||
Debug.Log($"Validator called with value2: {value}, {value is string s2 && s2 == "active"}");
|
||||
return value is string s && s == "active";
|
||||
});
|
||||
|
||||
var pipelineInfo = new MQTTPipeLineInfo("test_topic")
|
||||
.setDataMapper(dataMapper)
|
||||
.setValidator(validator)
|
||||
.setHandler(handler.HandleData);
|
||||
|
||||
testPipeLine.Add(pipelineInfo);
|
||||
|
||||
var invalidMessage = @"{""id"": 2, ""status"": ""inactive""}";
|
||||
|
||||
// Act
|
||||
testPipeLine.TestOnTopicMessage("test_topic", invalidMessage);
|
||||
|
||||
await UniTask.Delay(2000); // 핸들러가 호출되기 전에 잠시 대기
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(0, handler.CallCount, "유효성 검사에 실패했으므로 핸들러가 호출되지 않아야 합니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataValidator가 배열 데이터에서 유효한 항목만 필터링하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async UniTask OnTopicMessage_WithArrayAndValidator_FiltersInvalidDataAsync()
|
||||
{
|
||||
// Arrange
|
||||
var testPipeLine = new TestMQTTPipeLine();
|
||||
var handler = new TestDataHandler();
|
||||
var dataMapper = new DataMapper(new DataMask { ["id"] = 0, ["value"] = 0 });
|
||||
|
||||
// "value"가 15보다 큰 항목만 유효하도록 설정
|
||||
var validator = new DataValidator();
|
||||
validator.AddValidator("value", value =>
|
||||
{
|
||||
Debug.Log($"Validator called with value3: {value}, {value is int v2 && v2 > 15}");
|
||||
return value is int v && v > 15;
|
||||
});
|
||||
|
||||
var pipelineInfo = new MQTTPipeLineInfo("test_topic")
|
||||
.setDataMapper(dataMapper)
|
||||
.setValidator(validator)
|
||||
.setHandler(handler.HandleData);
|
||||
|
||||
testPipeLine.Add(pipelineInfo);
|
||||
|
||||
var arrayMessage = @"[
|
||||
{""id"": 1, ""value"": 10},
|
||||
{""id"": 2, ""value"": 20},
|
||||
{""id"": 3, ""value"": 5}
|
||||
]";
|
||||
|
||||
// Act
|
||||
testPipeLine.TestOnTopicMessage("test_topic", arrayMessage);
|
||||
|
||||
await UniTask.Delay(2000); // 핸들러가 호출되기 전에 잠시 대기
|
||||
|
||||
Debug.Log($"LastDataObject: {handler.LastDataObject.GetType()}");
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, handler.CallCount, "핸들러가 한 번 호출되어야 합니다.");
|
||||
Assert.IsNotNull(handler.LastDataObject, "데이터가 핸들러로 전달되어야 합니다.");
|
||||
Assert.IsTrue(handler.LastDataObject is DataArray, "결과는 DataArray여야 합니다.");
|
||||
|
||||
var dataArray = handler.LastDataObject as DataArray;
|
||||
Assert.AreEqual(1, dataArray.Count, "유효한 항목은 1개여야 합니다.");
|
||||
Assert.AreEqual(20, dataArray[0].GetInt("value"), "필터링된 데이터의 값이 올바르지 않습니다.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
// MQTTPipeLine의 OnTopicMessage 메서드를 테스트하기 위한 확장 클래스
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace UVC.Tests
|
||||
public static void RunAllTests()
|
||||
{
|
||||
//new DataMapperTests().TestAll();
|
||||
new HttpPipeLineTests().TestAll();
|
||||
//new MQTTPipeLineTests().TestAll();
|
||||
//new HttpPipeLineTests().TestAll();
|
||||
new MQTTPipeLineTests().TestAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
|
||||
using Cysharp.Threading.Tasks;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
@@ -22,8 +23,8 @@ namespace UVC.Threading
|
||||
/// // 백그라운드 작업 수행
|
||||
/// var result = ComputeIntensiveTask();
|
||||
///
|
||||
/// // UI 업데이트는 메인 스레드에서 수행
|
||||
/// MainThreadDispatcher.Instance.Enqueue(() => {
|
||||
/// // UI 업데이트는 메인 스레드에서 수행 (Fire-and-forget)
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// UpdateUIWithResult(result);
|
||||
/// });
|
||||
/// });
|
||||
@@ -40,8 +41,8 @@ namespace UVC.Threading
|
||||
/// var data = await FetchDataFromServerAsync();
|
||||
/// var processedData = ProcessLargeData(data);
|
||||
///
|
||||
/// // 결과를 메인 스레드로 전달하여 UI 업데이트
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// // 결과를 메인 스레드로 전달하여 UI 업데이트하고 완료까지 대기
|
||||
/// await MainThreadDispatcher.Instance.SendToMainThreadAsync(() => {
|
||||
/// // 여기에서 UI 컴포넌트를 안전하게 업데이트
|
||||
/// UpdateUI(processedData);
|
||||
/// ShowSuccessMessage("데이터 처리 완료");
|
||||
@@ -50,7 +51,7 @@ namespace UVC.Threading
|
||||
/// catch (Exception ex)
|
||||
/// {
|
||||
/// // 오류 발생 시에도 메인 스레드에서 UI 관련 작업 처리
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// await MainThreadDispatcher.Instance.SendToMainThreadAsync(() => {
|
||||
/// ShowErrorDialog($"오류 발생: {ex.Message}");
|
||||
/// Debug.LogException(ex);
|
||||
/// });
|
||||
@@ -78,8 +79,8 @@ namespace UVC.Threading
|
||||
/// // 시간이 걸리는 작업 수행
|
||||
/// var result = await PerformHeavyComputationAsync();
|
||||
///
|
||||
/// // 결과를 UI에 반영 (메인 스레드에서)
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// // 결과를 UI에 반영하고 완료까지 대기 (메인 스레드에서)
|
||||
/// await MainThreadDispatcher.Instance.SendToMainThreadAsync(() => {
|
||||
/// resultText.text = result.ToString();
|
||||
/// loadingIndicator.SetActive(false);
|
||||
/// });
|
||||
@@ -87,7 +88,7 @@ namespace UVC.Threading
|
||||
/// catch (Exception ex)
|
||||
/// {
|
||||
/// // 예외 처리도 메인 스레드에서
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// await MainThreadDispatcher.Instance.SendToMainThreadAsync(() => {
|
||||
/// errorText.text = ex.Message;
|
||||
/// loadingIndicator.SetActive(false);
|
||||
/// });
|
||||
@@ -165,7 +166,7 @@ namespace UVC.Threading
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SynchronizationContext를 통해 메인 스레드에서 액션을 실행합니다.
|
||||
/// 메인 스레드에서 액션을 실행합니다. (Fire-and-forget)
|
||||
/// </summary>
|
||||
/// <param name="action">메인 스레드에서 실행할 액션</param>
|
||||
public void SendToMainThread(Action action)
|
||||
@@ -184,6 +185,56 @@ namespace UVC.Threading
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 스레드에서 액션을 비동기적으로 실행하고, 해당 액션의 완료를 나타내는 UniTask를 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="action">메인 스레드에서 실행할 액션입니다.</param>
|
||||
/// <returns>액션의 실행이 완료되면 완료되는 UniTask입니다.</returns>
|
||||
public UniTask SendToMainThreadAsync(Action action)
|
||||
{
|
||||
if (action == null) return UniTask.CompletedTask;
|
||||
|
||||
// 현재 스레드가 메인 스레드인 경우 즉시 실행
|
||||
if (_mainThreadContext != null && SynchronizationContext.Current == _mainThreadContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return UniTask.FromException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
var utcs = new UniTaskCompletionSource();
|
||||
Action wrappedAction = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
utcs.TrySetResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
utcs.TrySetException(e);
|
||||
}
|
||||
};
|
||||
|
||||
if (_mainThreadContext != null)
|
||||
{
|
||||
_mainThreadContext.Post(_ => wrappedAction(), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// SynchronizationContext가 없는 경우 큐 사용
|
||||
Enqueue(wrappedAction);
|
||||
}
|
||||
|
||||
return utcs.Task;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 큐에 있는 모든 액션 처리
|
||||
|
||||
@@ -17,11 +17,7 @@ namespace UVC.UI.Info
|
||||
|
||||
[Tooltip("Label 정보 텍스트를 표시할 UI 요소")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI labelText;
|
||||
|
||||
[Tooltip("Value 정보 텍스트를 표시할 UI 요소")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI valueText;
|
||||
private TextMeshProUGUI text;
|
||||
|
||||
[Tooltip("정보 창을 닫을 버튼")]
|
||||
[SerializeField]
|
||||
@@ -29,7 +25,11 @@ namespace UVC.UI.Info
|
||||
|
||||
[Tooltip("UI가 객체를 가리지 않도록 할 월드 좌표계 오프셋")]
|
||||
[SerializeField]
|
||||
private Vector2 screenOffset = new Vector2(0f, 0f);
|
||||
private Vector2 screenOffset = new Vector2(10f, 10f);
|
||||
|
||||
[Tooltip("UI가 객체를 가리지 않도록 할 메뉴바 높이")]
|
||||
[SerializeField]
|
||||
private float menuBarHeight = 70f;
|
||||
|
||||
// 정보 창이 따라다닐 3D 객체의 Transform
|
||||
private Transform? target;
|
||||
@@ -40,20 +40,20 @@ namespace UVC.UI.Info
|
||||
/// </summary>
|
||||
public bool IsVisible => gameObject.activeSelf;
|
||||
|
||||
private RectTransform? rectTransform;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
// 닫기 버튼이 할당되었으면 클릭 이벤트를 연결합니다.
|
||||
if (closeButton != null)
|
||||
{
|
||||
closeButton.onClick.AddListener(Hide);
|
||||
}
|
||||
|
||||
RectTransform? rectTransform = transform as RectTransform;
|
||||
if (rectTransform != null && screenOffset == Vector2.zero) screenOffset = new Vector2(rectTransform.rect.width / 2 + 10f, - rectTransform.rect.height / 2);
|
||||
|
||||
// 처음에는 정보 창을 숨깁니다.
|
||||
if (gameObject.activeSelf)
|
||||
{
|
||||
@@ -90,39 +90,33 @@ namespace UVC.UI.Info
|
||||
// 추가 오프셋 적용
|
||||
screenPosRight.x += screenOffset.x;
|
||||
screenPosRight.y += screenOffset.y;
|
||||
|
||||
// 메뉴바 영역(상단 70픽셀) 고려 및 화면 밖으로 나가지 않도록 제한
|
||||
float menuBarHeight = 70f;
|
||||
screenPosRight.x = Mathf.Clamp(screenPosRight.x, 100f, Screen.width - 100f);
|
||||
screenPosRight.y = Mathf.Clamp(screenPosRight.y, 100f, Screen.height - menuBarHeight);
|
||||
// 메뉴바 영역 고려 및 화면 밖으로 나가지 않도록 제한
|
||||
screenPosRight.x = Mathf.Clamp(screenPosRight.x, 0, Screen.width - rectTransform!.rect.width);
|
||||
screenPosRight.y = Mathf.Clamp(screenPosRight.y, rectTransform!.rect.height, Screen.height - menuBarHeight);
|
||||
|
||||
// RectTransform을 사용하여 UI 위치 설정
|
||||
RectTransform? rectTransform = transform as RectTransform;
|
||||
if (rectTransform != null)
|
||||
// 캔버스의 렌더링 모드에 따라 다르게 처리
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null)
|
||||
{
|
||||
// 캔버스의 렌더링 모드에 따라 다르게 처리
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null)
|
||||
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
|
||||
{
|
||||
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
|
||||
{
|
||||
rectTransform.position = screenPosRight;
|
||||
}
|
||||
else if (canvas.renderMode == RenderMode.ScreenSpaceCamera ||
|
||||
canvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
// 스크린 좌표를 캔버스 상의 로컬 좌표로 변환
|
||||
Vector2 localPoint;
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvas.GetComponent<RectTransform>(),
|
||||
screenPosRight,
|
||||
canvas.renderMode == RenderMode.ScreenSpaceCamera ? canvas.worldCamera : Camera.main,
|
||||
out localPoint);
|
||||
|
||||
rectTransform.localPosition = new Vector3(localPoint.x, localPoint.y, rectTransform.localPosition.z);
|
||||
}
|
||||
rectTransform.position = screenPosRight;
|
||||
}
|
||||
else if (canvas.renderMode == RenderMode.ScreenSpaceCamera ||
|
||||
canvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
// 스크린 좌표를 캔버스 상의 로컬 좌표로 변환
|
||||
Vector2 localPoint;
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvas.GetComponent<RectTransform>(),
|
||||
screenPosRight,
|
||||
canvas.renderMode == RenderMode.ScreenSpaceCamera ? canvas.worldCamera : Camera.main,
|
||||
out localPoint);
|
||||
rectTransform.localPosition = new Vector3(localPoint.x, localPoint.y, rectTransform.localPosition.z);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// UI가 항상 보이도록 카메라를 향하게 설정 (World Space Canvas인 경우에만 필요)
|
||||
Canvas parentCanvas = GetComponentInParent<Canvas>();
|
||||
@@ -142,28 +136,8 @@ namespace UVC.UI.Info
|
||||
public void Show(Transform targetObject, Dictionary<string, object> information)
|
||||
{
|
||||
target = targetObject;
|
||||
if (labelText != null)
|
||||
{
|
||||
string labelString = string.Empty;
|
||||
string valueString = string.Empty;
|
||||
foreach (var kvp in information)
|
||||
{
|
||||
labelString += $"{kvp.Key}\n";
|
||||
valueString += $"{kvp.Value ?? "null"}\n";
|
||||
}
|
||||
labelString = labelString.TrimEnd('\n'); // 마지막 줄바꿈 제거
|
||||
valueString = valueString.TrimEnd('\n'); // 마지막 줄바꿈 제거
|
||||
Debug.Log($"InfoWindow: {labelString}, {valueString}");
|
||||
labelText.text = labelString;
|
||||
valueText.text = valueString;
|
||||
}
|
||||
|
||||
//size를 text에 맞게 조정합니다.
|
||||
RectTransform? rect = GetComponent<RectTransform>();
|
||||
if (rect != null)
|
||||
{
|
||||
rect.sizeDelta = new Vector2(rect.rect.width, valueText.preferredHeight + 25f);
|
||||
}
|
||||
UpdateInformation(information);
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
@@ -171,6 +145,30 @@ namespace UVC.UI.Info
|
||||
LateUpdate();
|
||||
}
|
||||
|
||||
public void UpdateInformation(Dictionary<string, object> information)
|
||||
{
|
||||
if (target == null) return;
|
||||
if (text != null)
|
||||
{
|
||||
string combinedString = string.Empty;
|
||||
foreach (var kvp in information)
|
||||
{
|
||||
// <indent> 태그를 사용하여 줄바꿈 시에도 정렬이 유지되도록 합니다.
|
||||
combinedString += $"{kvp.Key}:<pos=40%><indent=40%>{kvp.Value ?? "null"}</indent>\n";
|
||||
}
|
||||
combinedString = combinedString.TrimEnd('\n'); // 마지막 줄바꿈 제거
|
||||
text.text = combinedString;
|
||||
}
|
||||
// size를 text에 맞게 조정합니다.
|
||||
RectTransform? rect = GetComponent<RectTransform>();
|
||||
if (rect != null && text != null)
|
||||
{
|
||||
RectTransform textRect = text.GetComponent<RectTransform>();
|
||||
float marginHeight = rect.rect.height - textRect.rect.height; // 상하 여백
|
||||
rect.sizeDelta = new Vector2(rect.rect.width, text.preferredHeight + marginHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정보 창을 숨깁니다.
|
||||
/// </summary>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace UVC.UI.Menu
|
||||
///
|
||||
/// // 기존 모델에 새로운 메뉴 아이템 추가 또는 수정
|
||||
/// // 예: '도움말' 메뉴 추가
|
||||
/// model.MenuItems.Add(new MenuItemData("help", "menu_help", subMenuItems: new List<MenuItemData>
|
||||
/// model.MenuItems.Add(new MenuItemData("help", "menu_help", subMenuItems: new List<MenuItemData>;
|
||||
/// {
|
||||
/// new MenuItemData("help_about", "menu_help_about", new DebugLogCommand("도움말 > 정보 선택됨"))
|
||||
/// }));
|
||||
|
||||
Reference in New Issue
Block a user