playback 버그 수정

This commit is contained in:
logonkhi
2025-07-28 19:59:35 +09:00
parent f5a36697ba
commit 231af33e6f
43 changed files with 537 additions and 325 deletions

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
@@ -52,8 +52,8 @@ namespace UVC.Data.Http
///
/// // 단일 요청 설정 및 등록
/// var singleRequest = new HttpRequestConfig("https://api.example.com/data")
/// .setDataMapper(dataMapper)
/// .setSuccessHandler(data => {
/// .SetDataMapper(dataMapper)
/// .SetSuccessHandler(data => {
/// // 데이터 처리 로직
/// ULog.Debug($"데이터 수신: {data?.ToString() ?? "null"}");
/// });
@@ -61,12 +61,12 @@ namespace UVC.Data.Http
///
/// // 반복 요청 설정 및 등록
/// var repeatingRequest = new HttpRequestConfig("https://api.example.com/status")
/// .setDataMapper(dataMapper)
/// .setSuccessHandler(data => {
/// .SetDataMapper(dataMapper)
/// .SetSuccessHandler(data => {
/// // 상태 데이터 처리
/// ULog.Debug($"상태 업데이트: {data?.ToString() ?? "null"}");
/// })
/// .setRepeat(true, 0, 5000); // 5초마다 무한 반복
/// .SetRepeat(true, 0, 5000); // 5초마다 무한 반복
/// httpFetcher.Add("statusMonitor", repeatingRequest);
///
/// // 응답 분할 요청 설정 (예: 응답이 {"AGV": [...], "ALARM": [...]} 형태)
@@ -77,7 +77,7 @@ namespace UVC.Data.Http
/// var alarmMapper = new DataMapper(alarmMask);
///
/// var splitRequest = new HttpRequestConfig("https://api.example.com/baseinfo")
/// .setSplitResponseByKey(true) // 응답을 키별로 분할
/// .SetSplitResponseByKey(true) // 응답을 키별로 분할
/// .AddSplitConfig("AGV", agvMapper) // "AGV" 키에 대한 매퍼 설정
/// .AddSplitConfig("ALARM", alarmMapper); // "ALARM" 키에 대한 매퍼 설정
/// httpFetcher.Add("baseInfo", splitRequest);

View File

