코드 정리 및 DataValidator 추가, HttpPipeLine 쓰레드에서 실행되도록 수정
This commit is contained in:
@@ -58,6 +58,7 @@ namespace UVC.Data
|
||||
/// </summary>
|
||||
private DataMask mask;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 병렬 처리를 적용할 배열의 최소 크기
|
||||
/// </summary>
|
||||
|
||||
@@ -4,7 +4,6 @@ using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Unity.VisualScripting.Antlr3.Runtime;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
|
||||
358
Assets/Scripts/UVC/Data/DataValidator.cs
Normal file
358
Assets/Scripts/UVC/Data/DataValidator.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
#nullable enable
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 데이터의 유효성을 검사하는 클래스입니다.
|
||||
/// 이 클래스는 특정 속성(프로퍼티)에 대한 유효성 검사 규칙을 추가하고,
|
||||
/// 데이터가 해당 규칙을 모두 충족하는지 확인할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 유효성 검사기 생성
|
||||
/// var _validator = new DataValidator();
|
||||
///
|
||||
/// // 검증 규칙 추가: name 속성은 비어있지 않아야 함
|
||||
/// _validator.AddValidator("name", value => value is string name && !string.IsNullOrEmpty(name));
|
||||
///
|
||||
/// // 검증 규칙 추가: age 속성은 18 이상이어야 함
|
||||
/// _validator.AddValidator("age", value => value is long age && age >= 18);
|
||||
///
|
||||
/// // 데이터 검증 예시
|
||||
/// var userData = new JObject {
|
||||
/// { "name", "홍길동" },
|
||||
/// { "age", 20 }
|
||||
/// };
|
||||
///
|
||||
/// if (_validator.IsValid(userData))
|
||||
/// {
|
||||
/// Console.WriteLine("유효한 사용자 데이터입니다.");
|
||||
/// }
|
||||
/// else
|
||||
/// {
|
||||
/// Console.WriteLine("유효하지 않은 사용자 데이터입니다.");
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class DataValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// 유효성 검사 규칙의 목록입니다.
|
||||
/// 각 사전은 속성 이름(string)과 해당 속성의 검증 함수(Func<object, bool>)를 포함합니다.
|
||||
/// </summary>
|
||||
private List<Dictionary<string, Func<object, bool>>> validaters = new List<Dictionary<string, Func<object, bool>>>();
|
||||
|
||||
/// <summary>
|
||||
/// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다.
|
||||
/// 스트리밍 방식은 메모리 사용량을 줄이기 위해 대용량 데이터를 처리할 때 유용합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// true로 설정하면 대용량 JSON 데이터를 처리할 때 스트리밍 방식을 사용합니다.
|
||||
/// false로 설정하면 항상 전체 데이터를 메모리에 로드합니다.
|
||||
/// </remarks>
|
||||
public bool SupportsStreamParsing { get; internal set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 스트리밍 방식을 적용할 JSON 데이터의 최소 길이(바이트)입니다.
|
||||
/// 데이터 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 기본값은 10,000바이트입니다. 작은 데이터의 경우 스트리밍 처리가 오히려
|
||||
/// 오버헤드를 발생시킬 수 있으므로, 특정 크기 이상일 때만 스트리밍 방식을 적용합니다.
|
||||
/// </remarks>
|
||||
public int SupportsStreamLength { get; internal set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// 새로운 유효성 검사 규칙을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">검사할 속성 이름</param>
|
||||
/// <param name="validator">유효성 검사 함수 (매개변수: 속성값, 반환값: 유효성 여부)</param>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var _validator = new DataValidator();
|
||||
///
|
||||
/// // 문자열 속성이 비어있지 않은지 확인하는 규칙
|
||||
/// _validator.AddValidator("username", value =>
|
||||
/// {
|
||||
/// if (value is string str)
|
||||
/// return !string.IsNullOrEmpty(str);
|
||||
/// return false;
|
||||
/// });
|
||||
///
|
||||
/// // 숫자 속성이 특정 범위 내에 있는지 확인하는 규칙
|
||||
/// _validator.AddValidator("score", value =>
|
||||
/// {
|
||||
/// if (value is long num)
|
||||
/// return num >= 0 && num <= 100;
|
||||
/// return false;
|
||||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void AddValidator(string propertyName, Func<object, bool> validator)
|
||||
{
|
||||
// 유효성 검사기를 추가합니다.
|
||||
validaters.Add(new Dictionary<string, Func<object, bool>> { { propertyName, validator } });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 속성에 대한 모든 유효성 검사 규칙을 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">규칙을 제거할 속성 이름</param>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var _validator = new DataValidator();
|
||||
/// _validator.AddValidator("age", value => (long)value >= 18);
|
||||
///
|
||||
/// // 검증 과정 중에 'age' 속성 검사가 더 이상 필요하지 않을 경우
|
||||
/// _validator.RemoveValidator("age");
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void RemoveValidator(string propertyName)
|
||||
{
|
||||
// 유효성 검사기를 제거합니다.
|
||||
validaters.RemoveAll(v => v.ContainsKey(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataObject 객체가 모든 유효성 검사 규칙을 통과하는지 확인합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">검사할 DataObject</param>
|
||||
/// <returns>모든 규칙을 통과하면 true, 하나라도 실패하면 false</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var _validator = new DataValidator();
|
||||
/// _validator.AddValidator("email", value => {
|
||||
/// var emailStr = value as string;
|
||||
/// return emailStr != null && emailStr.Contains("@");
|
||||
/// });
|
||||
///
|
||||
/// var userData = new DataObject();
|
||||
/// userData["name"] = "홍길동";
|
||||
/// userData["email"] = "hong@example.com";
|
||||
///
|
||||
/// bool isValid = _validator.IsValid(userData); // true 반환
|
||||
/// </code>
|
||||
/// </example>
|
||||
public bool IsValid(DataObject? data)
|
||||
{
|
||||
// 데이터가 null인 경우 유효하지 않음
|
||||
if (data == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 각 유효성 검사기를 순회하며 검증을 수행합니다.
|
||||
foreach (var validator in validaters)
|
||||
{
|
||||
foreach (var kvp in validator)
|
||||
{
|
||||
// 해당 속성이 데이터에 존재하는지 확인
|
||||
if (!data.ContainsKey(kvp.Key))
|
||||
{
|
||||
continue; // 속성이 없으면 검증을 건너뜀
|
||||
}
|
||||
// 속성 값에 대한 검증을 수행
|
||||
if (!kvp.Value(data[kvp.Key]))
|
||||
{
|
||||
// 검증에 실패하면 false를 반환
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 검증을 통과하면 유효함
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JObject가 모든 유효성 검사 규칙을 통과하는지 확인합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">검사할 JObject</param>
|
||||
/// <returns>모든 규칙을 통과하면 true, 하나라도 실패하면 false</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var _validator = new DataValidator();
|
||||
///
|
||||
/// // 사용자 나이가 18세 이상인지 확인하는 규칙
|
||||
/// _validator.AddValidator("age", value =>
|
||||
/// value is long age && age >= 18
|
||||
/// );
|
||||
///
|
||||
/// // JSON 데이터 생성
|
||||
/// var jsonObject = JObject.Parse(@"{
|
||||
/// ""name"": ""김철수"",
|
||||
/// ""age"": 16
|
||||
/// }");
|
||||
///
|
||||
/// bool isValid = _validator.IsValid(jsonObject); // false 반환 (age가 18 미만)
|
||||
/// </code>
|
||||
/// </example>
|
||||
public bool IsValid(JObject? data)
|
||||
{
|
||||
// 데이터가 null인 경우 유효하지 않음
|
||||
if (data == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 각 유효성 검사기를 순회하며 검증을 수행합니다.
|
||||
foreach (var validator in validaters)
|
||||
{
|
||||
foreach (var kvp in validator)
|
||||
{
|
||||
// 해당 속성이 데이터에 존재하는지 확인
|
||||
if (!data.ContainsKey(kvp.Key))
|
||||
{
|
||||
continue; // 속성이 없으면 검증을 건너뜀
|
||||
}
|
||||
// 속성 값에 대한 검증을 수행
|
||||
if (!kvp.Value(data[kvp.Key]))
|
||||
{
|
||||
// 검증에 실패하면 false를 반환
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 검증을 통과하면 유효함
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 데이터 스트림이 모든 유효성 검사 규칙을 통과하는지 확인합니다.
|
||||
/// 대용량 JSON 데이터를 메모리 효율적으로 처리할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <param name="jsonStream">검사할 JSON 데이터 스트림</param>
|
||||
/// <returns>모든 규칙을 통과하면 true, 하나라도 실패하면 false</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var _validator = new DataValidator();
|
||||
/// _validator.AddValidator("isActive", value => value is bool active && active == true);
|
||||
///
|
||||
/// // 파일에서 JSON 데이터 스트림을 읽어 유효성 검사
|
||||
/// using (var fileStream = File.OpenRead("user-data.json"))
|
||||
/// {
|
||||
/// bool isValid = _validator.IsValid(fileStream);
|
||||
/// Console.WriteLine($"데이터 유효성: {isValid}");
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public bool IsValid(System.IO.Stream jsonStream)
|
||||
{
|
||||
if (jsonStream == null) return false;
|
||||
|
||||
// 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만,
|
||||
// 현재는 Newtonsoft.Json의 기본 역직렬화 사용
|
||||
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
||||
{
|
||||
// 청크 읽기 설정 - 메모리 사용량 최적화
|
||||
reader.SupportMultipleContent = true;
|
||||
|
||||
var serializer = new Newtonsoft.Json.JsonSerializer();
|
||||
var sourceObject = serializer.Deserialize<JObject>(reader);
|
||||
|
||||
return IsValid(sourceObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 배열에서 유효성 검사 규칙을 통과하는 항목만 필터링하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">필터링할 JSON 배열</param>
|
||||
/// <returns>유효한 항목만 포함된 새 JSON 배열 (유효한 항목이 없으면 null)</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var _validator = new DataValidator();
|
||||
///
|
||||
/// // 유효성 검사 규칙: status가 'active'인 항목만 유효
|
||||
/// _validator.AddValidator("status", value =>
|
||||
/// value is string status && status == "active"
|
||||
/// );
|
||||
///
|
||||
/// // 필터링할 JSON 배열 생성
|
||||
/// var usersArray = JArray.Parse(@"[
|
||||
/// {""id"": 1, ""name"": ""홍길동"", ""status"": ""active""},
|
||||
/// {""id"": 2, ""name"": ""김철수"", ""status"": ""inactive""},
|
||||
/// {""id"": 3, ""name"": ""이영희"", ""status"": ""active""}
|
||||
/// ]");
|
||||
///
|
||||
/// // 유효한 항목(status가 'active'인 항목)만 필터링
|
||||
/// JArray? validUsers = _validator.GetValidData(usersArray);
|
||||
///
|
||||
/// // validUsers는 홍길동과 이영희의 데이터만 포함 (2개 항목)
|
||||
/// Console.WriteLine($"유효한 사용자 수: {validUsers?.Count ?? 0}");
|
||||
/// </code>
|
||||
/// </example>
|
||||
public JArray? GetValidData(JArray? data)
|
||||
{
|
||||
if (data == null || data.Count == 0)
|
||||
{
|
||||
return data; // 빈 배열은 유효하지 않음
|
||||
}
|
||||
|
||||
JArray validData = new JArray();
|
||||
foreach (var item in data)
|
||||
{
|
||||
if (item is JObject jObject && IsValid(jObject))
|
||||
{
|
||||
validData.Add(jObject);
|
||||
}
|
||||
}
|
||||
return validData.Count > 0 ? validData : null; // 유효한 데이터가 없으면 null 반환
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 데이터 스트림에서 유효성 검사 규칙을 통과하는 항목만 필터링하여 반환합니다.
|
||||
/// 대용량 JSON 배열을 메모리 효율적으로 처리하기 위해 스트리밍 방식을 사용합니다.
|
||||
/// </summary>
|
||||
/// <param name="jsonStream">필터링할 JSON 배열 데이터 스트림</param>
|
||||
/// <returns>유효한 항목만 포함된 JSON 배열 (유효한 항목이 없으면 null)</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var _validator = new DataValidator();
|
||||
///
|
||||
/// // 필수 필드가 모두 있는지 확인하는 규칙 추가
|
||||
/// _validator.AddValidator("name", value => value != null && !string.IsNullOrEmpty(value.ToString()));
|
||||
/// _validator.AddValidator("email", value => value != null && value.ToString().Contains("@"));
|
||||
///
|
||||
/// // 파일에서 JSON 배열 데이터 스트림을 읽어 유효한 항목만 필터링
|
||||
/// using (var fileStream = File.OpenRead("users-data.json"))
|
||||
/// {
|
||||
/// JArray? validUsers = _validator.GetValidData(fileStream);
|
||||
///
|
||||
/// if (validUsers != null)
|
||||
/// {
|
||||
/// Console.WriteLine($"유효한 사용자 수: {validUsers.Count}");
|
||||
///
|
||||
/// // 첫 번째 유효한 사용자 정보 출력
|
||||
/// if (validUsers.Count > 0)
|
||||
/// {
|
||||
/// var firstUser = validUsers[0];
|
||||
/// Console.WriteLine($"첫 번째 유효 사용자: {firstUser["name"]}");
|
||||
/// }
|
||||
/// }
|
||||
/// else
|
||||
/// {
|
||||
/// Console.WriteLine("유효한 사용자가 없습니다.");
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public JArray? GetValidData(System.IO.Stream jsonStream)
|
||||
{
|
||||
if (jsonStream == null) return null;
|
||||
|
||||
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
||||
{
|
||||
// 청크 읽기 설정 - 메모리 사용량 최적화
|
||||
reader.SupportMultipleContent = true;
|
||||
|
||||
var serializer = new Newtonsoft.Json.JsonSerializer();
|
||||
var sourceArray = serializer.Deserialize<JArray>(reader);
|
||||
|
||||
return GetValidData(sourceArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/DataValidator.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/DataValidator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74866795406b7c74a86591446c9b455a
|
||||
@@ -12,6 +12,7 @@ using UnityEngine;
|
||||
using UVC.Log;
|
||||
using UVC.Network;
|
||||
using UVC.Tests;
|
||||
using UVC.Threading;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
@@ -29,6 +30,11 @@ namespace UVC.Data
|
||||
/// - 안전한 요청 취소 및 자원 정리
|
||||
/// - 테스트를 위한 목업 기능 지원
|
||||
///
|
||||
/// 모든 HTTP 요청은 백그라운드 스레드(스레드풀)에서 처리되어 메인 스레드 차단을 방지합니다.
|
||||
/// 요청 결과 처리 시 핸들러(SuccessHandler, FailHandler)는 자동으로 메인 스레드에서 호출됩니다.
|
||||
/// 이를 통해 UI 스레드 차단 없이 효율적인 네트워크 작업을 수행하면서도,
|
||||
/// UI 업데이트는 안전하게 메인 스레드에서 처리할 수 있습니다.
|
||||
///
|
||||
/// 모든 반복 실행은 CancellationTokenSource를 통해 취소할 수 있으며,
|
||||
/// 취소 후 현재 진행 중인 모든 요청이 안전하게 완료되는 것을 보장합니다.
|
||||
/// </remarks>
|
||||
@@ -142,22 +148,33 @@ namespace UVC.Data
|
||||
/// 지정한 키의 HTTP 요청을 실행합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">실행할 요청의 키</param>
|
||||
/// <param name="switchToMainThread">메인 스레드로 전환할지 여부</param>
|
||||
/// <returns>비동기 작업</returns>
|
||||
/// <remarks>
|
||||
/// 요청 정보의 repeat 속성에 따라 단일 실행 또는 반복 실행을 시작합니다.
|
||||
/// 이미 실행 중인 반복 작업이 있다면 먼저 중지하고 완료를 대기한 후 새로운 요청을 시작합니다.
|
||||
/// 단일 실행의 경우 완료될 때까지 대기하지만, 반복 실행은 백그라운드에서 실행됩니다.
|
||||
///
|
||||
/// 모든 HTTP 요청 처리는 백그라운드 스레드에서 수행되며, 핸들러만 메인 스레드에서 호출됩니다.
|
||||
/// </remarks>
|
||||
/// <exception cref="KeyNotFoundException">지정된 키가 등록되어 있지 않은 경우</exception>
|
||||
public async UniTask Excute(string key)
|
||||
public async UniTask Excute(string key, bool switchToMainThread = false)
|
||||
{
|
||||
if (infoList.ContainsKey(key))
|
||||
if (!infoList.ContainsKey(key))
|
||||
{
|
||||
HttpPipeLineInfo info = infoList[key];
|
||||
throw new KeyNotFoundException($"No HTTP request found with key '{key}'.");
|
||||
}
|
||||
|
||||
// 반복 설정에 관계없이 이전에 실행 중인 반복 작업이 있다면 중지
|
||||
await StopRepeat(key);
|
||||
HttpPipeLineInfo info = infoList[key];
|
||||
|
||||
// 반복 설정에 관계없이 이전에 실행 중인 반복 작업이 있다면 중지
|
||||
await StopRepeat(key);
|
||||
|
||||
// 스레드풀에서 요청 처리 실행
|
||||
await UniTask.SwitchToThreadPool();
|
||||
|
||||
try
|
||||
{
|
||||
if (!info.Repeat)
|
||||
{
|
||||
// 단일 실행 로직 호출
|
||||
@@ -166,12 +183,19 @@ namespace UVC.Data
|
||||
else
|
||||
{
|
||||
// 반복 설정이 있는 경우에만 StartRepeat 호출
|
||||
// Forget()을 호출하지 않고 StartRepeat가 스레드풀에서 계속 실행되도록 함
|
||||
StartRepeat(key).Forget();
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
throw new KeyNotFoundException($"No HTTP request found with key '{key}'.");
|
||||
// ExecuteSingle 또는 StartRepeat.Forget() 후 메인 스레드로 복귀하는 것은 선택 사항
|
||||
// 여기서는 원래 실행 컨텍스트로 돌아가기 위해 메인 스레드로 전환
|
||||
// 만약 계속해서 백그라운드 스레드에서 실행하려면 이 코드를 제거할 수 있음
|
||||
if (switchToMainThread)
|
||||
{
|
||||
await UniTask.SwitchToMainThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +207,9 @@ namespace UVC.Data
|
||||
/// <param name="cancellationToken">요청 취소를 위한 취소 토큰</param>
|
||||
/// <returns>비동기 작업</returns>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 HTTP 요청을 보내고, 응답 데이터를 파싱하여 IDataObject로 변환합니다.
|
||||
/// 이 메서드는 백그라운드 스레드에서 HTTP 요청을 보내고, 응답 데이터를 파싱하여 IDataObject로 변환합니다.
|
||||
/// JSON 객체 또는 배열 형식의 응답을 처리할 수 있으며, 취소 토큰을 통해 언제든지 작업을 취소할 수 있습니다.
|
||||
/// 모든 핸들러 호출은 메인 스레드에서 이루어져 UI 업데이트를 안전하게 수행할 수 있습니다.
|
||||
/// </remarks>
|
||||
/// <exception cref="OperationCanceledException">작업이 취소된 경우 발생</exception>
|
||||
/// <exception cref="JsonException">JSON 응답 파싱 중 오류가 발생한 경우</exception>
|
||||
@@ -237,7 +262,16 @@ namespace UVC.Data
|
||||
// 응답 마스크 적용 결과가 성공이 아니면 실패 핸들러 호출 후 반환
|
||||
if (!responseResult.IsSuccess)
|
||||
{
|
||||
info.FailHandler?.Invoke(responseResult.Message!);
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
string errorMessage = responseResult.Message!;
|
||||
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke(errorMessage);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -248,6 +282,44 @@ namespace UVC.Data
|
||||
|
||||
if (result.StartsWith("{"))
|
||||
{
|
||||
if (info.Validator != null)
|
||||
{
|
||||
if (info.Validator.SupportsStreamParsing && result.Length > info.Validator.SupportsStreamLength)
|
||||
{
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result)))
|
||||
{
|
||||
if (info.Validator != null && !info.Validator.IsValid(stream))
|
||||
{
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JObject source = JObject.Parse(result);
|
||||
if (info.Validator != null && !info.Validator.IsValid(source))
|
||||
{
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info.DataMapper != null)
|
||||
{
|
||||
if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength)
|
||||
@@ -266,19 +338,81 @@ namespace UVC.Data
|
||||
}
|
||||
else if (result.StartsWith("["))
|
||||
{
|
||||
if (info.Validator != null)
|
||||
{
|
||||
if (info.Validator.SupportsStreamParsing && result.Length > info.Validator.SupportsStreamLength)
|
||||
{
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result)))
|
||||
{
|
||||
JArray? validSource = info.Validator.GetValidData(stream);
|
||||
if (validSource == null)
|
||||
{
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JArray source = JArray.Parse(result);
|
||||
JArray? validSource = info.Validator.GetValidData(source);
|
||||
if (validSource == null)
|
||||
{
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is not Valid");
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info.DataMapper != null)
|
||||
{
|
||||
if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength)
|
||||
{
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result)))
|
||||
{
|
||||
dataObject = info.DataMapper.MapArrayStream(stream);
|
||||
if (info.Validator != null)
|
||||
{
|
||||
JArray? validSource = info.Validator.GetValidData(stream);
|
||||
if (validSource != null)
|
||||
{
|
||||
dataObject = info.DataMapper.Map(validSource);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dataObject = info.DataMapper.MapArrayStream(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JArray source = JArray.Parse(result);
|
||||
dataObject = info.DataMapper.Map(source);
|
||||
if (info.Validator != null)
|
||||
{
|
||||
JArray? validSource = info.Validator.GetValidData(source);
|
||||
if (validSource != null)
|
||||
{
|
||||
dataObject = info.DataMapper.Map(validSource);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dataObject = info.DataMapper.Map(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,12 +434,47 @@ namespace UVC.Data
|
||||
// 갱신 된 데이터가 있는 경우 핸들러 호출
|
||||
if (info.UpdatedDataOnly)
|
||||
{
|
||||
if (dataObject != null && dataObject.UpdatedCount > 0) info.SuccessHandler?.Invoke(dataObject);
|
||||
if (dataObject != null && dataObject.UpdatedCount > 0)
|
||||
{
|
||||
if (info.SuccessHandler != null)
|
||||
{
|
||||
// 로컬 변수로 복사하여 클로저에서 안전하게 사용
|
||||
var handlerData = dataObject;
|
||||
// UI 스레드에서 성공 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.SuccessHandler.Invoke(handlerData);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (dataObject != null)
|
||||
{
|
||||
if (info.SuccessHandler != null)
|
||||
{
|
||||
// 로컬 변수로 복사하여 클로저에서 안전하게 사용
|
||||
var handlerData = dataObject;
|
||||
// UI 스레드에서 성공 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.SuccessHandler.Invoke(handlerData);
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
info.SuccessHandler?.Invoke(dataObject);
|
||||
if (info.FailHandler != null)
|
||||
{
|
||||
// UI 스레드에서 실패 핸들러 호출
|
||||
MainThreadDispatcher.Instance.SendToMainThread(() =>
|
||||
{
|
||||
info.FailHandler.Invoke("Data is Null");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -349,10 +518,12 @@ namespace UVC.Data
|
||||
/// <param name="key">반복 실행할 요청의 키</param>
|
||||
/// <returns>비동기 작업</returns>
|
||||
/// <remarks>
|
||||
/// 지정된 간격(repeatInterval)으로 HTTP 요청을 반복 실행합니다.
|
||||
/// 지정된 간격(repeatInterval)으로 HTTP 요청을 백그라운드 스레드에서 반복 실행합니다.
|
||||
/// repeatCount가 0인 경우 무한 반복하며, 0보다 큰 경우 지정된 횟수만큼만 실행합니다.
|
||||
/// 작업 실행 중 예외가 발생하면 로그를 기록하고 다음 실행을 시도합니다.
|
||||
/// 취소 요청이 있거나 최대 실행 횟수에 도달하면 반복이 종료됩니다.
|
||||
///
|
||||
/// 이 메서드는 백그라운드 스레드에서 실행되며, 모든 핸들러 호출은 메인 스레드에서 이루어집니다.
|
||||
/// </remarks>
|
||||
/// <exception cref="KeyNotFoundException">지정된 키가 등록되어 있지 않은 경우</exception>
|
||||
private async UniTask StartRepeat(string key)
|
||||
|
||||
@@ -42,6 +42,42 @@ namespace UVC.Data
|
||||
private int _retryDelay = 1000; // 밀리초
|
||||
private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 응답 데이터의 유효성을 검사하는 데 사용되는 검사기입니다.
|
||||
/// 이 검사기를 통해 특정 조건을 만족하는 데이터만 처리하도록 필터링할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 데이터 유효성 검사는 HTTP 응답이 성공적으로 수신되고 JSON으로 파싱된 후,
|
||||
/// 성공 핸들러에 전달되기 전에 수행됩니다.
|
||||
/// 검사기가 null이 아닌 경우에만 유효성 검사가 실행됩니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // DataValidator 생성 및 규칙 추가
|
||||
/// var validator = new DataValidator();
|
||||
///
|
||||
/// // "status" 필드가 "active"인 데이터만 유효하도록 설정
|
||||
/// validator.AddValidator("status", value =>
|
||||
/// value is string status && status.Equals("active", StringComparison.OrdinalIgnoreCase));
|
||||
///
|
||||
/// // "temperature" 필드가 유효한 범위 내에 있는지 확인
|
||||
/// validator.AddValidator("temperature", value => {
|
||||
/// if (value is double temp)
|
||||
/// return temp >= -50 && temp <= 100;
|
||||
/// return false;
|
||||
/// });
|
||||
///
|
||||
/// // 파이프라인에 validator 설정
|
||||
/// var pipeline = new HttpPipeLineInfo("https://api.weather.com/current")
|
||||
/// .setValidator(validator)
|
||||
/// .setSuccessHandler(data => {
|
||||
/// // 여기에 전달되는 데이터는 이미 유효성 검사를 통과한 데이터만 포함됩니다.
|
||||
/// Console.WriteLine($"유효한 온도: {data["temperature"]}°C");
|
||||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
private DataValidator? _validator;
|
||||
|
||||
/// <summary>
|
||||
/// 요청을 보낼 URL 주소
|
||||
/// </summary>
|
||||
@@ -92,6 +128,16 @@ namespace UVC.Data
|
||||
/// </summary>
|
||||
public DataMapper? DataMapper => _dataMapper;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 응답 데이터의 유효성을 검사하는 검사기입니다.
|
||||
/// 이 속성을 통해 설정된 규칙에 따라 HTTP 응답 데이터를 필터링할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// null이 아닌 경우, HTTP 응답이 파싱된 후 데이터가 핸들러에 전달되기 전에
|
||||
/// 데이터가 검사기의 모든 규칙을 통과하는지 확인합니다.
|
||||
/// </remarks>
|
||||
public DataValidator? Validator => _validator;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 응답의 성공 여부를 확인하고, 성공 시 실제 데이터 페이로드를 추출하는 데 사용되는 <see cref="Data.HttpResponseMask"/>입니다.
|
||||
/// 이 객체에 정의된 규칙에 따라 원시 응답 문자열이 처리됩니다.
|
||||
@@ -140,6 +186,55 @@ namespace UVC.Data
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 응답 데이터의 유효성을 검사하는 검사기를 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="validator">HTTP 응답 데이터의 유효성 검사에 사용할 DataValidator 객체</param>
|
||||
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
/// <remarks>
|
||||
/// 이 메서드로 설정된 검사기는 HTTP 응답이 성공적으로 파싱된 후,
|
||||
/// 성공 핸들러에 전달되기 전에 데이터의 유효성을 검사합니다.
|
||||
/// 데이터가 모든 유효성 검사 규칙을 통과하지 못하면 성공 핸들러가 호출되지 않습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 1. DataValidator 생성 및 규칙 추가
|
||||
/// var validator = new DataValidator();
|
||||
///
|
||||
/// // 2. 유효성 검사 규칙 정의
|
||||
/// // 사용자 데이터에 이메일이 포함되어 있는지 확인
|
||||
/// validator.AddValidator("email", value => {
|
||||
/// if (value is string email)
|
||||
/// return email.Contains("@") && email.Contains(".");
|
||||
/// return false;
|
||||
/// });
|
||||
///
|
||||
/// // 사용자 나이가 18세 이상인지 확인
|
||||
/// validator.AddValidator("age", value => {
|
||||
/// if (value is long age)
|
||||
/// return age >= 18;
|
||||
/// return false;
|
||||
/// });
|
||||
///
|
||||
/// // 3. 검사기를 HTTP 파이프라인에 설정
|
||||
/// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/users", "get")
|
||||
/// .setDataMapper(userDataMapper)
|
||||
/// .setValidator(validator)
|
||||
/// .setSuccessHandler(userData => {
|
||||
/// // 여기에 도달하는 사용자 데이터는 모두 이메일이 유효하고 18세 이상입니다.
|
||||
/// Console.WriteLine($"유효한 사용자: {userData["name"]}, {userData["email"]}");
|
||||
/// })
|
||||
/// .setFailHandler(errorMsg => {
|
||||
/// Console.WriteLine($"요청 실패: {errorMsg}");
|
||||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public HttpPipeLineInfo setValidator(DataValidator validator)
|
||||
{
|
||||
this._validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 파이프라인에 적용할 ResponseMask를 설정하고 업데이트된 파이프라인 구성을 반환합니다.
|
||||
/// </summary>
|
||||
|
||||
@@ -205,12 +205,32 @@ namespace UVC.Data
|
||||
if (message.StartsWith("{"))
|
||||
{
|
||||
JObject source = JObject.Parse(message);
|
||||
if (info.Validator != null && !info.Validator.IsValid(source))
|
||||
{
|
||||
return; // 유효성 검사 실패 시 핸들러 호출을 중단
|
||||
}
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Map(source);
|
||||
}
|
||||
else if (message.StartsWith("["))
|
||||
{
|
||||
JArray source = JArray.Parse(message);
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Map(source);
|
||||
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);
|
||||
|
||||
@@ -32,6 +32,37 @@ namespace UVC.Data
|
||||
private DataMapper? _dataMapper = null; // 데이터 매퍼
|
||||
private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 메시지 데이터의 유효성을 검사하는 데 사용되는 검사기입니다.
|
||||
/// 메시지가 특정 조건이나 규칙을 만족하는지 확인하는 데 활용됩니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 데이터 유효성 검사는 메시지를 필터링하여 유효한 데이터만 처리하도록 합니다.
|
||||
/// 예를 들어, 필수 필드가 있는지, 값의 범위가 적절한지 등을 검사할 수 있습니다.
|
||||
/// null인 경우 유효성 검사를 수행하지 않습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // DataValidator 생성 및 규칙 추가
|
||||
/// var _validator = new DataValidator();
|
||||
///
|
||||
/// // 온도 값이 -50°C ~ 100°C 범위 내에 있는지 검사
|
||||
/// _validator.AddValidator("temperature", value =>
|
||||
/// value is double temp && temp >= -50 && temp <= 100);
|
||||
///
|
||||
/// // 습도 값이 0% ~ 100% 범위 내에 있는지 검사
|
||||
/// validator.AddValidator("humidity", value =>
|
||||
/// value is double humidity && humidity >= 0 && humidity <= 100);
|
||||
///
|
||||
/// // 파이프라인에 validator 설정
|
||||
/// var pipelineInfo = new MQTTPipeLineInfo("sensors/readings")
|
||||
/// .setDataMapper(dataMapper)
|
||||
/// .setValidator(validator)
|
||||
/// .setHandler(data => ProcessValidSensorData(data));
|
||||
/// </code>
|
||||
/// </example>
|
||||
private DataValidator? _validator;
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 토픽
|
||||
/// </summary>
|
||||
@@ -47,6 +78,11 @@ namespace UVC.Data
|
||||
/// </summary>
|
||||
public DataMapper? DataMapper => _dataMapper;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DataValidator"/> 인스턴스를 사용하여 데이터 유효성을 검사합니다.
|
||||
/// </summary>
|
||||
public DataValidator? Validator => _validator;
|
||||
|
||||
/// <summary>
|
||||
/// 업데이트된 데이터만 받을 여부 (true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다)
|
||||
/// </summary>
|
||||
@@ -110,5 +146,49 @@ namespace UVC.Data
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 메시지 데이터의 유효성을 검사하는 데 사용할 Validator를 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="validator">데이터 유효성 검사기</param>
|
||||
/// <returns>현재 MQTTPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
/// <remarks>
|
||||
/// 유효성 검사기는 수신된 MQTT 메시지 데이터가 유효한지 확인하는 역할을 합니다.
|
||||
/// 데이터가 특정 조건을 만족하는지 확인하고, 유효하지 않은 메시지는 필터링할 수 있습니다.
|
||||
/// 파이프라인 내에서 DataMapper가 데이터를 변환한 후, Validator가 유효성을 검사하고,
|
||||
/// 유효한 데이터만 Handler에게 전달됩니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // DataValidator 생성 및 설정
|
||||
/// var _validator = new DataValidator();
|
||||
///
|
||||
/// // 온도 값이 -50°C ~ 100°C 범위 내에 있는지 검사
|
||||
/// _validator.AddValidator("temperature", value => {
|
||||
/// if (value is double temp)
|
||||
/// return temp >= -50 && temp <= 100;
|
||||
/// return false;
|
||||
/// });
|
||||
///
|
||||
/// // deviceId가 null이 아니고 비어있지 않은지 검사
|
||||
/// validator.AddValidator("deviceId", value =>
|
||||
/// value is string id && !string.IsNullOrEmpty(id));
|
||||
///
|
||||
/// // Validator를 파이프라인에 설정
|
||||
/// var pipelineInfo = new MQTTPipeLineInfo("sensors/data")
|
||||
/// .setDataMapper(dataMapper)
|
||||
/// .setValidator(_validator)
|
||||
/// .setHandler(data => {
|
||||
/// // 여기서 처리되는 데이터는 모두 유효성 검사를 통과한 데이터
|
||||
/// Console.WriteLine($"유효한 센서 데이터: {data["deviceId"]} - {data["temperature"]}°C");
|
||||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
public MQTTPipeLineInfo setValidator(DataValidator validator)
|
||||
{
|
||||
this._validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,5 +174,15 @@ namespace UVC.Extension
|
||||
return t;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 <see cref="Transform"/>의 로컬 위치를 월드 공간 좌표로 변환합니다.
|
||||
/// </summary>
|
||||
/// <param name="t">로컬 위치를 변환할 <see cref="Transform"/>입니다.</param>
|
||||
/// <returns> <paramref name="t"/> 로컬 위치의 월드 공간 좌표를 나타내는 <see cref="Vector3"/>입니다.</returns>
|
||||
public static Vector3 ToWorldPosition(this Transform t)
|
||||
{
|
||||
return t.TransformPoint(t.localPosition);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UVC.Factory.Component;
|
||||
using UVC.Util;
|
||||
|
||||
namespace UVC.Factory.Alarm
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using UnityEngine;
|
||||
using UVC.Util;
|
||||
|
||||
namespace UVC.Factory.Alarm
|
||||
{
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UVC.Core;
|
||||
|
||||
namespace UVC.Factory
|
||||
{
|
||||
public class CameraController : SingletonScene<CameraController>
|
||||
{
|
||||
public float moveSpeed = 5.0f;
|
||||
public float rotateSpeed = 100.0f;
|
||||
|
||||
private Coroutine focusCoroutine;
|
||||
|
||||
|
||||
public void FocusOnTarget(Transform target, float distance)
|
||||
{
|
||||
if (focusCoroutine != null)
|
||||
{
|
||||
StopCoroutine(focusCoroutine);
|
||||
}
|
||||
focusCoroutine = StartCoroutine(FocusRoutine(target, distance));
|
||||
}
|
||||
|
||||
private IEnumerator FocusRoutine(Transform target, float distance)
|
||||
{
|
||||
Vector3 targetPosition = target.position - transform.forward * distance;
|
||||
Quaternion targetRotation = Quaternion.LookRotation(target.position - transform.position);
|
||||
|
||||
while (Vector3.Distance(transform.position, targetPosition) > 0.01f)
|
||||
{
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * moveSpeed);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotateSpeed);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1261d9494e4176a488c4e65b80396de6
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UVC.Data;
|
||||
@@ -16,33 +17,31 @@ namespace UVC.Factory.Component
|
||||
// 움직임과 회전의 부드러움을 조절할 속도 변수
|
||||
// Unity 인스펙터 창에서 실시간으로 값을 조절하며 테스트할 수 있습니다.
|
||||
[Tooltip("목표 지점까지의 이동 속도를 조절합니다.")]
|
||||
public float moveSpeed = 2.0f;
|
||||
public float moveSpeed = 1.0f;
|
||||
|
||||
[Tooltip("목표 방향까지의 회전 속도를 조절합니다.")]
|
||||
public float rotationSpeed = 5.0f;
|
||||
public float rotationSpeed = 2.0f;
|
||||
|
||||
[Tooltip("이 거리(미터)를 초과하면 보간 없이 즉시 위치를 변경합니다.")]
|
||||
public float teleportDistanceThreshold = 5.0f; // 5미터 이상 차이나면 순간이동
|
||||
|
||||
private void Start()
|
||||
{
|
||||
targetPosition = transform.position;
|
||||
targetRotation = transform.rotation;
|
||||
|
||||
DataOrderedMask = new List<string>
|
||||
{
|
||||
"Name",
|
||||
"Type",
|
||||
"Status",
|
||||
"BatteryLevel",
|
||||
"Location",
|
||||
"LastMaintenance"
|
||||
"VHL_NAME",
|
||||
"NODE_ID",
|
||||
"REAL_ID",
|
||||
"VHL_STATE",
|
||||
"BATT",
|
||||
"JOB_ID",
|
||||
"TIMESTAMP",
|
||||
};
|
||||
}
|
||||
|
||||
public override void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
Debug.Log($"OnPointerClick3: {gameObject.name}");
|
||||
base.OnPointerClick(eventData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 내부 상태를 업데이트하고 정렬된 마스크에 정의된 특정 키에 대한 작업을 수행하여 제공된 데이터 객체를 처리합니다.
|
||||
// </summary>
|
||||
@@ -119,11 +118,32 @@ namespace UVC.Factory.Component
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 매 프레임마다 현재 위치에서 목표 위치로 부드럽게 이동시킵니다.
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * moveSpeed);
|
||||
if (transform.position != targetPosition)
|
||||
{
|
||||
if (Vector3.Distance(transform.position, targetPosition) < 0.1f)
|
||||
{
|
||||
// 현재 위치와 목표 위치 사이의 거리가 임계값을 초과하면 순간이동합니다.
|
||||
transform.position = targetPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 매 프레임마다 현재 위치에서 목표 위치로 부드럽게 이동시킵니다.
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * moveSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// 매 프레임마다 현재 회전에서 목표 회전으로 부드럽게 회전시킵니다.
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotationSpeed);
|
||||
if (transform.rotation != targetRotation)
|
||||
{
|
||||
if (Quaternion.Angle(transform.rotation, targetRotation) < 0.1f)
|
||||
{
|
||||
transform.rotation = targetRotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 매 프레임마다 현재 회전에서 목표 회전으로 부드럽게 회전시킵니다.
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotationSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace UVC.Factory.Component
|
||||
{
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class ClickTest : MonoBehaviour
|
||||
{
|
||||
|
||||
private InputSystemActions inputSystemActions;
|
||||
private Camera mainCamera;
|
||||
|
||||
//void Start()
|
||||
//{
|
||||
// Collider col = GetComponent<Collider>();
|
||||
// if (col != null)
|
||||
// {
|
||||
// // 콜라이더의 중심과 크기 출력 (BoxCollider의 경우)
|
||||
// if (col is BoxCollider boxCollider)
|
||||
// {
|
||||
// Debug.Log($"BoxCollider - Center: {boxCollider.center}, Size: {boxCollider.size}");
|
||||
// }
|
||||
|
||||
// // 콜라이더의 월드 바운드 출력
|
||||
// Debug.Log($"콜라이더의 월드 바운드: {col.bounds}");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Debug.LogError($"{gameObject.name}에 콜라이더가 없습니다!");
|
||||
// }
|
||||
//}
|
||||
|
||||
void OnMouseDown()
|
||||
{
|
||||
Debug.Log($"OnMouseDown: {gameObject.name}");
|
||||
transform.localScale -= new Vector3(0.05f, 0.05f, 0);
|
||||
//or
|
||||
//transform.GetComponent<SpriteRenderer>().color += new Color(40, 40, 40);
|
||||
}
|
||||
|
||||
void OnMouseUp()
|
||||
{
|
||||
Debug.Log($"OnMouseUp: {gameObject.name}");
|
||||
transform.localScale += new Vector3(0.05f, 0.05f, 0);
|
||||
//or
|
||||
//transform.GetComponent<SpriteRenderer>().color -= new Color(40, 40, 40);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (UnityEngine.Input.GetMouseButtonDown(0)) // 0 represents the left mouse button
|
||||
{
|
||||
|
||||
Ray ray = Camera.main.ScreenPointToRay(UnityEngine.Input.mousePosition);
|
||||
RaycastHit hit;
|
||||
|
||||
// 레이 시작점과 방향 로깅
|
||||
Debug.Log($"Ray 원점: {ray.origin}, 방향: {ray.direction}");
|
||||
|
||||
// 디버그용 레이를 씬 뷰에 그립니다
|
||||
Debug.DrawRay(ray.origin, ray.direction * 100, Color.red, 60f);
|
||||
|
||||
bool hitSomething = Physics.Raycast(ray, out hit);
|
||||
Debug.Log($"Mouse button clicked - hit something: {hitSomething}");
|
||||
|
||||
if (hitSomething)
|
||||
{
|
||||
Debug.Log($"Hit object: {hit.collider.gameObject.name}, distance: {hit.distance}, point: {hit.point}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 모든 콜라이더 로깅
|
||||
Collider[] allColliders = FindObjectsByType<Collider>(FindObjectsSortMode.None);
|
||||
Debug.Log($"씬에 있는 콜라이더 수: {allColliders.Length}");
|
||||
|
||||
foreach (var collider in allColliders)
|
||||
{
|
||||
if (collider.gameObject.activeInHierarchy && collider.enabled)
|
||||
Debug.Log($"활성화된 콜라이더: {collider.gameObject.name}, 레이어: {LayerMask.LayerToName(collider.gameObject.layer)}");
|
||||
}
|
||||
}
|
||||
|
||||
ray = Camera.main.ScreenPointToRay(UnityEngine.Input.mousePosition);
|
||||
RaycastHit[] tempinfo = new RaycastHit[100];
|
||||
var hitcount = Physics.RaycastNonAlloc(ray, tempinfo, Mathf.Infinity);
|
||||
Debug.Log($"Mouse button clicked hitcount:{hitcount} {Physics.Raycast(ray, out hit)}");
|
||||
|
||||
if (Physics.Raycast(ray, out hit))
|
||||
{
|
||||
// Check if the hit object has the ClickableObject script
|
||||
ClickTest clickableObject = hit.collider.GetComponent<ClickTest>();
|
||||
if (clickableObject != null)
|
||||
{
|
||||
// 수동으로 이벤트 호출
|
||||
PointerEventData pointerData = new PointerEventData(EventSystem.current);
|
||||
pointerData.position = UnityEngine.Input.mousePosition;
|
||||
OnPointerClick(pointerData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("Clicked on: " + hit.collider.gameObject.name + ", but it's not clickable.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if (Mouse.current.leftButton.wasPressedThisFrame)
|
||||
//{
|
||||
// Debug.Log("마우스 클릭 감지");
|
||||
//}
|
||||
}
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
Debug.Log($"OnPointerClick1: {gameObject.name}");
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 포인터가 이 객체 위로 들어왔을 때 호출됩니다. 하이라이트 효과 등에 사용할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <param name="eventData">포인터 이벤트와 관련된 데이터입니다.</param>
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
Debug.Log($"OnPointerEnter: {gameObject.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 포인터가 이 객체에서 벗어났을 때 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="eventData">포인터 이벤트와 관련된 데이터입니다.</param>
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
Debug.Log($"OnPointerExit: {gameObject.name}");
|
||||
}
|
||||
|
||||
public void OnMouseClick()
|
||||
{
|
||||
Debug.Log($"OnClick: {gameObject.name}");
|
||||
}
|
||||
public void OnMouseEnter()
|
||||
{
|
||||
Debug.Log($"OnMouseEnter: {gameObject.name}");
|
||||
}
|
||||
|
||||
public void OnMouseExit()
|
||||
{
|
||||
Debug.Log($"OnMouseExit: {gameObject.name}");
|
||||
}
|
||||
|
||||
public void OnMouseWheel()
|
||||
{
|
||||
Debug.Log($"OnMouseWheel: {gameObject.name}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bf81994712147c45b75b5394f2b0aca
|
||||
@@ -61,7 +61,6 @@ namespace UVC.Factory.Component
|
||||
/// <param name="eventData">포인터 클릭과 관련된 이벤트 데이터입니다.</param>
|
||||
public override void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
Debug.Log($"OnPointerClick2: {gameObject.name}");
|
||||
if (data != null && data.Count > 0)
|
||||
{
|
||||
Dictionary<string, object> info = new Dictionary<string, object>();
|
||||
@@ -81,7 +80,6 @@ namespace UVC.Factory.Component
|
||||
// dataOrderedMask가 설정되어 있지 않으면 모든 데이터를 사용합니다.
|
||||
info = new Dictionary<string, object>(data);
|
||||
}
|
||||
Debug.Log($"OnPointerClick: {gameObject.name} - {info.Count} items");
|
||||
InfoWindow.Instance.Show(transform, info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,25 +20,18 @@ namespace UVC.Object3d
|
||||
/// 포인터로 이 객체를 클릭했을 때 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="eventData">클릭 이벤트와 관련된 데이터입니다.</param>
|
||||
public virtual void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
Debug.Log($"OnPointerClick1: {gameObject.name}");
|
||||
}
|
||||
public virtual void OnPointerClick(PointerEventData eventData) {}
|
||||
|
||||
/// <summary>
|
||||
/// 포인터가 이 객체 위로 들어왔을 때 호출됩니다. 하이라이트 효과 등에 사용할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <param name="eventData">포인터 이벤트와 관련된 데이터입니다.</param>
|
||||
public virtual void OnPointerEnter(PointerEventData eventData) {
|
||||
Debug.Log($"OnPointerEnter: {gameObject.name}");
|
||||
}
|
||||
public virtual void OnPointerEnter(PointerEventData eventData) {}
|
||||
|
||||
/// <summary>
|
||||
/// 포인터가 이 객체에서 벗어났을 때 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="eventData">포인터 이벤트와 관련된 데이터입니다.</param>
|
||||
public virtual void OnPointerExit(PointerEventData eventData) {
|
||||
Debug.Log($"OnPointerExit: {gameObject.name}");
|
||||
}
|
||||
public virtual void OnPointerExit(PointerEventData eventData) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 745da5f00db92f84cb12049e0ca9cd85
|
||||
guid: 1ea984474e64af94d8fa097f80657cca
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
218
Assets/Scripts/UVC/Threading/MainThreadDispatcher.cs
Normal file
218
Assets/Scripts/UVC/Threading/MainThreadDispatcher.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity 메인 스레드에서 작업을 실행하기 위한 디스패처 클래스입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 싱글톤 패턴으로 구현되어 있으며, 백그라운드 스레드에서 메인 스레드(UI 스레드)로
|
||||
/// 작업을 디스패치하는 기능을 제공합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 백그라운드 스레드에서 UI 업데이트를 해야 하는 경우 (Task 사용)
|
||||
/// await Task.Run(() =>
|
||||
/// {
|
||||
/// // 백그라운드 작업 수행
|
||||
/// var result = ComputeIntensiveTask();
|
||||
///
|
||||
/// // UI 업데이트는 메인 스레드에서 수행
|
||||
/// MainThreadDispatcher.Instance.Enqueue(() => {
|
||||
/// UpdateUIWithResult(result);
|
||||
/// });
|
||||
/// });
|
||||
///
|
||||
/// // UniTask를 사용한 비동기 작업과 메인 스레드 디스패치 예제
|
||||
/// async UniTask ProcessDataInBackgroundAsync()
|
||||
/// {
|
||||
/// // 스레드풀에서 실행
|
||||
/// await UniTask.SwitchToThreadPool();
|
||||
///
|
||||
/// try
|
||||
/// {
|
||||
/// // 백그라운드에서 시간이 걸리는 작업 수행
|
||||
/// var data = await FetchDataFromServerAsync();
|
||||
/// var processedData = ProcessLargeData(data);
|
||||
///
|
||||
/// // 결과를 메인 스레드로 전달하여 UI 업데이트
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// // 여기에서 UI 컴포넌트를 안전하게 업데이트
|
||||
/// UpdateUI(processedData);
|
||||
/// ShowSuccessMessage("데이터 처리 완료");
|
||||
/// });
|
||||
/// }
|
||||
/// catch (Exception ex)
|
||||
/// {
|
||||
/// // 오류 발생 시에도 메인 스레드에서 UI 관련 작업 처리
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// ShowErrorDialog($"오류 발생: {ex.Message}");
|
||||
/// Debug.LogException(ex);
|
||||
/// });
|
||||
/// }
|
||||
/// finally
|
||||
/// {
|
||||
/// // 작업 완료 후 필요한 경우 다시 메인 스레드로 전환
|
||||
/// await UniTask.SwitchToMainThread();
|
||||
/// // 여기서부터는 다시 메인 스레드에서 실행됨
|
||||
/// CleanupResources();
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // MainThreadDispatcher와 UniTask를 함께 활용한 이벤트 핸들러 예제
|
||||
/// public async void OnButtonClick()
|
||||
/// {
|
||||
/// // UI 상태 업데이트
|
||||
/// loadingIndicator.SetActive(true);
|
||||
///
|
||||
/// try
|
||||
/// {
|
||||
/// // 스레드풀로 전환하여 무거운 작업 수행
|
||||
/// await UniTask.SwitchToThreadPool();
|
||||
///
|
||||
/// // 시간이 걸리는 작업 수행
|
||||
/// var result = await PerformHeavyComputationAsync();
|
||||
///
|
||||
/// // 결과를 UI에 반영 (메인 스레드에서)
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// resultText.text = result.ToString();
|
||||
/// loadingIndicator.SetActive(false);
|
||||
/// });
|
||||
/// }
|
||||
/// catch (Exception ex)
|
||||
/// {
|
||||
/// // 예외 처리도 메인 스레드에서
|
||||
/// MainThreadDispatcher.Instance.SendToMainThread(() => {
|
||||
/// errorText.text = ex.Message;
|
||||
/// loadingIndicator.SetActive(false);
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class MainThreadDispatcher : MonoBehaviour
|
||||
{
|
||||
private static MainThreadDispatcher _instance;
|
||||
private static bool _instantiated;
|
||||
private static SynchronizationContext _mainThreadContext;
|
||||
|
||||
/// <summary>
|
||||
/// MainThreadDispatcher의 단일 인스턴스를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 인스턴스가 아직 생성되지 않은 경우, 새로운 GameObject를 생성하고
|
||||
/// 그 GameObject에 MainThreadDispatcher 컴포넌트를 추가합니다.
|
||||
/// </remarks>
|
||||
public static MainThreadDispatcher Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_instantiated)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 스레드 디스패처를 초기화합니다.
|
||||
/// </summary>
|
||||
private static void Initialize()
|
||||
{
|
||||
if (!_instantiated)
|
||||
{
|
||||
// 이미 존재하는지 확인
|
||||
_instance = FindFirstObjectByType<MainThreadDispatcher>();
|
||||
|
||||
// 없으면 새로 생성
|
||||
if (_instance == null)
|
||||
{
|
||||
var go = new GameObject("MainThreadDispatcher");
|
||||
_instance = go.AddComponent<MainThreadDispatcher>();
|
||||
DontDestroyOnLoad(go); // 씬 전환 시에도 유지
|
||||
}
|
||||
|
||||
_instantiated = true;
|
||||
_mainThreadContext = SynchronizationContext.Current;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ConcurrentQueue<Action> _actionQueue = new ConcurrentQueue<Action>();
|
||||
|
||||
/// <summary>
|
||||
/// 메인 스레드에서 실행할 액션을 큐에 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="action">메인 스레드에서 실행할 액션</param>
|
||||
public void Enqueue(Action action)
|
||||
{
|
||||
if (action == null) return;
|
||||
|
||||
// 현재 스레드가 메인 스레드면 바로 실행
|
||||
if (Thread.CurrentThread.ManagedThreadId == 1)
|
||||
{
|
||||
action.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// 아니면 큐에 추가하여 나중에 실행
|
||||
_actionQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SynchronizationContext를 통해 메인 스레드에서 액션을 실행합니다.
|
||||
/// </summary>
|
||||
/// <param name="action">메인 스레드에서 실행할 액션</param>
|
||||
public void SendToMainThread(Action action)
|
||||
{
|
||||
if (action == null) return;
|
||||
|
||||
// SynchronizationContext가 있으면 사용
|
||||
if (_mainThreadContext != null)
|
||||
{
|
||||
_mainThreadContext.Post(_ => action(), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// SynchronizationContext가 없으면 큐에 추가
|
||||
Enqueue(action);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 큐에 있는 모든 액션 처리
|
||||
while (_actionQueue.TryDequeue(out Action action))
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = this;
|
||||
_mainThreadContext = SynchronizationContext.Current;
|
||||
_instantiated = true;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else if (_instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 116a97c0eb67f214388de1a4c61c6046
|
||||
@@ -15,9 +15,13 @@ namespace UVC.UI.Info
|
||||
public class InfoWindow : SingletonScene<InfoWindow>
|
||||
{
|
||||
|
||||
[Tooltip("정보 텍스트를 표시할 UI 요소")]
|
||||
[Tooltip("Label 정보 텍스트를 표시할 UI 요소")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI infoText;
|
||||
private TextMeshProUGUI labelText;
|
||||
|
||||
[Tooltip("Value 정보 텍스트를 표시할 UI 요소")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI valueText;
|
||||
|
||||
[Tooltip("정보 창을 닫을 버튼")]
|
||||
[SerializeField]
|
||||
@@ -25,7 +29,7 @@ namespace UVC.UI.Info
|
||||
|
||||
[Tooltip("UI가 객체를 가리지 않도록 할 월드 좌표계 오프셋")]
|
||||
[SerializeField]
|
||||
private Vector3 worldOffset = new Vector3(0, 1.5f, 0);
|
||||
private Vector2 screenOffset = new Vector2(0f, 0f);
|
||||
|
||||
// 정보 창이 따라다닐 3D 객체의 Transform
|
||||
private Transform? target;
|
||||
@@ -47,6 +51,9 @@ namespace UVC.UI.Info
|
||||
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)
|
||||
{
|
||||
@@ -57,10 +64,73 @@ namespace UVC.UI.Info
|
||||
private void LateUpdate()
|
||||
{
|
||||
// target이 설정되어 있고 활성화 상태일 때만 위치와 방향을 업데이트합니다.
|
||||
if (target != null && gameObject.activeSelf)
|
||||
if (target != null && gameObject.activeSelf && Camera.main != null)
|
||||
{
|
||||
// 위치 업데이트
|
||||
transform.position = target.position + worldOffset;
|
||||
// 타겟의 렌더러 또는 콜라이더를 가져와 화면 상의 크기를 계산
|
||||
Bounds bounds = new Bounds(target.position, Vector3.one);
|
||||
Renderer? renderer = target.GetComponent<Renderer>();
|
||||
Collider? collider = target.GetComponent<Collider>();
|
||||
|
||||
// 렌더러가 있으면 렌더러의 바운드를, 없으면 콜라이더의 바운드를 사용
|
||||
if (renderer != null)
|
||||
{
|
||||
bounds = renderer.bounds;
|
||||
}
|
||||
else if (collider != null)
|
||||
{
|
||||
bounds = collider.bounds;
|
||||
}
|
||||
|
||||
// 바운드의 오른쪽 지점을 월드 좌표로 계산
|
||||
Vector3 rightPoint = bounds.center + new Vector3(bounds.extents.x, 0, 0);
|
||||
|
||||
// 바운드의 오른쪽 지점을 스크린 좌표로 변환
|
||||
Vector3 screenPosRight = Camera.main.WorldToScreenPoint(rightPoint);
|
||||
|
||||
// 추가 오프셋 적용
|
||||
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);
|
||||
|
||||
// RectTransform을 사용하여 UI 위치 설정
|
||||
RectTransform? rectTransform = transform as RectTransform;
|
||||
if (rectTransform != null)
|
||||
{
|
||||
// 캔버스의 렌더링 모드에 따라 다르게 처리
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI가 항상 보이도록 카메라를 향하게 설정 (World Space Canvas인 경우에만 필요)
|
||||
Canvas parentCanvas = GetComponentInParent<Canvas>();
|
||||
if (parentCanvas != null && parentCanvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
transform.LookAt(transform.position + Camera.main.transform.rotation * Vector3.forward,
|
||||
Camera.main.transform.rotation * Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +142,29 @@ namespace UVC.UI.Info
|
||||
public void Show(Transform targetObject, Dictionary<string, object> information)
|
||||
{
|
||||
target = targetObject;
|
||||
if (infoText != null)
|
||||
if (labelText != null)
|
||||
{
|
||||
infoText.text = information.ToString();
|
||||
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);
|
||||
}
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
// 즉시 위치와 방향을 업데이트합니다.
|
||||
|
||||
@@ -4,7 +4,7 @@ using System; // System.Type 사용을 위해 추가
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using UVC.Log;
|
||||
using UVC.util;
|
||||
using UVC.Util;
|
||||
|
||||
namespace UVC.UI.Modal
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ using UVC.Extension;
|
||||
using UVC.Locale;
|
||||
using UVC.UI.Toolbar.Model;
|
||||
using UVC.UI.Tooltip;
|
||||
using UVC.util;
|
||||
using UVC.Util;
|
||||
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UVC.Locale;
|
||||
using UVC.util; // LocalizationManager를 사용한다면 필요합니다.
|
||||
using UVC.Util; // LocalizationManager를 사용한다면 필요합니다.
|
||||
|
||||
namespace UVC.UI.Tooltip
|
||||
{
|
||||
|
||||
244
Assets/Scripts/UVC/Util/CameraController.cs
Normal file
244
Assets/Scripts/UVC/Util/CameraController.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UVC.Core;
|
||||
|
||||
namespace UVC.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// 마우스 입력에 따라 카메라를 이동, 회전, 줌하는 컨트롤러입니다.
|
||||
/// - 마우스 가운데 버튼 드래그: 카메라 평행 이동 (Pan)
|
||||
/// - 마우스 오른쪽 버튼 드래그: 카메라 회전 (Orbit)
|
||||
/// - 마우스 휠 스크롤: 카메라 줌 (Zoom)
|
||||
/// </summary>
|
||||
public class CameraController : SingletonScene<CameraController>
|
||||
{
|
||||
[Tooltip("카메라 평행 이동 속도")]
|
||||
public float panSpeed = 20f;
|
||||
|
||||
[Tooltip("카메라 회전 속도")]
|
||||
public float rotationSpeed = 300f;
|
||||
|
||||
[Tooltip("카메라 줌 속도")]
|
||||
public float zoomSpeed = 10f;
|
||||
|
||||
[Header("Movement Smoothing")]
|
||||
[Tooltip("패닝 시 마우스 이동량의 최대값을 제한하여, 프레임 드랍 시 카메라가 급격하게 튀는 현상을 방지합니다.")]
|
||||
public float maxPanDelta = 50f;
|
||||
|
||||
private Vector3 lastPanPosition;
|
||||
private Vector3 rotationPivot;
|
||||
private bool isRotating = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 스크립트 시작 시, 회전의 기준이 되는 중심점을 카메라 앞쪽으로 초기화합니다.
|
||||
rotationPivot = transform.position + transform.forward * 10f;
|
||||
}
|
||||
|
||||
// Update 대신 LateUpdate를 사용하여 카메라 움직임이 다른 모든 업데이트 이후에 처리되도록 합니다.
|
||||
// 이를 통해 카메라의 떨림이나 끊김 현상을 줄일 수 있습니다.
|
||||
void LateUpdate()
|
||||
{
|
||||
HandlePanning();
|
||||
HandleRotation();
|
||||
HandleZoom();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 가운데 버튼으로 카메라를 평행 이동시킵니다.
|
||||
/// 프레임 지연으로 인한 급격한 이동을 방지하기 위해 이동량을 제한합니다.
|
||||
/// </summary>
|
||||
private void HandlePanning()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(2))
|
||||
{
|
||||
lastPanPosition = Input.mousePosition;
|
||||
}
|
||||
|
||||
if (Input.GetMouseButton(2))
|
||||
{
|
||||
Vector3 delta = Input.mousePosition - lastPanPosition;
|
||||
|
||||
// 프레임 드랍 시 델타 값이 너무 커져서 카메라가 튀는 것을 방지하기 위해 최대값을 제한합니다.
|
||||
if (delta.magnitude > maxPanDelta)
|
||||
{
|
||||
delta = delta.normalized * maxPanDelta;
|
||||
}
|
||||
|
||||
// 카메라의 로컬 좌표계를 기준으로 이동량을 변환하여 월드 좌표계에서 이동시킵니다.
|
||||
transform.Translate(transform.right * -delta.x * panSpeed * Time.deltaTime, Space.World);
|
||||
transform.Translate(transform.up * -delta.y * panSpeed * Time.deltaTime, Space.World);
|
||||
|
||||
lastPanPosition = Input.mousePosition;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 오른쪽 버튼으로 카메라를 회전시킵니다.
|
||||
/// 회전 축이 변하는 것을 방지하여 안정적인 회전을 구현합니다.
|
||||
/// </summary>
|
||||
private void HandleRotation()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(1))
|
||||
{
|
||||
isRotating = true;
|
||||
// 마우스 클릭 지점으로 Ray를 쏴서 회전의 중심점(pivot)을 설정합니다.
|
||||
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, 1000f))
|
||||
{
|
||||
rotationPivot = hit.point;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ray가 아무 오브젝트에도 맞지 않았다면, 카메라 앞쪽의 특정 거리를 중심점으로 사용합니다.
|
||||
rotationPivot = transform.position + transform.forward * 10f;
|
||||
}
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonUp(1))
|
||||
{
|
||||
isRotating = false;
|
||||
}
|
||||
|
||||
if (isRotating && Input.GetMouseButton(1))
|
||||
{
|
||||
float mouseX = Input.GetAxis("Mouse X") * rotationSpeed * Time.deltaTime;
|
||||
float mouseY = Input.GetAxis("Mouse Y") * rotationSpeed * Time.deltaTime;
|
||||
|
||||
// 수평 회전으로 인해 수직 회전 축(transform.right)이 변질되는 것을 방지하기 위해
|
||||
// 회전 전의 right 벡터를 미리 저장해 둡니다.
|
||||
Vector3 verticalRotationAxis = transform.right;
|
||||
|
||||
// 설정된 중심점을 기준으로 카메라를 회전시킵니다.
|
||||
// 1. 수평 회전 (월드 Y축 기준)
|
||||
transform.RotateAround(rotationPivot, Vector3.up, -mouseX);
|
||||
// 2. 수직 회전 (미리 저장해 둔 카메라의 오른쪽 축 기준)
|
||||
transform.RotateAround(rotationPivot, verticalRotationAxis, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 휠로 카메라를 줌 인/아웃합니다.
|
||||
/// </summary>
|
||||
private void HandleZoom()
|
||||
{
|
||||
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
||||
if (scroll != 0f)
|
||||
{
|
||||
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
Vector3 zoomTarget;
|
||||
|
||||
if (Physics.Raycast(ray, out RaycastHit hit))
|
||||
{
|
||||
zoomTarget = hit.point;
|
||||
}
|
||||
else
|
||||
{
|
||||
zoomTarget = ray.GetPoint(1000);
|
||||
}
|
||||
|
||||
Vector3 direction = zoomTarget - transform.position;
|
||||
|
||||
// 줌 실행
|
||||
transform.position += direction.normalized * scroll * zoomSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 Transform을 중심으로 카메라를 포커싱합니다.
|
||||
/// </summary>
|
||||
/// <param name="equipmentTransform">포커스할 대상의 Transform</param>
|
||||
/// <param name="distance">대상과의 거리</param>
|
||||
public void FocusOnTargetFast(Transform equipmentTransform, float distance)
|
||||
{
|
||||
if (equipmentTransform == null)
|
||||
return;
|
||||
|
||||
// 카메라가 바라볼 대상의 중심점
|
||||
Vector3 targetPosition = equipmentTransform.position;
|
||||
|
||||
// 현재 카메라의 회전각을 유지하면서 타겟을 바라보는 방향 설정
|
||||
Vector3 directionToTarget = (targetPosition - transform.position).normalized;
|
||||
|
||||
// 타겟으로부터 지정된 거리만큼 떨어진 위치 계산
|
||||
Vector3 newPosition = targetPosition - directionToTarget * distance;
|
||||
|
||||
// 카메라 위치 설정 및 타겟을 바라보도록 함
|
||||
transform.position = newPosition;
|
||||
transform.LookAt(targetPosition);
|
||||
|
||||
// 회전 피봇 포인트 업데이트
|
||||
rotationPivot = targetPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 Transform을 중심으로 카메라를 포커싱합니다.
|
||||
/// </summary>
|
||||
/// <param name="equipmentTransform">포커스할 대상의 Transform</param>
|
||||
/// <param name="distance">대상과의 거리</param>
|
||||
/// <param name="duration">이동에 걸리는 시간(초), 기본값 1초</param>
|
||||
public void FocusOnTarget(Transform equipmentTransform, float distance, float duration = 1.0f)
|
||||
{
|
||||
if (equipmentTransform == null)
|
||||
return;
|
||||
|
||||
// 코루틴을 사용하여 부드러운 이동 구현
|
||||
StartCoroutine(SmoothFocusOnTarget(equipmentTransform, distance, duration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 부드럽게 타겟까지 이동하는 코루틴
|
||||
/// </summary>
|
||||
private IEnumerator SmoothFocusOnTarget(Transform targetTransform, float distance, float duration)
|
||||
{
|
||||
// 카메라가 바라볼 대상의 중심점
|
||||
Vector3 targetPosition = targetTransform.position;
|
||||
|
||||
// 시작 위치와 회전 저장
|
||||
Vector3 startPosition = transform.position;
|
||||
Quaternion startRotation = transform.rotation;
|
||||
|
||||
// 타겟을 보는 최종 위치와 회전 계산
|
||||
Vector3 directionToTarget = (targetPosition - startPosition).normalized;
|
||||
Vector3 endPosition = targetPosition - directionToTarget * distance;
|
||||
|
||||
// 최종 회전값 계산
|
||||
Quaternion endRotation = Quaternion.LookRotation(targetPosition - endPosition);
|
||||
|
||||
// 이동 시간 계산을 위한 변수
|
||||
float elapsedTime = 0f;
|
||||
|
||||
while (elapsedTime < duration)
|
||||
{
|
||||
elapsedTime += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsedTime / duration);
|
||||
|
||||
// 부드러운 이동을 위한 Easing 함수 적용
|
||||
float smoothT = EaseInOutCubic(t);
|
||||
|
||||
// 위치와 회전 보간
|
||||
transform.position = Vector3.Lerp(startPosition, endPosition, smoothT);
|
||||
transform.rotation = Quaternion.Slerp(startRotation, endRotation, smoothT);
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 정확한 최종 위치와 회전 설정
|
||||
transform.position = endPosition;
|
||||
transform.LookAt(targetPosition);
|
||||
|
||||
// 회전 피봇 포인트 업데이트
|
||||
rotationPivot = targetPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cubic ease-in/out 함수로 부드러운 이동 효과를 줍니다.
|
||||
/// </summary>
|
||||
private float EaseInOutCubic(float t)
|
||||
{
|
||||
return t < 0.5f ? 4f * t * t * t : 1f - Mathf.Pow(-2f * t + 2f, 3f) / 2f;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Util/CameraController.cs.meta
Normal file
2
Assets/Scripts/UVC/Util/CameraController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61deddb674c074049a9b43fd58f1b355
|
||||
@@ -1,58 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.input
|
||||
{
|
||||
public class InputHandler : MonoBehaviour
|
||||
{
|
||||
public Transform clickedObject;
|
||||
|
||||
// 프레임당 한 번씩 업데이트가 호출됩니다
|
||||
void Update()
|
||||
{
|
||||
// 터치 입력 확인
|
||||
if (UnityEngine.Input.touchCount > 0)
|
||||
{
|
||||
if (UnityEngine.Input.GetTouch(0).phase == TouchPhase.Began)
|
||||
{
|
||||
handleClick(UnityEngine.Input.GetTouch(0).position);
|
||||
}
|
||||
}
|
||||
// 왼쪽 마우스 버튼 클릭 확인
|
||||
if (UnityEngine.Input.GetMouseButtonDown(0))
|
||||
{
|
||||
handleClick(UnityEngine.Input.mousePosition);
|
||||
}
|
||||
}
|
||||
|
||||
void handleClick(Vector3 screenClickPosition)
|
||||
{
|
||||
// 클릭한 위치에서 카메라에 수직인 Ray를 그립니다
|
||||
// Ray가 교차하는 첫 번째 객체를 캡처하기 위해 RayHit를 생성합니다
|
||||
Ray ray = Camera.main.ScreenPointToRay(screenClickPosition);
|
||||
RaycastHit rayHit;
|
||||
if (Physics.Raycast(ray.origin, ray.direction, out rayHit))
|
||||
{
|
||||
Debug.Log($"Ray Origin: {ray.origin}, Direction: {ray.direction}");
|
||||
if (rayHit.transform.tag == "Clickable")
|
||||
{
|
||||
// 클릭한 객체에 어떤 작업을 합니다.
|
||||
clickedObject = rayHit.transform;
|
||||
// 다음 클릭을 위해 ray와 rayHit을 재설정합니다.
|
||||
ray = new Ray();
|
||||
rayHit = new RaycastHit();
|
||||
}
|
||||
else if (clickedObject != null)
|
||||
{
|
||||
// 클릭한 객체의 y 위치에서 xz 축에 평면을 그립니다.
|
||||
Plane xzPlane = new Plane(Vector3.up, clickedObject.position);
|
||||
float distance; // 클릭과 평면이 교차 하는 지점을 찾습니다.
|
||||
// 그리고 클릭한 객체를 새로운 위치로 이동합니다.
|
||||
if (xzPlane.Raycast(ray, out distance))
|
||||
{
|
||||
clickedObject.position = ray.GetPoint(distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8de502d6dd9c6114f993b104854864c5
|
||||
@@ -1,79 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace UVC.input
|
||||
{
|
||||
public class MouseInputHandler : MonoBehaviour
|
||||
{
|
||||
private MouseControls mouseControls;
|
||||
private Camera mainCamera;
|
||||
|
||||
private GameObject lastHoveredObject = null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
mouseControls = new MouseControls();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
mouseControls.Player.Enable();
|
||||
mouseControls.Player.Click.performed += OnClick;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
mouseControls.Player.Disable();
|
||||
mouseControls.Player.Click.performed -= OnClick;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 마우스 위치로 Ray 발사
|
||||
Ray ray = mainCamera.ScreenPointToRay(mouseControls.Player.Point.ReadValue<Vector2>());
|
||||
RaycastHit hit;
|
||||
|
||||
GameObject currentHoveredObject = null;
|
||||
|
||||
if (Physics.Raycast(ray, out hit))
|
||||
{
|
||||
// Ray에 맞은 객체가 있을 경우
|
||||
currentHoveredObject = hit.collider.gameObject;
|
||||
}
|
||||
|
||||
// 마우스 오버/아웃 상태 변경 감지
|
||||
if (currentHoveredObject != lastHoveredObject)
|
||||
{
|
||||
// 이전에 가리키던 객체가 있었다면 Mouse Out 이벤트 호출
|
||||
if (lastHoveredObject != null)
|
||||
{
|
||||
// 여기에 Mouse Out 로직을 추가하세요.
|
||||
Debug.Log("Mouse Out: " + lastHoveredObject.name);
|
||||
// 예: lastHoveredObject.GetComponent<Renderer>().material.color = Color.white;
|
||||
}
|
||||
|
||||
// 현재 가리키는 객체가 있다면 Mouse Over 이벤트 호출
|
||||
if (currentHoveredObject != null)
|
||||
{
|
||||
// 여기에 Mouse Over 로직을 추가하세요.
|
||||
Debug.Log("Mouse Over: " + currentHoveredObject.name);
|
||||
// 예: currentHoveredObject.GetComponent<Renderer>().material.color = Color.red;
|
||||
}
|
||||
|
||||
lastHoveredObject = currentHoveredObject;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClick(InputAction.CallbackContext context)
|
||||
{
|
||||
Debug.Log("Clicked");
|
||||
// 현재 마우스가 가리키고 있는 객체가 있을 때만 클릭 이벤트 처리
|
||||
if (lastHoveredObject != null)
|
||||
{
|
||||
// 여기에 Mouse Click 로직을 추가하세요.
|
||||
Debug.Log("Clicked on: " + lastHoveredObject.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72b9b338934540349a2d195547344dab
|
||||
@@ -2,7 +2,7 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UVC.util
|
||||
namespace UVC.Util
|
||||
{
|
||||
public static class CanvasUtil
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user