@@ -207,7 +207,7 @@ namespace UVC.Data.Http
{
continue;
}
// 분할된 데이터를 `subKey`를 키로 사용하여 DataRepository에 저장합니다.
var repoObject = DataRepository.Instance.AddOrUpdateData(subKey, subMappedObject, info.UpdatedDataOnly);

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -18,19 +18,19 @@ namespace UVC.Data.Http
/// <example>
/// <code>
/// var config = new HttpRequestConfig("https://api.example.com/data", "GET")
/// .setDataMapper(new DataMapper(dataMask))
/// .setSuccessHandler(data => Console.WriteLine(data)) // 성공 핸들러 예시
/// .setFailHandler(errorData => Console.Error.WriteLine(errorData)) // 실패 핸들러 예시
/// .setRetry(5, 2000)
/// .setRepeat(true, 10, 5000);
/// .SetDataMapper(new DataMapper(dataMask))
/// .SetSuccessHandler(data => Console.WriteLine(data)) // 성공 핸들러 예시
/// .SetFailHandler(errorData => Console.Error.WriteLine(errorData)) // 실패 핸들러 예시
/// .SetRetry(5, 2000)
/// .SetRepeat(true, 10, 5000);
///
/// // 응답을 키별로 분할하는 설정
/// // 응답이 {"AGV": [...], "ALARM": [...]} 형태일 때 사용
/// var splitConfig = new HttpRequestConfig("https://api.example.com/alldata")
/// .setSplitResponseByKey(true) // 이 옵션을 활성화
/// .SetSplitResponseByKey(true) // 이 옵션을 활성화
/// .AddSplitConfig("AGV", new DataMapper(agvMask), agvValidator)
/// .AddSplitConfig("ALARM", new DataMapper(alarmMask))
/// .setSuccessHandler(splitData => {
/// .SetSuccessHandler(splitData => {
/// // HttpDataFetcher 구현에 따라, 분할된 각 데이터가 처리된 후 이 핸들러가 호출될 수 있습니다.
/// // 이 경우 핸들러의 IDataObject는 null일 수 있습니다.
/// Console.WriteLine("Split data processing completed.");
@@ -204,7 +204,7 @@ namespace UVC.Data.Http
/// </summary>
/// <param name="dataMapper">사용할 데이터 매퍼 객체</param>
/// <returns>현재 HttpRequestConfig 인스턴스 (메서드 체이닝용)</returns>
public HttpRequestConfig setDataMapper(DataMapper dataMapper)
public HttpRequestConfig SetDataMapper(DataMapper dataMapper)
{
_dataMapper = dataMapper;
return this;
@@ -242,18 +242,18 @@ namespace UVC.Data.Http
///
/// // 3. 검사기를 HTTP 파이프라인에 설정
/// var pipelineInfo = new HttpRequestConfig("https://api.example.com/users", "get")
/// .setDataMapper(userDataMapper)
/// .setValidator(validator)
/// .setSuccessHandler(userData => {
/// .SetDataMapper(userDataMapper)
/// .SetValidator(validator)
/// .SetSuccessHandler(userData => {
/// // 여기에 도달하는 사용자 데이터는 모두 이메일이 유효하고 18세 이상입니다.
/// Console.WriteLine($"유효한 사용자: {userData["name"]}, {userData["email"]}");
/// })
/// .setFailHandler(errorMsg => {
/// .SetFailHandler(errorMsg => {
/// Console.WriteLine($"요청 실패: {errorMsg}");
/// });
/// </code>
/// </example>
public HttpRequestConfig setValidator(DataValidator validator)
public HttpRequestConfig SetValidator(DataValidator validator)
{
this._validator = validator;
return this;
@@ -281,7 +281,7 @@ namespace UVC.Data.Http
/// </summary>
/// <param name="responseMask">HTTP response에 적용할 <see cref="HttpResponseMask"/>입니다.</param>
/// <returns>지정된 response 마스크가 적용된 업데이트된 <see cref="HttpRequestConfig"/> 인스턴스입니다.</returns>
public HttpRequestConfig setResponseMask(HttpResponseMask responseMask)
public HttpRequestConfig SetResponseMask(HttpResponseMask responseMask)
{
_responseMask = responseMask;
return this;
@@ -294,7 +294,7 @@ namespace UVC.Data.Http
/// </summary>
/// <param name="handler">응답 데이터를 처리할 콜백 함수</param>
/// <returns>현재 HttpRequestConfig 인스턴스 (메서드 체이닝용)</returns>
public HttpRequestConfig setSuccessHandler(Action<IDataObject?>? handler)
public HttpRequestConfig SetSuccessHandler(Action<IDataObject?>? handler)
{
_successhandler = handler;
return this;
@@ -306,7 +306,7 @@ namespace UVC.Data.Http
/// </summary>
/// <param name="handler">실패 정보를 처리할 콜백 함수입니다. 실패 시 관련 데이터를 인자로 받습니다.</param>
/// <returns>현재 HttpRequestConfig 인스턴스 (메서드 체이닝용)</returns>
public HttpRequestConfig setFailHandler(Action<string>? handler)
public HttpRequestConfig SetFailHandler(Action<string>? handler)
{
_failhandler = handler;
return this;
@@ -318,7 +318,7 @@ namespace UVC.Data.Http
/// <param name="maxRetryCount">최대 재시도 횟수 (기본값: 3)</param>
/// <param name="retryDelay">재시도 간 대기 시간(밀리초) (기본값: 1000)</param>
/// <returns>현재 HttpRequestConfig 인스턴스 (메서드 체이닝용)</returns>
public HttpRequestConfig setRetry(int maxRetryCount = 3, int retryDelay = 1000)
public HttpRequestConfig SetRetry(int maxRetryCount = 3, int retryDelay = 1000)
{
_maxRetryCount = maxRetryCount;
_retryDelay = retryDelay;
@@ -341,16 +341,16 @@ namespace UVC.Data.Http
/// <code>
/// // 5초마다 10번 반복 요청, 변경된 데이터만 처리
/// var pipelineInfo = new HttpRequestConfig("https://api.example.com/data", "GET")
/// .setHandler(data => ProcessData(data))
/// .setRepeat(true, 10, 5000, true);
/// .SetHandler(data => ProcessData(data))
/// .SetRepeat(true, 10, 5000, true);
///
/// // 3초마다 무한 반복, 모든 응답 데이터 처리
/// var pipelineInfo = new HttpRequestConfig("https://api.example.com/status", "GET")
/// .setHandler(data => UpdateStatus(data))
/// .setRepeat(true, 0, 3000, false);
/// .SetHandler(data => UpdateStatus(data))
/// .SetRepeat(true, 0, 3000, false);
/// </code>
/// </example>
public HttpRequestConfig setRepeat(bool repeat, int count = 0, int interval = 1000, bool updatedDataOnly = true)
public HttpRequestConfig SetRepeat(bool repeat, int count = 0, int interval = 1000, bool updatedDataOnly = true)
{
_repeat = repeat;
_repeatCount = count;
@@ -366,11 +366,20 @@ namespace UVC.Data.Http
/// </summary>
/// <param name="split">분할 처리 여부</param>
/// <returns>현재 HttpRequestConfig 인스턴스 (메서드 체이닝용)</returns>
public HttpRequestConfig setSplitResponseByKey(bool split)
public HttpRequestConfig SetSplitResponseByKey(bool split)
{
_splitResponseByKey = split;
return this;
}
/// <summary>
/// 업데이트된 데이터만 처리할지 여부를 나타내는 값을 설정합니다.
/// </summary>
/// <param name="v">부울 값. <see langword="true"/>는 업데이트된 데이터만 처리하도록 지정합니다. 그렇지 않으면
/// <see langword="false"/>입니다.</param>
public void SetUpdatedDataOnly(bool v)
{
_updatedDataOnly = v;
}
}
}

View File

@@ -47,8 +47,8 @@ namespace UVC.Data.Mqtt
///
/// // 4. MqttSubscriptionConfig 생성 및 설정
/// var pipelineInfo = new MqttSubscriptionConfig("sensor/+/data")
/// .setDataMapper(dataMapper)
/// .setHandler(dataHandler);
/// .SetDataMapper(dataMapper)
/// .SetHandler(dataHandler);
///
/// // 5. MqttDataReceiver 인스턴스 생성
/// var mqttReceiver = new MqttDataReceiver("mqtt.eclipseprojects.io", 1883);
@@ -62,7 +62,7 @@ namespace UVC.Data.Mqtt
/// // ... 애플리케이션 로직 수행 ...
///
/// // 8. 파이프라인 중지 및 리소스 해제
/// mqttReceiver.Stop();
/// mqttReceiver.Exit();
/// mqttReceiver.Dispose();
/// </code>
/// </example>

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using UVC.Data.Core;
@@ -22,8 +22,8 @@ namespace UVC.Data.Mqtt
/// dataMask["timestamp"] = DateTime.Now;
///
/// var config = new MqttSubscriptionConfig("device/status")
/// .setDataMapper(new DataMapper(dataMask))
/// .setHandler(data => Console.WriteLine(data));
/// .SetDataMapper(new DataMapper(dataMask))
/// .SetHandler(data => Console.WriteLine(data));
/// </code>
/// </example>
public class MqttSubscriptionConfig
@@ -113,7 +113,7 @@ namespace UVC.Data.Mqtt
/// 핸들러는 메시지가 수신되고 DataMapper에 의해 변환된 후 호출됩니다.
/// UpdatedDataOnly 속성이 true인 경우, 데이터가 변경된 경우에만 호출됩니다.
/// </remarks>
public MqttSubscriptionConfig setHandler(Action<IDataObject?> handler)
public MqttSubscriptionConfig SetHandler(Action<IDataObject?> handler)
{
_handler = handler;
return this;
@@ -137,8 +137,8 @@ namespace UVC.Data.Mqtt
/// dataMask["timestamp"] = ""; // 문자열 타입 지정
///
/// var pipelineInfo = new MqttSubscriptionConfig("sensor/data")
/// .setDataMapper(new DataMapper(dataMask))
/// .setHandler(data => ProcessSensorData(data));
/// .SetDataMapper(new DataMapper(dataMask))
/// .SetHandler(data => ProcessSensorData(data));
/// </code>
/// </example>
public MqttSubscriptionConfig setDataMapper(DataMapper dataMapper)
@@ -177,9 +177,9 @@ namespace UVC.Data.Mqtt
///
/// // Validator를 파이프라인에 설정
/// var pipelineInfo = new MqttSubscriptionConfig("sensors/data")
/// .setDataMapper(dataMapper)
/// .setValidator(_validator)
/// .setHandler(data => {
/// .SetDataMapper(dataMapper)
/// .SetValidator(_validator)
/// .SetHandler(data => {
/// // 여기서 처리되는 데이터는 모두 유효성 검사를 통과한 데이터
/// Console.WriteLine($"유효한 센서 데이터: {data["deviceId"]} - {data["temperature"]}°C");
/// });

View File

@@ -40,9 +40,9 @@ namespace UVC.Data.Mqtt
/// <summary>
/// 스레드를 안전하게 종료시키기 위한 CancellationTokenSource 입니다.
/// Stop() 메서드가 호출되면 취소 신호를 보냅니다.
/// Exit() 메서드가 호출되면 취소 신호를 보냅니다.
/// </summary>
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private readonly Dictionary<string, List<MqttDataPacket>> topicBuffers = new Dictionary<string, List<MqttDataPacket>>();
private readonly Dictionary<string, Action<string, List<MqttDataPacket>>> listeners = new Dictionary<string, Action<string, List<MqttDataPacket>>>();
@@ -101,6 +101,11 @@ namespace UVC.Data.Mqtt
public void Start()
{
if (isRunning) return;
if (cancellationTokenSource.IsCancellationRequested)
{
cancellationTokenSource.Dispose();
cancellationTokenSource = new CancellationTokenSource();
}
isRunning = true;
workerThread = new Thread(Run);
workerThread.IsBackground = true; // 메인 앱 종료 시 스레드 자동 종료
@@ -228,7 +233,7 @@ namespace UVC.Data.Mqtt
// 다음 전파 주기까지 대기합니다.
if (cancellationToken.WaitHandle.WaitOne(propagationIntervalMs))
{
break; // Stop() 호출 시 루프 종료
break; // Exit() 호출 시 루프 종료
}
}
catch (ObjectDisposedException) { break; }
@@ -236,7 +241,7 @@ namespace UVC.Data.Mqtt
}
/// <summary>
/// 직접 전파 모드의 메인 루프입니다. Stop()이 호출될 때까지 스레드를 대기시킵니다.
/// 직접 전파 모드의 메인 루프입니다. Exit()이 호출될 때까지 스레드를 대기시킵니다.
/// </summary>
private void RunDirectPropagationLoop()
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using UnityEngine;
using UVC.Core;
@@ -103,6 +103,8 @@ namespace UVC.Factory
private Coroutine focusCoroutine; // 현재 실행 중인 포커싱 코루틴을 저장할 변수
public bool Enable = false; // 카메라 컨트롤 활성화 여부
void Start()
{
// 스크립트 시작 시, 회전의 기준이 되는 중심점을 카메라 앞쪽으로 초기화합니다.
@@ -207,6 +209,7 @@ namespace UVC.Factory
// 이를 통해 카메라의 떨림이나 끊김 현상을 줄일 수 있습니다.
void LateUpdate()
{
if (!Enable) return; // 카메라 컨트롤이 비활성화된 경우, 업데이트를 건너뜁니다.
HandlePanning();
HandleRotation();
HandleZoom();

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
using UVC.Data.Core;
using UVC.Factory.Playback;
namespace UVC.Factory.Component
{
@@ -26,17 +27,32 @@ namespace UVC.Factory.Component
// 움직임과 회전의 부드러움을 조절할 속도 변수입니다.
// Unity 인스펙터 창에서 실시간으로 값을 조절하며 최적의 움직임을 찾을 수 있습니다.
[Tooltip("목표 지점까지의 이동 속도를 조절합니다.")]
public float moveSpeed = 1.0f;
[Tooltip("목표 지점까지 도달하는 데 걸리는 시간(초)입니다. 작을수록 빠릅니다.")]
[SerializeField]
private float moveSpeed = 0.9f;
[Tooltip("목표 방향까지의 회전 속도를 조절합니다.")]
public float rotationSpeed = 2.0f;
[Tooltip("목표 방향까지 도달하는 데 걸리는 시간(초)입니다. 작을수록 빠릅니다.")]
[SerializeField]
private float rotationSpeed = 0.5f;
[Tooltip("이 거리(미터)를 초과하면 보간 없이 즉시 위치를 변경합니다.")]
public float teleportDistanceThreshold = 5.0f; // 5미터 이상 차이나면 순간이동
[SerializeField]
private float teleportDistanceThreshold = 2.0f; // 1미터 이상 차이나면 순간이동
[Tooltip("이 각도를 초과하면 보간 없이 즉시 회전각을 변경합니다.")]
[SerializeField]
private float teleportRotationThreshold = 45.0f; // 5도 이상 차이나면 순간이동
private Renderer renderer;
private Renderer? renderer;
private bool isRed = false;
private float timeScale = 1.0f; // 현재 시간 스케일, 기본값은 1.0f (실시간)
// SmoothDamp 함수가 사용하는 현재 속도 값입니다. 0으로 초기화해야 합니다.
private Vector3 velocity = Vector3.zero;
private float angularVelocity = 0;
/// <summary>
/// AGV 객체가 생성될 때 처음 한 번 호출되는 초기화 메서드입니다.
@@ -61,6 +77,18 @@ namespace UVC.Factory.Component
"JOB_ID",
"TIMESTAMP",
};
PlaybackService.Instance.OnChangeTimeScale += OnChangeTimeScaleHandler;
}
private void OnChangeTimeScaleHandler(float timeScale)
{
this.timeScale = timeScale;
}
protected override void OnDestroy()
{
PlaybackService.Instance.OnChangeTimeScale -= OnChangeTimeScaleHandler;
base.OnDestroy();
}
/// <summary>
@@ -118,8 +146,6 @@ namespace UVC.Factory.Component
bool changed = false;
bool isTeleport = false;
float? newX = newData.GetFloat("X");
float? newY = newData.GetFloat("Y");
float x = data.GetFloat("X").Value;
@@ -137,7 +163,7 @@ namespace UVC.Factory.Component
if (distanceToTarget > teleportDistanceThreshold)
{
transform.position = newTargetPosition;
isTeleport = true; // 순간이동이 발생했음을 표시합니다.
velocity = Vector3.zero; // 순간이동 후 속도 초기화
}
// 새로운 목표 지점을 설정합니다.
@@ -152,8 +178,18 @@ namespace UVC.Factory.Component
{
Quaternion newTargetRotation = Quaternion.Euler(0, newDegree.Value, 0);
// 거리가 설정된 임계값을 초과하면, 보간을 건너뛰고 즉시 위치/회전을 설정합니다
if (isTeleport) transform.rotation = newTargetRotation;
float distanceToTargetRotation = Quaternion.Angle(transform.rotation, newTargetRotation);
// 현재 회전과 새로운 목표 회전 사이의 각도 차이를 계산합니다.
if (distanceToTargetRotation > 0)
{
// 각도 차이가 설정된 임계값을 초과하면, 보간을 건너뛰고 즉시 회전을 설정합니다.
if (distanceToTargetRotation > teleportRotationThreshold)
{
transform.rotation = newTargetRotation;
angularVelocity = 0f;
}
}
// 새로운 목표 지점을 설정합니다.
// (순간이동을 했든 안 했든, 다음 프레임부터의 보간을 위해 목표 지점은 항상 갱신되어야 합니다.)
@@ -174,7 +210,9 @@ namespace UVC.Factory.Component
private void ChangeColor(Color color)
{
renderer.material.color = color;
if(color == Color.red && isRed && renderer != null) return; // 이미 빨간색이면 변경하지 않음
isRed = color == Color.red;
if(renderer != null) renderer!.material.color = color;
}
/// <summary>
@@ -183,36 +221,41 @@ namespace UVC.Factory.Component
/// </summary>
void Update()
{
// 현재 위치가 목표 위치와 다를 경우에만 이동 로직을 실행합니다.
if (transform.position != targetPosition)
bool isMoving = Vector3.SqrMagnitude(transform.position - targetPosition) > 0.001f;
bool isRotating = Quaternion.Angle(transform.rotation, targetRotation) > 0.01f;
// 이동과 회전이 모두 끝났으면 더 이상 계산할 필요가 없습니다.
if (!isMoving && !isRotating)
{
// 목표 지점과의 거리가 매우 가까우면 (0.01미터 미만) 그냥 목표 위치로 설정하여 미세한 떨림을 방지합니다.
if (Vector3.Distance(transform.position, targetPosition) < 0.01f)
{
// 현재 위치와 목표 위치 사이의 거리가 임계값을 초과하면 순간이동합니다.
transform.position = targetPosition;
}
else
{
// Vector3.Lerp를 사용하여 현재 위치에서 목표 위치로 부드럽게 이동시킵니다.
// Time.deltaTime * moveSpeed는 프레임 속도에 관계없이 일정한 속도를 보장합니다.
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * moveSpeed);
}
return;
}
// 현재 회전이 목표 회전과 다를 경우에만 회전 로직을 실행합니다.
if (transform.rotation != targetRotation)
float dampedTime = Time.deltaTime * timeScale;
if (dampedTime <= 0) return;
// 위치 업데이트
if (isMoving)
{
// 목표 회전과의 각도 차이가 매우 작으면 (0.1도 미만) 그냥 목표 회전으로 설정합니다.
if (Quaternion.Angle(transform.rotation, targetRotation) < 0.01f)
{
transform.rotation = targetRotation;
}
else
{
// Quaternion.Slerp를 사용하여 현재 회전에서 목표 회전으로 부드럽게 회전시킵니다.
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotationSpeed);
}
transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, moveSpeed, Mathf.Infinity, dampedTime);
}
else
{
transform.position = targetPosition;
velocity = Vector3.zero;
}
// 회전 업데이트
if (isRotating)
{
float currentAngle = transform.eulerAngles.y;
float targetAngle = targetRotation.eulerAngles.y;
float newAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angularVelocity, rotationSpeed, Mathf.Infinity, dampedTime);
transform.rotation = Quaternion.Euler(0, newAngle, 0);
}
else
{
transform.rotation = targetRotation;
angularVelocity = 0f;
}
}
}

View File

@@ -100,6 +100,8 @@ namespace UVC.Factory.Component
private bool created = false;
public bool Created => created;
/// <summary>
/// AGVManager의 초기화 메서드입니다.
/// Awake 메서드에서 호출되며, MonoBehaviour가 생성될 때 한 번만 실행됩니다.
@@ -168,7 +170,7 @@ namespace UVC.Factory.Component
var RemovedItems = arr.RemovedItems;
var ModifiedList = arr.ModifiedList;
//Debug.Log($"AGVManager received data: count:{arr.Count}, Added={AddedItems.Count}, Removed={RemovedItems.Count}, Modified={ModifiedList.Count}");
Debug.Log($"AGVManager received data: count:{arr.Count}, Added={AddedItems.Count}, Removed={RemovedItems.Count}, Modified={ModifiedList.Count}");
// 새로 추가된 AGV 처리
foreach (var item in AddedItems.ToList())

View File

@@ -1,3 +1,4 @@
#nullable enable
using UnityEngine;
using UVC.Data;
using UVC.Factory.Playback.UI;
@@ -9,9 +10,9 @@ namespace UVC.Factory.Playback
{
public class PlaybackCommand : ICommand
{
public async void Execute(object parameter = null)
public async void Execute(object? parameter = null)
{
FactoryCameraController.Instance.Enable = false;
var modalContent = new ModalContent(UIPlaybackListModal.PrefabPath)
{
Title = "Playback List",
@@ -19,21 +20,24 @@ namespace UVC.Factory.Playback
ShowCancelButton = false
};
object result = await UVC.UI.Modal.Modal.Open<bool>(modalContent);
UIPlaybackListItemData? result = await UVC.UI.Modal.Modal.Open<UIPlaybackListItemData>(modalContent);
Debug.Log($"PlaybackCommand result:{result}");
Debug.Log($"PlaybackCommand result==null:{result==null}");
if (result != null)
{
UIPlaybackListItemData data = (UIPlaybackListItemData)result;
UILoading.Show();
UIPlaybackListItemData data = result;
Debug.Log($"PlaybackCommand data:{data}");
UIPlayback.Instance.Show();
DataRepository.Instance.MqttReceiver.Stop();
await UIPlayback.Instance.SetData(data.date, data.time, data.sqlFileName);
await PlaybackService.Instance.StartAsync(data);
FactoryCameraController.Instance.Enable = true;
UILoading.Hide();
}
else
{
UILoading.Show();
PlaybackService.Instance.Stop();
PlaybackService.Instance.Exit();
FactoryCameraController.Instance.Enable = true;
UILoading.Hide();
}

View File

@@ -49,7 +49,7 @@ namespace UVC.Factory.Playback
{
try
{
return HttpRequester.Download($"{Constants.PlaybackDomain}/playback/{fileName}", savePath, OnComplete, OnProgress, OnError);
return HttpRequester.Download($"{URLList.Get("playbackFile")}/{fileName}", savePath, OnComplete, OnProgress, OnError);
}
catch (Exception e)
{

View File

@@ -1,10 +1,11 @@
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks;
using SQLite4Unity3d;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UVC.Factory.Playback;
using UVC.Util;
namespace UVC.Factory
@@ -32,7 +33,7 @@ namespace UVC.Factory
{
this.date = date;
this.sqliteFileName = sqliteFileName;
dbConnection = new SQLiteConnection(Path.Combine(Application.streamingAssetsPath, "playback", date, sqliteFileName));
dbConnection = new SQLiteConnection(Path.Combine(PlaybackService.PlaybackFolderPath, date, sqliteFileName));
}
public void CloseDB()
@@ -78,6 +79,7 @@ namespace UVC.Factory
};
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecond(string selectTime, int second, bool orderAsc = true, int limit = 0)
{
bool isMainThread = PlayerLoopHelper.IsMainThread;
List<PlaybackSQLiteDataEntity> result = await UniTask.RunOnThreadPool(() =>
{
DateTime date = DateTimeUtil.UtcParse(selectTime).AddSeconds(second);
@@ -104,6 +106,7 @@ namespace UVC.Factory
queryBuilder.Clear();
return dbConnection.Query<PlaybackSQLiteDataEntity>(query);
});
if (!isMainThread) await UniTask.SwitchToThreadPool();
return result;
}
@@ -122,6 +125,7 @@ namespace UVC.Factory
StringBuilder queryBuilder = new();
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecondBaseInfo(string selectTime, int second, bool orderAsc = false, int limit = 1)
{
bool isMainThread = PlayerLoopHelper.IsMainThread;
List<PlaybackSQLiteDataEntity> result = await UniTask.RunOnThreadPool(() =>
{
DateTime date = DateTimeUtil.UtcParse(selectTime).AddSeconds(second);
@@ -146,7 +150,7 @@ namespace UVC.Factory
queryBuilder.Clear();
return dbConnection.Query<PlaybackSQLiteDataEntity>(query);
});
if (!isMainThread) await UniTask.SwitchToThreadPool();
return result;
}
}

View File

@@ -1,14 +1,14 @@
#nullable enable
#nullable enable
using Best.HTTP;
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
using UVC.Data;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Json;
using UVC.Factory.Playback.UI;
using UVC.Util;
namespace UVC.Factory.Playback
{
@@ -20,14 +20,33 @@ namespace UVC.Factory.Playback
static PlaybackService() { }
#endregion
public static readonly string PlaybackFolderPath = Path.Combine(Application.persistentDataPath, "playback");//streamingAssetsPath, "playback"); appData 폴더로 변경
private readonly PlaybackRepository repository;
private string date;
private string time;
private string fileName;
public Action OnStopPlayback;
public Action OnExitPlayback;
private float timeScale = 1.0f;
public float TimeScale
{
get => timeScale;
internal set
{
if (value < 1f) value = 1f;
if (timeScale != value)
{
timeScale = value;
//Time.timeScale = timeScale;
OnChangeTimeScale?.Invoke(timeScale);
}
}
}
public Action<float> OnChangeTimeScale;
public PlaybackService(PlaybackRepository repository)
{
@@ -42,28 +61,29 @@ namespace UVC.Factory.Playback
public async UniTask DispatchBaseInfoData(string date, string time, string fileName, string minute = "00", string second = "00")
{
Debug.Log($"DispatchBaseInfoData {date} {time} {minute} {second} {fileName}");
//헝가리 시간임
this.date = date;
this.time = time;
this.fileName = fileName;
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:{minute}:{second}.000Z");
string formatTime = DateTimeUtil.FormatTime(dateTime);
//baseInfo 가져오기
List<PlaybackSQLiteDataEntity> list = await repository.SelectBySecondBaseInfo(date, fileName, formatTime);
if (list.Count > 0)
await UniTask.RunOnThreadPool(async () =>
{
HttpRequestConfig httpRequestConfig = new HttpRequestConfig("");
httpRequestConfig.setSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
//헝가리 시간임
this.date = date;
this.time = time;
this.fileName = fileName;
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:{minute}:{second}.000Z");
string formatTime = DateTimeUtil.FormatTime(dateTime);
//baseInfo 가져오기
List <PlaybackSQLiteDataEntity> list = await repository.SelectBySecondBaseInfo(date, fileName, formatTime);
if (list.Count > 0)
{
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
HttpRequestConfig httpRequestConfig = new HttpRequestConfig("");
httpRequestConfig.SetUpdatedDataOnly(true);
httpRequestConfig.SetSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
}
}
}
});
}
/// <summary>
@@ -72,45 +92,55 @@ namespace UVC.Factory.Playback
/// <param name="second">0 ~ 3600</param>
public async UniTask DispatchRealTimeData(int second, int speed)
{
int newSecond = second;
if (newSecond > 36000) newSecond = 36000;
//utc 시간으로 변환
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:00:00.000Z").AddSeconds(newSecond);//.Add(-DateTimeUtil.UtcKoreaGap);
string formatTime = DateTimeUtil.FormatTime(dateTime);
List<PlaybackSQLiteDataEntity> list = await repository.SelectBySecondAsync(date, fileName, formatTime, 1);
if (list.Count > 0)
await UniTask.RunOnThreadPool(async () =>
{
HttpRequestConfig httpRequestConfig = new HttpRequestConfig("");
httpRequestConfig.setSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
int newSecond = second;
if (newSecond > 36000) newSecond = 36000;
//utc 시간으로 변환
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:00:00.000Z").AddSeconds(newSecond);//.Add(-DateTimeUtil.UtcKoreaGap);
string formatTime = DateTimeUtil.FormatTime(dateTime);
List<PlaybackSQLiteDataEntity> list = await repository.SelectBySecondAsync(date, fileName, formatTime, 1);
//Debug.Log($"DispatchRealTimeData {date} {time} {formatTime} {newSecond} {list.Count}");
if (list.Count > 0)
{
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
HttpRequestConfig httpRequestConfig = new HttpRequestConfig("");
httpRequestConfig.SetUpdatedDataOnly(true);
httpRequestConfig.SetSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
}
}
}
});
}
public async Task StartAsync(UIPlaybackListItemData data)
{
timeScale = 1.0f; //기본 시간 스케일 설정
UIPlayback.Instance.Show();
await UIPlayback.Instance.SetData(data.date, data.time, data.sqlFileName);
}
public void Stop()
public void Exit()
{
OnStopPlayback?.Invoke();
OnExitPlayback?.Invoke();
}
public HTTPRequest? ReadyData(string date, string time, string fileName, Action<long, long, float> OnProgress, Action<string> OnComplete)
{
//date : "2024-12-05"
//fileName : "2024-12-05_0.sqlite.7z"
string playbackPath = Path.Combine(Application.streamingAssetsPath, "playback");
string playbackPath = PlaybackService.PlaybackFolderPath;
string tempPath = Path.Combine(playbackPath, "temp");//한국 시간으로 변경하기 때문에 임시 폴더 만들어서 압축 해제 후 이동
string datePath = Path.Combine(playbackPath, date);
var fileNameArr = fileName.Split(".");
string zipFilePath = Path.Combine(datePath, fileName);
string sqlFilePath = Path.Combine(datePath, fileNameArr[0] + ".sqlite");
DateTime utcDateTime = DateTimeUtil.Parse(fileNameArr[0], "yyyy-MM-dd_H").Add(-DateTimeUtil.UtcKoreaGap);
DateTime utcDateTime = DateTimeUtil.Parse(fileNameArr[0], "yyyy-MM-dd_H");//.Add(-DateTimeUtil.UtcKoreaGap);
string utcDatePath = Path.Combine(playbackPath, utcDateTime.ToString("yyyy-MM-dd"));
string utcFileName = utcDateTime.ToString("yyyy-MM-dd_H") + "." + fileNameArr[1] + "." + fileNameArr[2];
var utcFileNameArr = utcFileName.Split(".");
@@ -144,15 +174,22 @@ namespace UVC.Factory.Playback
if (File.Exists(utcZipFilePath))
{
if (OnProgress != null) OnProgress.Invoke(50, 100, 0.5f);
//압축해제 후
//압축해제 후
var zipper = new Zipper();
string errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
{
if (OnProgress != null)
{
bool isComplte = false;
float percentRate = 0.5f + percent / 2;
if (percentRate > 0.99)
{
percentRate = 0.99f;
isComplte = true;
}
OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
if (0.5f + percent / 2 > 100f)
if (isComplte)
{
Debug.Log($" DownloadReadData :{downloadTotal + read} , DownloadTotalData :{downloadTotal + total} ,DownloadPlaybackData OnProgress:{percent}");
}
@@ -181,16 +218,20 @@ namespace UVC.Factory.Playback
// Debug.Log($"zipper3 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
//}
//압축해제 한 파일 이동
if (File.Exists(utcSqlFilePath))
await UniTask.RunOnThreadPool(() =>
{
//동일한 파일명이 있을경우 제거후 다시
File.Copy(utcSqlFilePath, sqlFilePath);
File.Delete(utcSqlFilePath);
}
//압축해제 한 파일 이동
if (File.Exists(utcSqlFilePath))
{
//동일한 파일명이 있을경우 제거후 다시
File.Copy(utcSqlFilePath, sqlFilePath);
File.Delete(utcSqlFilePath);
}
//zip 파일 삭제
File.Delete(utcZipFilePath);
});
//zip 파일 삭제
File.Delete(utcZipFilePath);
if (OnComplete != null) OnComplete.Invoke(errorMessage);
}
},

View File

@@ -61,6 +61,8 @@ namespace UVC.Factory.Playback.UI
private UIPlaybackProgressBar progressBar;
[SerializeField]
private CanvasGroup canvasGroup;
[SerializeField]
private UIDragger uiDragger;
private bool isPlaying = false;
private bool preparingData = false;
@@ -118,6 +120,7 @@ namespace UVC.Factory.Playback.UI
if (canvas.name == "ModalCanvas")
{
transform.SetParent(canvas.transform, false);
uiDragger.SetDragArea(canvas.transform as RectTransform);
break;
}
}
@@ -126,7 +129,7 @@ namespace UVC.Factory.Playback.UI
public void Hide()
{
Time.timeScale = 1;
UpdateTimeScale(1);
IsTick = false;
gameObject.SetActive(false);
}
@@ -137,8 +140,8 @@ namespace UVC.Factory.Playback.UI
isPlaying = false;
UpdatePlayState();
Hide();
PlaybackService.Instance.Stop();
UILoading.Hide();
PlaybackService.Instance.Exit();
}
private void OnClickPlay()
@@ -156,11 +159,12 @@ namespace UVC.Factory.Playback.UI
{
if (isPlaying)
{
if (Time.timeScale != sliderSpeed.Value) Time.timeScale = sliderSpeed.Value;
//if (Time.timeScale != sliderSpeed.Value) UpdateTimeScale(sliderSpeed.Value);
UpdateTimeScale(sliderSpeed.Value);
}
else
{
Time.timeScale = 1;
UpdateTimeScale(1);
}
}
@@ -208,24 +212,23 @@ namespace UVC.Factory.Playback.UI
this.time = time;
this.fileName = fileName;
Debug.Log($"UIPlayback SetData {date} {time}");
int timeInt = int.Parse(time);
dateTimeTxt0.text = dateTimeTxt1.text = date.Substring(2).Replace("-", ".");
progressBar.Init(timeInt);
sliderSpeed.Init();
Time.timeScale = 1;
UpdateTimeScale(1);
canvasGroup.alpha = opacitySlider.value = 1;
preparingData = true;
progressBar.Interactable = !preparingData;
isPlaying = false;
UpdatePlayState();
//UILoading.Show();
await UniTask.WaitForSeconds(0.5f);
await PlaybackService.Instance.DispatchBaseInfoData(date, time, fileName);
preparingData = false;
progressBar.Interactable = !preparingData;
//UILoading.Hide();
await UniTask.WaitForSeconds(0.5f);
}
private void UpdatePlayState()
@@ -249,7 +252,6 @@ namespace UVC.Factory.Playback.UI
}
progressBar.Value += 1;
//PlaybackService.Instance.DispatchingTimelineEvent = false;
PlaybackService.Instance.DispatchRealTimeData(progressBar.Value, sliderSpeed.Value).Forget();
if (isTick)
@@ -260,6 +262,11 @@ namespace UVC.Factory.Playback.UI
}
}
private void UpdateTimeScale(float timeScale)
{
PlaybackService.Instance.TimeScale = timeScale;
}
}
}

View File

@@ -1,4 +1,4 @@
using Best.HTTP;
using Best.HTTP;
using System;
using System.Collections.Generic;
using System.IO;
@@ -210,7 +210,7 @@ namespace UVC.Factory.Playback.UI
{
try
{
string playbackPath = Path.Combine(Application.streamingAssetsPath, "playback");
string playbackPath = PlaybackService.PlaybackFolderPath;
string tempPath = Path.Combine(playbackPath, "temp");
string datePath = Path.Combine(playbackPath, data.date);
var fileNameArr = data.zipFileName.Split('.');

View File

@@ -1,3 +1,4 @@
#nullable enable
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
@@ -19,23 +20,24 @@ namespace UVC.Factory.Playback.UI
public static UIPlaybackListModal CreateFromPrefab(Transform parent = null)
{
GameObject prefab = Resources.Load(PrefabPath, typeof(GameObject)) as GameObject;
GameObject prefab = Resources.Load<GameObject>(PrefabPath);
GameObject go = UnityEngine.Object.Instantiate(prefab);
UIPlaybackListModal modal = go.GetComponent<UIPlaybackListModal>();
return modal;
}
private Dictionary<string, List<UIPlaybackListItemData>> data;
private Dictionary<string, List<UIPlaybackListItemData>>? data;
public bool IsOkable => (selectedItem != null && selectedItem.status == UIPlaybackListItemStatus.Downloaded && UIPlaybackListItem.DownloadingItems.Count == 0);
private UIPlaybackListItemData? selectedItem;
private UIPlaybackListItemData? selectedItem = null;
[SerializeField]
private TMP_Dropdown dropdownDate;
[SerializeField]
private ScrollRect scrollRectTime;
private UIPlaybackListItemData? resultData = null;
public override async UniTask OnOpen(ModalContent content)
{
@@ -44,17 +46,15 @@ namespace UVC.Factory.Playback.UI
initContent();
}
public override object GetResult()
public override object? GetResult()
{
if (data != null) data.Clear();
data = null;
return selectedItem;
return resultData;
}
public override async UniTask OnClose(ModalContent content)
{
await base.OnClose(content);
}
@@ -63,7 +63,7 @@ namespace UVC.Factory.Playback.UI
{
confirmButton.interactable = false;
Dictionary<string, Dictionary<string, string>> data = await PlaybackService.Instance.RequestDataAsync();
Dictionary<string, Dictionary<string, string>>? data = await PlaybackService.Instance.RequestDataAsync();
dropdownDate.onValueChanged.AddListener(DropDownDateChanged);
LocalSetData();
if (data != null) SetData(data);
@@ -81,7 +81,7 @@ namespace UVC.Factory.Playback.UI
Dictionary<string, List<UIPlaybackListItemData>> newData = new Dictionary<string, List<UIPlaybackListItemData>>();
var dateList = new List<TMP_Dropdown.OptionData>();
string playbackPath = Path.Combine(Application.streamingAssetsPath, "playback");
string playbackPath = PlaybackService.PlaybackFolderPath;
DirectoryInfo di = new DirectoryInfo(playbackPath);
if (di.Exists)
{
@@ -171,7 +171,7 @@ namespace UVC.Factory.Playback.UI
var dateList = new List<TMP_Dropdown.OptionData>();
//로컬에 저장 되 있는데 sqlite 파일 찾아서 추가
string playbackPath = Path.Combine(Application.streamingAssetsPath, "playback");
string playbackPath = PlaybackService.PlaybackFolderPath;
DirectoryInfo di = new DirectoryInfo(playbackPath);
if (di.Exists)
{
@@ -307,5 +307,10 @@ namespace UVC.Factory.Playback.UI
confirmButton.interactable = IsOkable;
}
public override void OnConfirmButtonClicked()
{
resultData = selectedItem;
}
}
}

View File

@@ -505,8 +505,11 @@ namespace UVC.Network
}
break;
default:
ULog.Error(req.State.ToString(), new Exception(resp.Message));
OnError?.Invoke(req.State.ToString());
string detailedError = req.Exception != null ? req.Exception.ToString() : (resp != null ? resp.Message : "Unknown error");
string errorMsgDefault = $"Request failed! State: {req.State}, URL: {req.CurrentUri}, Error: {detailedError}";
ULog.Error(errorMsgDefault, req.Exception);
//ULog.Error(req.State.ToString(), req.Exception != null ? req.Exception: new Exception(resp?.Message));
OnError?.Invoke(errorMsgDefault);
break;
}
};

View File

@@ -250,8 +250,8 @@ namespace UVC.Tests.Data
// HttpRequestConfig 설정
var info = new HttpRequestConfig("http://test.com")
.setDataMapper(dataMapper)
.setSuccessHandler((data) =>
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) =>
{
Debug.Log("핸들러 호출됨");
handlerCalled = true;
@@ -308,8 +308,8 @@ namespace UVC.Tests.Data
// HttpRequestConfig 설정
var info = new HttpRequestConfig("http://test.com")
.setDataMapper(dataMapper)
.setSuccessHandler((data) =>
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) =>
{
handlerCalled = true;
receivedData = data;
@@ -385,8 +385,8 @@ namespace UVC.Tests.Data
// HttpRequestConfig 설정
var info = new HttpRequestConfig(agvUrl, "get")
.setDataMapper(dataMapper)
.setSuccessHandler((data) =>
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) =>
{
handlerCalled = true;
receivedData = data;
@@ -439,8 +439,8 @@ namespace UVC.Tests.Data
// HttpRequestConfig 설정
var info = new HttpRequestConfig(alarmUrl, "get")
.setDataMapper(dataMapper)
.setSuccessHandler((data) =>
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) =>
{
handlerCalled = true;
receivedData = data;
@@ -522,8 +522,8 @@ namespace UVC.Tests.Data
{
string key = item.Key;
var info = new HttpRequestConfig(item.Value, "get")
.setDataMapper(new DataMapper(dataMasks[key]))
.setSuccessHandler((data) =>
.SetDataMapper(new DataMapper(dataMasks[key]))
.SetSuccessHandler((data) =>
{
handlerCallCount++;
results[key] = data;
@@ -573,8 +573,8 @@ namespace UVC.Tests.Data
// HttpRequestConfig 설정
var info = new HttpRequestConfig(testUrl, "get")
.setDataMapper(dataMapper)
.setSuccessHandler((data) =>
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) =>
{
handlerCalled = true;
receivedData = data;
@@ -651,8 +651,8 @@ namespace UVC.Tests.Data
// HttpRequestConfig 설정
var info = new HttpRequestConfig(baseInfoUrl, "get")
.setDataMapper(dataMapper)
.setSuccessHandler((data) =>
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) =>
{
handlerCalled = true;
receivedData = data;
@@ -737,8 +737,8 @@ namespace UVC.Tests.Data
// 반복 실행 설정을 포함한 HttpRequestConfig 생성
var info = new HttpRequestConfig(testUrl, "get")
.setDataMapper(dataMapper)
.setSuccessHandler(async (data) =>
.SetDataMapper(dataMapper)
.SetSuccessHandler(async (data) =>
{
handlerCallCount++;
if (data is DataObject dataObject)
@@ -756,7 +756,7 @@ namespace UVC.Tests.Data
MockHttpRequester.SetResponse(testUrl, mockResponses[handlerCallCount]);
}
})
.setRepeat(true, expectedCallCount, repeatInterval, false);
.SetRepeat(true, expectedCallCount, repeatInterval, false);
pipeLine.UseMockup = true;
pipeLine.Add("repeatTest", info);
@@ -811,9 +811,9 @@ namespace UVC.Tests.Data
// 무한 반복 설정을 포함한 HttpRequestConfig 생성
var info = new HttpRequestConfig(testUrl, "get")
.setDataMapper(dataMapper)
.setSuccessHandler((data) => { handlerCallCount++; })
.setRepeat(true, 0, repeatInterval, false); // 무한 반복 (repeatCount = 0)
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) => { handlerCallCount++; })
.SetRepeat(true, 0, repeatInterval, false); // 무한 반복 (repeatCount = 0)
pipeLine.UseMockup = true;
pipeLine.Add("infiniteRepeatTest", info);
@@ -879,14 +879,14 @@ namespace UVC.Tests.Data
// 두 개의 반복 요청 설정
var info1 = new HttpRequestConfig(testUrl1, "get")
.setDataMapper(dataMapper)
.setSuccessHandler((data) => { handlerCallCount1++; })
.setRepeat(true, 0, repeatInterval1, false);
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) => { handlerCallCount1++; })
.SetRepeat(true, 0, repeatInterval1, false);
var info2 = new HttpRequestConfig(testUrl2, "get")
.setDataMapper(dataMapper)
.setSuccessHandler((data) => { handlerCallCount2++; })
.setRepeat(true, 0, repeatInterval2, false);
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) => { handlerCallCount2++; })
.SetRepeat(true, 0, repeatInterval2, false);
pipeLine.UseMockup = true;
pipeLine.Add("repeatTest1", info1);
@@ -961,13 +961,13 @@ namespace UVC.Tests.Data
// 반복 횟수가 지정된 HttpRequestConfig 생성
var info = new HttpRequestConfig(testUrl, "get")
.setDataMapper(dataMapper)
.setSuccessHandler((data) =>
.SetDataMapper(dataMapper)
.SetSuccessHandler((data) =>
{
handlerCallCount++;
receivedData.Add(data);
})
.setRepeat(true, repeatCount, repeatInterval, false);
.SetRepeat(true, repeatCount, repeatInterval, false);
pipeLine.UseMockup = true;
pipeLine.Add("countedRepeatTest", info);
@@ -1162,14 +1162,14 @@ namespace UVC.Tests.Data
});
var info = new HttpRequestConfig(testUrl)
.setDataMapper(dataMapper)
.setValidator(validator)
.setSuccessHandler(data =>
.SetDataMapper(dataMapper)
.SetValidator(validator)
.SetSuccessHandler(data =>
{
handlerCalled = true;
receivedData = data;
})
.setFailHandler((message) =>
.SetFailHandler((message) =>
{
Debug.LogError("Fail message: " + message);
});
@@ -1212,9 +1212,9 @@ namespace UVC.Tests.Data
validator.AddValidator("status", value => value is string s && s == "active");
var info = new HttpRequestConfig(testUrl)
.setDataMapper(dataMapper)
.setValidator(validator)
.setSuccessHandler(data =>
.SetDataMapper(dataMapper)
.SetValidator(validator)
.SetSuccessHandler(data =>
{
handlerCalled = true; // 이 핸들러는 호출되지 않아야 함
});
@@ -1266,9 +1266,9 @@ namespace UVC.Tests.Data
});
var info = new HttpRequestConfig(testUrl)
.setDataMapper(dataMapper)
.setValidator(validator)
.setSuccessHandler(data =>
.SetDataMapper(dataMapper)
.SetValidator(validator)
.SetSuccessHandler(data =>
{
handlerCalled = true;
receivedData = data;

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Cysharp.Threading.Tasks;
using NUnit.Framework;
@@ -284,7 +284,7 @@ namespace UVC.Tests.Data
var pipelineInfo = new MqttSubscriptionConfig(topic, updatedDataOnly)
.setDataMapper(new DataMapper(dataMasks[topic]))
.setHandler(handlers[topic].HandleData);
.SetHandler(handlers[topic].HandleData);
mqttReceiver.Add(pipelineInfo);
}
@@ -315,7 +315,7 @@ namespace UVC.Tests.Data
// AGV 토픽만 등록
var agvInfo = new MqttSubscriptionConfig("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
.SetHandler(handlers["AGV"].HandleData);
mqttReceiver.Add(agvInfo);
mqttReceiver.Start();
@@ -350,7 +350,7 @@ namespace UVC.Tests.Data
// UpdatedDataOnly가 true인 AGV 토픽 추가
var agvInfo = new MqttSubscriptionConfig("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
.SetHandler(handlers["AGV"].HandleData);
testPipeLine.Add(agvInfo);
@@ -382,7 +382,7 @@ namespace UVC.Tests.Data
bool updatedDataOnly = topic != "ALL";
var pipelineInfo = new MqttSubscriptionConfig(topic, updatedDataOnly)
.setDataMapper(new DataMapper(dataMasks[topic]))
.setHandler(handlers[topic].HandleData);
.SetHandler(handlers[topic].HandleData);
testPipeLine.Add(pipelineInfo);
}
@@ -410,7 +410,7 @@ namespace UVC.Tests.Data
var pipelineInfo = new MqttSubscriptionConfig("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
.SetHandler(handlers["AGV"].HandleData);
testPipeLine.Add(pipelineInfo);
@@ -434,7 +434,7 @@ namespace UVC.Tests.Data
var pipelineInfo = new MqttSubscriptionConfig("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
.SetHandler(handlers["AGV"].HandleData);
testPipeLine.Add(pipelineInfo);
@@ -453,7 +453,7 @@ namespace UVC.Tests.Data
var pipelineInfo = new MqttSubscriptionConfig("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
.SetHandler(handlers["AGV"].HandleData);
testPipeLine.Add(pipelineInfo);
@@ -484,7 +484,7 @@ namespace UVC.Tests.Data
// 4. UpdatedDataOnly=true로 토픽 등록
var pipelineInfo = new MqttSubscriptionConfig("AGV", true)
.setDataMapper(new DataMapper(dataMask))
.setHandler(handler.HandleData);
.SetHandler(handler.HandleData);
pipeline.Add(pipelineInfo);
@@ -599,7 +599,7 @@ namespace UVC.Tests.Data
var pipelineInfo = new MqttSubscriptionConfig("test_topic")
.setDataMapper(dataMapper)
.setValidator(validator)
.setHandler(handler.HandleData);
.SetHandler(handler.HandleData);
testPipeLine.Add(pipelineInfo);
@@ -638,7 +638,7 @@ namespace UVC.Tests.Data
var pipelineInfo = new MqttSubscriptionConfig("test_topic")
.setDataMapper(dataMapper)
.setValidator(validator)
.setHandler(handler.HandleData);
.SetHandler(handler.HandleData);
testPipeLine.Add(pipelineInfo);
@@ -675,7 +675,7 @@ namespace UVC.Tests.Data
var pipelineInfo = new MqttSubscriptionConfig("test_topic")
.setDataMapper(dataMapper)
.setValidator(validator)
.setHandler(handler.HandleData);
.SetHandler(handler.HandleData);
testPipeLine.Add(pipelineInfo);

View File

@@ -1,24 +1,25 @@
using System;
#nullable enable
using System;
using UnityEngine;
namespace UVC.UI.Commands
{
public class ActionCommand : ICommand
{
private readonly Action _action;
private readonly Action<object> _actionWithParam;
private readonly Action? _action;
private readonly Action<object?>? _actionWithParam;
public ActionCommand(Action action)
{
_action = action ?? throw new ArgumentNullException(nameof(action));
}
public ActionCommand(Action<object> actionWithParam)
public ActionCommand(Action<object?> actionWithParam)
{
_actionWithParam = actionWithParam ?? throw new ArgumentNullException(nameof(actionWithParam));
}
public void Execute(object parameter = null)
public void Execute(object? parameter = null)
{
_action?.Invoke();
_actionWithParam?.Invoke(parameter);
@@ -31,7 +32,7 @@ namespace UVC.UI.Commands
// 또는, ICommand<T> 인터페이스를 고려할 수도 있습니다 (아래 2번 방법).
public class ActionCommand<T> : ICommand<T> // ICommand<T>를 구현
{
private readonly Action<T> _action;
private readonly Action<T>? _action;
private readonly T _defaultParameter;
private bool _useDefaultParameterForParameterlessExecute;
@@ -39,7 +40,7 @@ namespace UVC.UI.Commands
{
_action = action ?? throw new ArgumentNullException(nameof(action));
_useDefaultParameterForParameterlessExecute = true; // 기본적으로 default(T) 사용
_defaultParameter = default(T);
_defaultParameter = default(T)!;
}
public ActionCommand(Action<T> action, T defaultParameter, bool useDefaultForParameterless = true)
@@ -52,7 +53,7 @@ namespace UVC.UI.Commands
// ICommand<T>의 Start(T parameter) 구현
public void Execute(T parameter)
{
_action.Invoke(parameter);
_action?.Invoke(parameter);
}
// ICommand<T> 인터페이스에 의해 추가된 파라미터 없는 Start()
@@ -75,7 +76,7 @@ namespace UVC.UI.Commands
// ICommand의 Start(object parameter = null) 구현
void ICommand.Execute(object parameter) // 명시적 인터페이스 구현
void ICommand.Execute(object? parameter) // 명시적 인터페이스 구현
{
if (parameter is T typedParameter)
{
@@ -91,7 +92,7 @@ namespace UVC.UI.Commands
else
{
// T가 참조 타입이면 default(T)는 null. 값 타입이면 0, false 등.
Execute(default(T));
Execute(default(T)!);
}
}
else

View File

@@ -1,6 +1,6 @@
using UnityEngine;
#nullable enable
using UnityEngine;
using UVC.Locale;
using UVC.Log;
namespace UVC.UI.Commands
{
@@ -15,7 +15,7 @@ namespace UVC.UI.Commands
_languageCode = languageCode;
}
public void Execute(object parameter = null)
public void Execute(object? parameter = null)
{
string targetLanguage = _languageCode;

View File

@@ -1,4 +1,5 @@
using UVC.Log;
#nullable enable
using UVC.Log;
namespace UVC.UI.Commands
{
@@ -12,7 +13,7 @@ namespace UVC.UI.Commands
_message = message;
}
public void Execute(object parameter = null)
public void Execute(object? parameter = null)
{
string finalMessage = _message;
if (parameter != null)

View File

@@ -1,14 +1,21 @@
namespace UVC.UI.Commands
#nullable enable
namespace UVC.UI.Commands
{
public interface ICommand
{
void Execute(object parameter = null);
void Execute(object? parameter = null);
}
public interface ICommand<T> : ICommand
{
void Execute(T parameter);
void Execute() => Execute(default(T)); // 기본 Start 구현 제공 가능
void Execute()
{
// CS8604: T가 참조형이고 null이 허용되지 않을 때 경고가 발생하므로,
// T가 null 허용 타입이거나 값 타입일 때만 default(T)를 전달합니다.
// 그렇지 않으면, 명시적으로 default(T)를 전달하되, T가 null 허용 타입임을 명시합니다.
Execute(default(T)!);
}
}
}

View File

@@ -1,4 +1,5 @@
using UnityEngine;
#nullable enable
using UnityEngine;
namespace UVC.UI.Commands.Mono
{
@@ -9,7 +10,7 @@ namespace UVC.UI.Commands.Mono
{
// MonoCommand는 MonoBehaviour를 상속받아 Unity의 생명주기를 활용할 수 있습니다.
// ICommand 인터페이스를 구현하여 명령 패턴을 따릅니다.
public virtual void Execute(object parameter = null)
public virtual void Execute(object? parameter = null)
{
// 기본 실행 로직 (필요시 override 가능)
Debug.Log("MonoCommand executed.");

View File

@@ -1,11 +1,12 @@
using UnityEngine;
#nullable enable
using UnityEngine;
namespace UVC.UI.Commands
{
// 애플리케이션 종료 커맨드
public class QuitApplicationCommand : ICommand
{
public void Execute(object parameter = null)
public void Execute(object? parameter = null)
{
// 파라미터는 여기서는 사용되지 않을 수 있음
if (parameter != null)

View File

@@ -42,7 +42,7 @@ namespace UVC.UI.Loading
private bool animatting = false;
private Transform loadingImageTransform;
private float loadingSpeed = 1.5f;
private float loadingSpeed = -1.5f;
private float rotationSpeed = -1.0f;
private void Awake()

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Cysharp.Threading.Tasks;
using System; // System.Type 사용을 위해 추가
using System.Threading;
@@ -234,19 +234,19 @@ namespace UVC.UI.Modal
// 기존 리스너 제거 후 새 리스너 추가
modalView.confirmButton.onClick.RemoveAllListeners();
// content.ShowConfirmButton 여부는 ModalView.OnOpen에서 버튼 자체의 활성화로 처리
modalView.confirmButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, true, modalView));
modalView.confirmButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, true, modalView, "confirm"));
}
if (modalView.cancelButton != null)
{
modalView.cancelButton.onClick.RemoveAllListeners();
modalView.cancelButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, false, modalView));
modalView.cancelButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, false, modalView, "close"));
}
if (modalView.closeButton != null)
{
modalView.closeButton.onClick.RemoveAllListeners();
modalView.closeButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, false, modalView));
modalView.closeButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, false, modalView, "close"));
}
}
@@ -263,7 +263,8 @@ namespace UVC.UI.Modal
ModalContent content,
UniTaskCompletionSource<T> tcs,
bool isConfirmAction,
ModalView modalViewContext)
ModalView modalViewContext,
string buttonType)
{
// 📜 이야기: 이 함수는 사용자가 버튼을 눌렀을 때 실행돼요.
// 그런데 만약 이전에 처리하던 약속 증서(activeTcs)와 지금 받은 증서(tcs)가 다르거나,
@@ -277,6 +278,19 @@ namespace UVC.UI.Modal
// 📜 이야기: 사용자가 버튼을 하나 눌렀으니, 다른 버튼들은 잠깐 못 누르게 막아요. (실수로 두 번 누르는 것 방지)
modalViewContext.SetAllButtonsInteractable(false);
if(buttonType == "confirm")
{
modalViewContext.OnConfirmButtonClicked();
}
else if (buttonType == "cancel")
{
modalViewContext.OnCancelButtonClicked();
}
else if (buttonType == "close")
{
modalViewContext.OnCloseButtonClicked();
}
// 📜 이야기: 이제 모달을 닫고 뒷정리를 할 시간이에요!
// CleanupCurrentModalResources 조수에게 "이 모달 뷰를 사용했고, 사용자는 '확인'(또는 '취소')을 눌렀어요" 라고 알려주며 뒷정리를 부탁해요.
// 이 뒷정리 과정에서 '약속 증서'에 최종 결과가 기록될 거예요.

View File

@@ -1,4 +1,5 @@
using Cysharp.Threading.Tasks;
#nullable enable
using Cysharp.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
@@ -260,7 +261,7 @@ namespace UVC.UI.Modal
/// // Modal.Open<string>(...) 이렇게 호출하면, 입력된 문자열을 받을 수 있어요.
/// </code>
/// </example>
public virtual object GetResult()
public virtual object? GetResult()
{
return null;
}
@@ -277,5 +278,25 @@ namespace UVC.UI.Modal
if (closeButton != null) closeButton.interactable = interactable;
}
public virtual void OnConfirmButtonClicked()
{
// 확인 버튼이 눌렸을 때 호출되는 메서드예요.
// 기본적으로는 아무것도 하지 않지만, 필요하면 재정의해서 특별한 동작을 추가할 수 있어요.
// 예: ULog.Debug("확인 버튼이 눌렸어요!");
}
public virtual void OnCancelButtonClicked()
{
// 취소 버튼이 눌렸을 때 호출되는 메서드예요.
// 기본적으로는 아무것도 하지 않지만, 필요하면 재정의해서 특별한 동작을 추가할 수 있어요.
// 예: ULog.Debug("취소 버튼이 눌렸어요!");
}
public virtual void OnCloseButtonClicked()
{
// 닫기 버튼이 눌렸을 때 호출되는 메서드예요.
// 기본적으로는 아무것도 하지 않지만, 필요하면 재정의해서 특별한 동작을 추가할 수 있어요.
// 예: ULog.Debug("닫기 버튼이 눌렸어요!");
}
}
}

View File

@@ -94,13 +94,25 @@ namespace UVC.UI
dragArea = GetComponentInParent<Canvas>()?.transform as RectTransform;
if (dragArea == null)
{
Debug.LogError("<b>[UIDragger]</b> 드래그 영역(dragArea)으로 사용할 Canvas를 찾을 수 없습니다.", this);
Debug.Log("<b>[UIDragger]</b> 드래그 영역(dragArea)으로 사용할 Canvas를 찾을 수 없습니다.", this);
enabled = false;
return;
}
}
}
/// <summary>
/// 드래그가 허용되는 영역을 설정합니다.
/// </summary>
/// <remarks>이 메서드를 활성화하면 지정된 영역 내에서 드래그 기능이 활성화됩니다.
///</remarks>
/// <param name="area">드래그 영역의 경계를 정의하는 <see cref="RectTransform"/>입니다. null일 수 없습니다.</param>
public void SetDragArea(RectTransform area)
{
dragArea = area;
enabled = true;
}
/// <summary>
/// 드래그가 시작될 때 호출됩니다. (IBeginDragHandler)
/// </summary>