menu 개발 중. 언어 변경 시 반영 않됨
This commit is contained in:
@@ -1,11 +1,8 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Unity.VisualScripting.Antlr3.Runtime;
|
||||
using UnityEngine;
|
||||
using UVC.Log;
|
||||
|
||||
namespace UVC.Data
|
||||
@@ -378,7 +375,7 @@ namespace UVC.Data
|
||||
private object ConvertJTokenToObject(JToken token)
|
||||
{
|
||||
if (token == null) return null;
|
||||
|
||||
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Object:
|
||||
@@ -504,7 +501,7 @@ namespace UVC.Data
|
||||
// 예: maskTemplates.Count가 3이고, sourceArray.Count가 5인 경우
|
||||
// 0, 1, 2 인덱스는 maskTemplates[0], maskTemplates[1], maskTemplates[2] 사용
|
||||
// 3, 4 인덱스는 저장 않함
|
||||
if( i >= maskTemplates.Count)
|
||||
if (i >= maskTemplates.Count)
|
||||
continue;
|
||||
int templateIndex = Math.Min(i, maskTemplates.Count - 1);
|
||||
maskTemplate = maskTemplates[templateIndex];
|
||||
|
||||
@@ -233,15 +233,16 @@ namespace UVC.Data
|
||||
{
|
||||
try
|
||||
{
|
||||
string? responseData = info.ResponseMask.Apply(result);
|
||||
// responseData가 null인 경우는 올바른 응답이 아니므로 처리하지 않음
|
||||
if (responseData == null)
|
||||
HttpResponseResult responseResult = info.ResponseMask.Apply(result);
|
||||
// 응답 마스크 적용 결과가 성공이 아니면 실패 핸들러 호출 후 반환
|
||||
if (!responseResult.IsSuccess)
|
||||
{
|
||||
throw new Exception($"유효하지 않은 response data. {result}");
|
||||
info.FailHandler?.Invoke(responseResult.Message!);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = responseData.Trim();
|
||||
result = responseResult.Data!.Trim();
|
||||
}
|
||||
|
||||
|
||||
@@ -299,11 +300,11 @@ namespace UVC.Data
|
||||
// 갱신 된 데이터가 있는 경우 핸들러 호출
|
||||
if (info.UpdatedDataOnly)
|
||||
{
|
||||
if (dataObject != null && dataObject.UpdatedCount > 0) info.Handler?.Invoke(dataObject);
|
||||
if (dataObject != null && dataObject.UpdatedCount > 0) info.SuccessHandler?.Invoke(dataObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Handler?.Invoke(dataObject);
|
||||
info.SuccessHandler?.Invoke(dataObject);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace UVC.Data
|
||||
/// <code>
|
||||
/// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/data", "GET")
|
||||
/// .setDataMapper(new DataMapper(dataMask))
|
||||
/// .setHandler(data => Console.WriteLine(data))
|
||||
/// .setSuccessHandler(data => Console.WriteLine(data)) // 성공 핸들러 예시
|
||||
/// .setFailHandler(errorData => Console.Error.WriteLine(errorData)) // 실패 핸들러 예시
|
||||
/// .setRetry(5, 2000)
|
||||
/// .setRepeat(true, 10, 5000);
|
||||
/// </code>
|
||||
@@ -30,12 +31,13 @@ namespace UVC.Data
|
||||
private string _method;
|
||||
private Dictionary<string, string>? _headers = null;
|
||||
private string? _body = null;
|
||||
private Action<IDataObject?>? _handler = null;
|
||||
private Action<IDataObject?>? _successhandler = null; //요청 성공 시 호출될 내부 핸들러입니다.
|
||||
private Action<string>? _failhandler = null; //요청 실패 시 호출될 내부 핸들러입니다.
|
||||
private bool _repeat = false; // 반복 실행 여부
|
||||
private int _repeatCount = 0; // 반복 횟수, 0은 무한 반복
|
||||
private int _repeatInterval = 1000; // 반복 간격 (ms)
|
||||
private DataMapper? _dataMapper = null; // 데이터 매퍼
|
||||
private ResponseMask _responseMask = new ResponseMask(); // response 데이터 성공 여부와 데이터를 구분하는 마스크
|
||||
private HttpResponseMask _responseMask = new HttpResponseMask(); // response 데이터 성공 여부와 데이터를 구분하는 마스크
|
||||
private int _maxRetryCount = 3;
|
||||
private int _retryDelay = 1000; // 밀리초
|
||||
private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부
|
||||
@@ -61,9 +63,14 @@ namespace UVC.Data
|
||||
public string? Body => _body;
|
||||
|
||||
/// <summary>
|
||||
/// 요청 완료 후 호출될 핸들러
|
||||
/// 요청 성공 시 호출될 핸들러
|
||||
/// </summary>
|
||||
public Action<IDataObject?>? Handler => _handler;
|
||||
public Action<IDataObject?>? SuccessHandler => _successhandler;
|
||||
|
||||
/// <summary>
|
||||
/// 요청 실패 시 호출될 핸들러
|
||||
/// </summary>
|
||||
public Action<string>? FailHandler => _failhandler;
|
||||
|
||||
/// <summary>
|
||||
/// 반복 실행 여부
|
||||
@@ -86,9 +93,10 @@ namespace UVC.Data
|
||||
public DataMapper? DataMapper => _dataMapper;
|
||||
|
||||
/// <summary>
|
||||
/// response에 적용되는 데이터 마스크
|
||||
/// HTTP 응답의 성공 여부를 확인하고, 성공 시 실제 데이터 페이로드를 추출하는 데 사용되는 <see cref="Data.HttpResponseMask"/>입니다.
|
||||
/// 이 객체에 정의된 규칙에 따라 원시 응답 문자열이 처리됩니다.
|
||||
/// </summary>
|
||||
public ResponseMask ResponseMask => _responseMask;
|
||||
public HttpResponseMask ResponseMask => _responseMask;
|
||||
|
||||
/// <summary>
|
||||
/// 최대 재시도 횟수
|
||||
@@ -133,11 +141,11 @@ namespace UVC.Data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 파이프라인에 적용할 response 마스크를 설정하고 업데이트된 파이프라인 구성을 반환합니다.
|
||||
/// HTTP 파이프라인에 적용할 ResponseMask를 설정하고 업데이트된 파이프라인 구성을 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="responseMask">HTTP response에 적용할 <see cref="DataMask"/>입니다.</param>
|
||||
/// <param name="responseMask">HTTP response에 적용할 <see cref="HttpResponseMask"/>입니다.</param>
|
||||
/// <returns>지정된 response 마스크가 적용된 업데이트된 <see cref="HttpPipeLineInfo"/> 인스턴스입니다.</returns>
|
||||
public HttpPipeLineInfo setResponseMask(ResponseMask responseMask)
|
||||
public HttpPipeLineInfo setResponseMask(HttpResponseMask responseMask)
|
||||
{
|
||||
_responseMask = responseMask;
|
||||
return this;
|
||||
@@ -146,13 +154,25 @@ namespace UVC.Data
|
||||
/// <summary>
|
||||
/// HTTP 요청이 완료된 후 호출될 핸들러를 설정합니다.
|
||||
/// 변경 된 데이터는 IDataObject로 전달됩니다.
|
||||
/// 변경 된 데이터가 없으면 호출 되지 않습니다.
|
||||
/// UpdatedDataOnly=true일 경우 변경 된 데이터가 없으면 호출 되지 않습니다.
|
||||
/// </summary>
|
||||
/// <param name="handler">응답 데이터를 처리할 콜백 함수</param>
|
||||
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
public HttpPipeLineInfo setHandler(Action<IDataObject?>? handler)
|
||||
public HttpPipeLineInfo setSuccessHandler(Action<IDataObject?>? handler)
|
||||
{
|
||||
_handler = handler;
|
||||
_successhandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청이 실패했을 때 호출될 핸들러를 설정합니다.
|
||||
/// 실패 정보 또는 관련 데이터가 string 형태로 전달될 수 있습니다.
|
||||
/// </summary>
|
||||
/// <param name="handler">실패 정보를 처리할 콜백 함수입니다. 실패 시 관련 데이터를 인자로 받습니다.</param>
|
||||
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
public HttpPipeLineInfo setFailHandler(Action<string>? handler)
|
||||
{
|
||||
_failhandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -204,41 +224,4 @@ namespace UVC.Data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ResponseMask
|
||||
{
|
||||
private string successKey = "message";
|
||||
private string successValue = "success";
|
||||
private string dataKey = "data";
|
||||
|
||||
public string SuccessKey { get; }
|
||||
public string SuccessValue { get; }
|
||||
public string DataKey { get; }
|
||||
|
||||
public ResponseMask() { }
|
||||
|
||||
public ResponseMask(string successKey, string successValue, string dataKey)
|
||||
{
|
||||
this.successKey = successKey;
|
||||
this.successValue = successValue;
|
||||
this.dataKey = dataKey;
|
||||
}
|
||||
|
||||
|
||||
public string? Apply(string response)
|
||||
{
|
||||
JObject responseObject = JObject.Parse(response);
|
||||
if(responseObject.TryGetValue(successKey, out JToken? successToken) &&
|
||||
responseObject.TryGetValue(dataKey, out JToken? dataToken) &&
|
||||
successToken?.ToString().ToLower() == successValue)
|
||||
{
|
||||
// 성공적인 응답 처리 로직
|
||||
return dataToken?.ToString(); // 실제로는 dataKey에 해당하는 데이터만 반환할 수 있습니다.
|
||||
}
|
||||
else
|
||||
{
|
||||
return null; // 실패한 응답 처리 로직
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
Assets/Scripts/UVC/Data/HttpResponseMask.cs
Normal file
88
Assets/Scripts/UVC/Data/HttpResponseMask.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP 응답 문자열을 파싱하여 성공 여부를 확인하고 실제 데이터를 추출하는 규칙을 정의하는 클래스입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 주로 JSON 형식의 응답에서 특정 키 값을 기준으로 작업의 성공 여부를 판단하고,
|
||||
/// 성공한 경우 데이터가 담긴 부분을 추출하는 데 사용됩니다.
|
||||
/// </remarks>
|
||||
public class HttpResponseMask
|
||||
{
|
||||
/// <summary>
|
||||
/// 응답에서 성공 여부를 나타내는 상태 메시지의 키 이름입니다. 기본값은 "message"입니다.
|
||||
/// </summary>
|
||||
private string successKey = "message";
|
||||
/// <summary>
|
||||
/// 응답에서 성공 상태를 나타내는 특정 값입니다. 이 값과 <see cref="successKey"/>의 값이 일치하면 성공으로 간주합니다. 기본값은 "success"입니다.
|
||||
/// </summary>
|
||||
private string successValue = "success";
|
||||
/// <summary>
|
||||
/// 응답에서 실제 데이터 페이로드를 담고 있는 키 이름입니다. 기본값은 "data"입니다.
|
||||
/// </summary>
|
||||
private string dataKey = "data";
|
||||
|
||||
/// <summary>
|
||||
/// 응답에서 성공 여부를 나타내는 상태 메시지의 키 이름을 가져옵니다.
|
||||
/// </summary>
|
||||
public string SuccessKey => successKey;
|
||||
/// <summary>
|
||||
/// 응답에서 성공 상태를 나타내는 특정 값을 가져옵니다.
|
||||
/// </summary>
|
||||
public string SuccessValue => successValue;
|
||||
/// <summary>
|
||||
/// 응답에서 실제 데이터 페이로드를 담고 있는 키 이름을 가져옵니다.
|
||||
/// </summary>
|
||||
public string DataKey => dataKey;
|
||||
|
||||
/// <summary>
|
||||
/// 기본값("message", "success", "data")으로 <see cref="HttpResponseMask"/> 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
public HttpResponseMask() { }
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 키 값으로 <see cref="HttpResponseMask"/> 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="successKey">성공 여부 판단에 사용될 키 이름입니다.</param>
|
||||
/// <param name="successValue">성공 상태를 나타내는 값입니다.</param>
|
||||
/// <param name="dataKey">실제 데이터가 포함된 키 이름입니다.</param>
|
||||
public HttpResponseMask(string successKey, string successValue, string dataKey)
|
||||
{
|
||||
this.successKey = successKey;
|
||||
this.successValue = successValue;
|
||||
this.dataKey = dataKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 제공된 HTTP 응답 문자열에 마스크 규칙을 적용하여 파싱 결과를 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="response">파싱할 HTTP 응답 문자열입니다. JSON 형식이어야 합니다.</param>
|
||||
/// <returns>
|
||||
/// 파싱 결과를 담고 있는 <see cref="HttpResponseResult"/> 객체입니다.
|
||||
/// <see cref="HttpResponseResult.IsSuccess"/>가 <c>true</c>이면, <see cref="HttpResponseResult.Data"/>에 추출된 데이터 문자열이 포함됩니다.
|
||||
/// <see cref="HttpResponseResult.IsSuccess"/>가 <c>false</c>이면, <see cref="HttpResponseResult.Message"/>에 원본 응답 문자열이 포함될 수 있습니다.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 먼저 응답 문자열을 <see cref="JObject"/>로 파싱합니다.
|
||||
/// 그 다음, <see cref="SuccessKey"/>에 해당하는 토큰 값과 <see cref="SuccessValue"/>를 비교하여 (대소문자 구분 없이) 성공 여부를 판단합니다.
|
||||
/// 성공한 경우, <see cref="DataKey"/>에 해당하는 토큰의 문자열 값을 <see cref="HttpResponseResult.Data"/>에 설정하고 <see cref="HttpResponseResult.IsSuccess"/>를 <c>true</c>로 설정합니다.
|
||||
/// 만약 <see cref="SuccessKey"/>에 해당하는 토큰 값과 <see cref="SuccessValue"/>를 비교하여 같지만 <see cref="DataKey"/>에 해당하는 토큰이 없거나 null인 경우에도 <see cref="HttpResponseResult.IsSuccess"/>를 <c>false</c>로 설정합니다.
|
||||
/// 실패한 경우, 원본 응답 문자열을 <see cref="HttpResponseResult.Message"/>에 설정하고 <see cref="HttpResponseResult.IsSuccess"/>를 <c>false</c>로 설정합니다.
|
||||
/// </remarks>
|
||||
public HttpResponseResult Apply(string response)
|
||||
{
|
||||
JObject responseObject = JObject.Parse(response);
|
||||
if (responseObject.TryGetValue(successKey, out JToken? successToken) &&
|
||||
responseObject.TryGetValue(dataKey, out JToken? dataToken) &&
|
||||
successToken?.ToString().ToLower() == successValue.ToLower()) // successValue 비교 시에도 ToLower() 추가하여 일관성 유지
|
||||
{
|
||||
// 성공적인 응답 처리 로직
|
||||
if (dataToken != null) return new HttpResponseResult(true, dataToken!.ToString(Formatting.None)); // 실제로는 dataKey에 해당하는 데이터만 반환할 수 있습니다.
|
||||
}
|
||||
return new HttpResponseResult(false, null, response); // 실패한 응답 처리 로직
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/HttpResponseMask.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/HttpResponseMask.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28af8b9febad2954a93c6bebda8d62ee
|
||||
36
Assets/Scripts/UVC/Data/HttpResponseResult.cs
Normal file
36
Assets/Scripts/UVC/Data/HttpResponseResult.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpResponseMask.Apply 메서드의 결과를 나타내는 클래스입니다.
|
||||
/// </summary>
|
||||
public class HttpResponseResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 응답이 성공했는지 여부를 나타냅니다.
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
/// <summary>
|
||||
/// 성공적으로 파싱된 경우, 추출된 데이터의 문자열 표현입니다.
|
||||
/// <see cref="IsSuccess"/>가 <c>false</c>인 경우 이 값은 <c>null</c> 입니다.
|
||||
/// </summary>
|
||||
public string? Data { get; set; }
|
||||
/// <summary>
|
||||
/// 파싱에 실패한 경우, 원본 응답 메시지 또는 오류 메시지를 포함합니다.
|
||||
/// <see cref="IsSuccess"/>가 <c>true</c>인 경우 이 값은 일반적으로 <c>null</c>입니다.
|
||||
/// </summary>
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="HttpResponseResult"/> 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="isSuccess">파싱 성공 여부입니다.</param>
|
||||
/// <param name="data">추출된 데이터입니다. 성공하지 못한 경우 <c>null</c>일 수 있습니다.</param>
|
||||
/// <param name="message">실패 시 메시지입니다. 성공한 경우 <c>null</c>일 수 있습니다.</param>
|
||||
public HttpResponseResult(bool isSuccess, string? data, string? message = null)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
Data = data;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/HttpResponseResult.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/HttpResponseResult.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52872327cdcef7540be0704fa181c778
|
||||
8
Assets/Scripts/UVC/Locale.meta
Normal file
8
Assets/Scripts/UVC/Locale.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9beacd432e8d2a940943f6d1452747ec
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Assets/Scripts/UVC/Locale/LocalizationDataSource.cs
Normal file
49
Assets/Scripts/UVC/Locale/LocalizationDataSource.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Locale
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 형식의 다국어 데이터를 담는 클래스입니다.
|
||||
/// 이 클래스는 로드된 JSON 파일의 전체 구조에 해당합니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// JSON 파일 예시 (`locale.json`):
|
||||
/// <code>
|
||||
/// {
|
||||
/// "en-US": {
|
||||
/// "welcome_message": "Welcome!",
|
||||
/// "greeting": "Hello, {0}!"
|
||||
/// },
|
||||
/// "ko-KR": {
|
||||
/// "welcome_message": "환영합니다!",
|
||||
/// "greeting": "안녕하세요, {0}님!"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class LocalizationDataSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 다국어 번역 데이터를 저장하는 딕셔너리입니다.
|
||||
/// 키는 언어 코드(예: "en-US", "ko-KR")이며,
|
||||
/// 값은 해당 언어의 번역 문자열 키와 번역된 텍스트를 담는 또 다른 딕셔너리입니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // "en-US" 언어의 "welcome_message" 키에 해당하는 값은 "Welcome!" 입니다.
|
||||
/// // Translations["en-US"]["welcome_message"] -> "Welcome!"
|
||||
/// </code>
|
||||
/// </example>
|
||||
public Dictionary<string, Dictionary<string, string>> Translations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="LocalizationDataSource"/> 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// <see cref="Translations"/> 딕셔너리를 빈 상태로 생성합니다.
|
||||
/// </summary>
|
||||
public LocalizationDataSource()
|
||||
{
|
||||
Translations = new Dictionary<string, Dictionary<string, string>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Locale/LocalizationDataSource.cs.meta
Normal file
2
Assets/Scripts/UVC/Locale/LocalizationDataSource.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2461a6116e68f54985c3dc4f5b08574
|
||||
301
Assets/Scripts/UVC/Locale/LocalizationManager.cs
Normal file
301
Assets/Scripts/UVC/Locale/LocalizationManager.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UVC.Json;
|
||||
using UVC.Log;
|
||||
|
||||
namespace UVC.Locale
|
||||
{
|
||||
/// <summary>
|
||||
/// 다국어 데이터를 관리하고, 현재 설정된 언어에 맞는 번역 문자열을 제공하는 클래스입니다.
|
||||
/// JSON 파일을 로드하여 다국어 데이터를 초기화하며, 언어 변경 시 이벤트를 통해 알림을 제공합니다.
|
||||
/// 이 클래스는 싱글톤으로 구현되어 어디서든 <see cref="Instance"/>를 통해 접근할 수 있습니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 1. LocalizationManager 인스턴스 접근 (싱글톤)
|
||||
/// // LocalizationManager locManager = LocalizationManager.Instance; // 직접 인스턴스화 대신 Instance 사용
|
||||
///
|
||||
/// // 2. (선택 사항) 언어 변경 이벤트 구독
|
||||
/// LocalizationManager.Instance.OnLanguageChanged += (newLanguage) => {
|
||||
/// ULog.Debug($"언어가 {newLanguage}(으)로 변경되었습니다.");
|
||||
/// // UI 업데이트 로직 등
|
||||
/// };
|
||||
///
|
||||
/// // 3. 다국어 데이터 로드 (StreamingAssets 폴더의 locale.json 파일을 기본으로 로드)
|
||||
/// // 인스턴스 생성 시 자동으로 로드하거나, 별도의 초기화 메서드를 통해 로드할 수 있습니다.
|
||||
/// // 예시: 로드 메서드가 private 생성자 또는 별도 초기화 메서드에서 호출된다고 가정
|
||||
/// if (LocalizationManager.Instance.LoadDefaultLocalizationData()) // LoadDefaultLocalizationData가 public이라고 가정
|
||||
/// {
|
||||
/// ULog.Debug("다국어 데이터 로드 성공!");
|
||||
///
|
||||
/// // 4. 사용 가능한 언어 목록 확인
|
||||
/// List<string> availableLangs = LocalizationManager.Instance.AvailableLanguages;
|
||||
/// ULog.Debug("사용 가능한 언어: " + string.Join(", ", availableLangs));
|
||||
///
|
||||
/// // 5. 언어 변경
|
||||
/// LocalizationManager.Instance.SetCurrentLanguage("ko-KR"); // 한국어로 변경
|
||||
///
|
||||
/// // 6. 번역된 문자열 가져오기
|
||||
/// string welcomeMsg = LocalizationManager.Instance.GetString("welcome_message");
|
||||
/// ULog.Debug(welcomeMsg); // 출력 (한국어): 환영합니다!
|
||||
///
|
||||
/// LocalizationManager.Instance.SetCurrentLanguage("en-US"); // 영어로 변경
|
||||
/// welcomeMsg = LocalizationManager.Instance.GetString("welcome_message");
|
||||
/// ULog.Debug(welcomeMsg); // 출력 (영어): Welcome!
|
||||
/// }
|
||||
/// else
|
||||
/// {
|
||||
/// ULog.Error("다국어 데이터 로드 실패.");
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class LocalizationManager
|
||||
{
|
||||
#region singleton
|
||||
|
||||
// 싱글톤 인스턴스를 저장하기 위한 private static 변수
|
||||
private static LocalizationManager _instance;
|
||||
// 멀티스레드 환경에서의 동시 접근을 제어하기 위한 lock 객체
|
||||
private static readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="LocalizationManager"/>의 싱글톤 인스턴스를 가져옵니다.
|
||||
/// 인스턴스가 아직 생성되지 않았다면 새로 생성하여 반환합니다.
|
||||
/// </summary>
|
||||
public static LocalizationManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
// Double-checked locking으로 스레드 안전성 확보
|
||||
if (_instance == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new LocalizationManager("en-US");
|
||||
_instance.LoadDefaultLocalizationData(); // 기본 언어 데이터 로드
|
||||
}
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private LocalizationDataSource _dataSource;
|
||||
private string _currentLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// 언어가 변경되었을 때 발생하는 이벤트의 델리게이트입니다.
|
||||
/// </summary>
|
||||
/// <param name="newLanguageCode">새롭게 설정된 언어 코드입니다 (예: "en-US", "ko-KR").</param>
|
||||
public delegate void LanguageChangedHandler(string newLanguageCode);
|
||||
/// <summary>
|
||||
/// 현재 언어가 <see cref="SetCurrentLanguage"/> 메서드를 통해 성공적으로 변경되었을 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
public event LanguageChangedHandler OnLanguageChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 설정된 언어 코드를 가져옵니다.
|
||||
/// </summary>
|
||||
public string CurrentLanguage => _currentLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// 로드된 다국어 데이터에서 사용 가능한 모든 언어 코드 목록을 가져옵니다.
|
||||
/// 데이터가 로드되지 않았거나 번역 데이터가 비어있으면 빈 리스트를 반환합니다.
|
||||
/// </summary>
|
||||
public List<string> AvailableLanguages
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_dataSource.Translations != null)
|
||||
{
|
||||
return new List<string>(_dataSource.Translations.Keys);
|
||||
}
|
||||
return new List<string>(); // 데이터가 없을 경우 빈 리스트 반환
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="LocalizationManager"/> 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// 기본 언어를 "ko-KR"로 설정합니다.
|
||||
/// 생성자를 private으로 변경하여 외부에서의 직접적인 인스턴스 생성을 막습니다.
|
||||
/// </summary>
|
||||
private LocalizationManager(string defaultLanguage = "ko-KR")
|
||||
{
|
||||
_dataSource = new LocalizationDataSource();
|
||||
// 기본 언어를 설정하거나, 시스템 설정을 따르도록 초기화할 수 있습니다.
|
||||
_currentLanguage = defaultLanguage; // 예시 기본 언어
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기본 경로(<see cref="Application.streamingAssetsPath"/>/locale.json)에서 다국어 데이터를 로드합니다.
|
||||
/// </summary>
|
||||
/// <returns>데이터 로드 성공 시 true, 실패 시 false를 반환합니다.</returns>
|
||||
public bool LoadDefaultLocalizationData()
|
||||
{
|
||||
string path = Path.Combine(Application.streamingAssetsPath, "locale.json");
|
||||
return LoadLocalizationData(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 파일 경로에서 JSON 형식의 다국어 데이터를 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="filePath">로드할 JSON 파일의 전체 경로입니다.</param>
|
||||
/// <returns>데이터 로드 및 분석 성공 시 true, 실패 시 false를 반환합니다.</returns>
|
||||
public bool LoadLocalizationData(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
ULog.Error($"Localization file not found: {filePath}", new System.Exception($"Localization file not found: {filePath}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string jsonData = File.ReadAllText(filePath);
|
||||
// 직접 Dictionary로 역직렬화한 후 Translations에 할당
|
||||
var translationsData = JsonHelper.FromJson<Dictionary<string, Dictionary<string, string>>>(jsonData);
|
||||
|
||||
if (translationsData == null)
|
||||
{
|
||||
ULog.Error($"Failed to deserialize localization data or no translations found in: {filePath}");
|
||||
_dataSource = new LocalizationDataSource(); // 빈 데이터 소스로 초기화
|
||||
return false;
|
||||
}
|
||||
|
||||
_dataSource.Translations = translationsData; // 읽어온 데이터를 Translations 프로퍼티에 할당
|
||||
|
||||
// 데이터 로드 후, 현재 설정된 언어가 유효한지 확인하고,
|
||||
// 유효하지 않다면 사용 가능한 첫 번째 언어나 기본 언어로 설정할 수 있습니다.
|
||||
if (!string.IsNullOrEmpty(_currentLanguage) && !_dataSource.Translations.ContainsKey(_currentLanguage))
|
||||
{
|
||||
ULog.Warning($"Current language '{_currentLanguage}' not found in the new data source. Attempting to set to default or first available.");
|
||||
if (AvailableLanguages.Count > 0)
|
||||
{
|
||||
SetCurrentLanguage(AvailableLanguages[0]); // 사용 가능한 첫 번째 언어로 설정
|
||||
}
|
||||
// else: 사용 가능한 언어가 없으면 변경하지 않음 (또는 특정 기본값으로 설정)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ULog.Error($"Error loading localization data from {filePath}: {ex.Message}", ex);
|
||||
_dataSource = new LocalizationDataSource(); // 오류 발생 시 빈 데이터 소스로 초기화
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 사용 언어를 변경합니다.
|
||||
/// 성공적으로 언어가 변경되면 <see cref="OnLanguageChanged"/> 이벤트가 발생합니다.
|
||||
/// </summary>
|
||||
/// <param name="languageCode">설정할 언어 코드입니다 (예: "en-US", "ko-KR").</param>
|
||||
public void SetCurrentLanguage(string languageCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(languageCode))
|
||||
{
|
||||
ULog.Warning("Language code cannot be null or empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dataSource?.Translations == null)
|
||||
{
|
||||
ULog.Warning("Translations data source is not loaded. Cannot set language.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dataSource.Translations.ContainsKey(languageCode))
|
||||
{
|
||||
if (_currentLanguage != languageCode)
|
||||
{
|
||||
_currentLanguage = languageCode;
|
||||
OnLanguageChanged?.Invoke(_currentLanguage); // 언어가 성공적으로 변경되면 이벤트 발생
|
||||
ULog.Debug($"Current language changed to: {languageCode}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Warning($"Language '{languageCode}' not found in data source. Current language remains '{_currentLanguage}'.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 설정된 언어를 기준으로, 주어진 키에 해당하는 번역된 문자열을 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="key">번역 문자열을 찾기 위한 키입니다.</param>
|
||||
/// <returns>
|
||||
/// 번역된 문자열을 반환합니다.
|
||||
/// 키가 null이거나 비어있으면 빈 문자열을 반환합니다.
|
||||
/// 해당 키 또는 현재 언어에 대한 번역을 찾을 수 없으면, "[키]" 형식의 문자열을 반환합니다.
|
||||
/// </returns>
|
||||
public string GetString(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
ULog.Warning("Requested localization key is null or empty.");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (_dataSource?.Translations == null)
|
||||
{
|
||||
ULog.Warning("Translations data source is not loaded. Cannot get string for key: " + key);
|
||||
return $"[{key}]";
|
||||
}
|
||||
|
||||
if (_dataSource.Translations.TryGetValue(_currentLanguage, out var languageSpecificStrings) &&
|
||||
languageSpecificStrings.TryGetValue(key, out var translatedString))
|
||||
{
|
||||
return translatedString;
|
||||
}
|
||||
|
||||
ULog.Debug($"Translation for key '{key}' not found in language '{_currentLanguage}'.");
|
||||
return $"[{key}]"; // 키를 그대로 반환하는 예시
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 언어의 특정 키에 대한 번역된 문자열을 직접 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="languageCode">문자열을 가져올 대상 언어 코드입니다.</param>
|
||||
/// <param name="key">번역 문자열을 찾기 위한 키입니다.</param>
|
||||
/// <returns>
|
||||
/// 번역된 문자열을 반환합니다.
|
||||
/// 언어 코드나 키가 null이거나 비어있으면 빈 문자열을 반환합니다.
|
||||
/// 해당 언어 또는 키에 대한 번역을 찾을 수 없으면 null을 반환합니다.
|
||||
/// </returns>
|
||||
public string? GetString(string languageCode, string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(languageCode))
|
||||
{
|
||||
ULog.Warning("Requested language code is null or empty for GetString(languageCode, key).");
|
||||
return null;
|
||||
}
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
ULog.Warning("Requested localization key is null or empty for GetString(languageCode, key).");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_dataSource?.Translations == null)
|
||||
{
|
||||
ULog.Warning("Translations data source is not loaded. Cannot get string for key: " + key + " in language: " + languageCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_dataSource.Translations.TryGetValue(languageCode, out var languageSpecificStrings) &&
|
||||
languageSpecificStrings.TryGetValue(key, out var translatedString))
|
||||
{
|
||||
return translatedString;
|
||||
}
|
||||
|
||||
ULog.Debug($"Translation for key '{key}' not found in language '{languageCode}'.");
|
||||
return null; // 해당 언어/키가 없음을 표시 (이전에는 "[languageCode:key]" 반환)
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Locale/LocalizationManager.cs.meta
Normal file
2
Assets/Scripts/UVC/Locale/LocalizationManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f617c21b60c55f42ace7c3a88f81a32
|
||||
@@ -1,4 +1,6 @@
|
||||
using log4net;
|
||||
#nullable enable
|
||||
|
||||
using log4net;
|
||||
using log4net.Appender;
|
||||
using log4net.Core;
|
||||
using log4net.Filter;
|
||||
@@ -26,6 +28,7 @@ namespace UVC.Log
|
||||
{
|
||||
log4net.Config.XmlConfigurator.Configure(new System.IO.FileInfo(Path.Combine(Application.dataPath, @"Resources\log4net.runtime.xml")));
|
||||
}
|
||||
// unity editor 일때
|
||||
else
|
||||
{
|
||||
log4net.Config.XmlConfigurator.Configure(new System.IO.FileInfo(Path.Combine(Application.dataPath, @"Resources\log4net.editor.xml")));
|
||||
@@ -78,13 +81,13 @@ namespace UVC.Log
|
||||
}
|
||||
|
||||
|
||||
public static void Warning(string msg, Exception ex)
|
||||
public static void Warning(string msg, Exception? ex = null)
|
||||
{
|
||||
if (logger.IsWarnEnabled)
|
||||
{
|
||||
if (useUnityDebug)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning(msg + ", " + ex);
|
||||
UnityEngine.Debug.LogWarning(msg + (ex != null ? ", " + ex: ""));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -99,13 +102,13 @@ namespace UVC.Log
|
||||
}
|
||||
}
|
||||
|
||||
public static void Error(string msg, Exception ex)
|
||||
public static void Error(string msg, Exception? ex = null)
|
||||
{
|
||||
if (logger.IsErrorEnabled)
|
||||
{
|
||||
if (useUnityDebug)
|
||||
{
|
||||
UnityEngine.Debug.LogError(msg + ", " + ex);
|
||||
UnityEngine.Debug.LogError(msg + (ex != null ? ", " + ex : ""));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -120,13 +123,13 @@ namespace UVC.Log
|
||||
}
|
||||
}
|
||||
|
||||
public static void Fatal(string msg, Exception ex)
|
||||
public static void Fatal(string msg, Exception? ex = null)
|
||||
{
|
||||
if (logger.IsFatalEnabled)
|
||||
{
|
||||
if (useUnityDebug)
|
||||
{
|
||||
UnityEngine.Debug.LogError(msg + ", " + ex);
|
||||
UnityEngine.Debug.LogError(msg + (ex != null ? ", " + ex : ""));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -495,12 +495,12 @@ namespace UVC.Network
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Debug($"Server error({resp.StatusCode} - {resp.Message})! " + new Exception(resp.Message));
|
||||
ULog.Warning($"Server error({resp.StatusCode} - {resp.Message})! ");
|
||||
OnError?.Invoke($"Server error({resp.StatusCode} - {resp.Message})!");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ULog.Debug(req.State.ToString() + ", " + new Exception(resp.Message));
|
||||
ULog.Error(req.State.ToString(), new Exception(resp.Message));
|
||||
OnError?.Invoke(req.State.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ namespace UVC.network
|
||||
/// </remarks>
|
||||
private void OnErrorMQTT(MQTTClient client, string reason)
|
||||
{
|
||||
ULog.Error($"MQTT OnError reason: '{reason}'", new Exception(reason));
|
||||
ULog.Error($"MQTT OnError reason: '{reason}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -359,7 +359,7 @@ namespace UVC.network
|
||||
{
|
||||
if (client == null || client.State != ClientStates.Connected)
|
||||
{
|
||||
ULog.Error("MQTT client is not connected. Cannot publish message.", new Exception("MQTT client is not connected. Cannot publish message."));
|
||||
ULog.Error("MQTT client is not connected. Cannot publish message.");
|
||||
return;
|
||||
}
|
||||
client.CreateApplicationMessageBuilder(topic)
|
||||
|
||||
@@ -4,10 +4,8 @@ using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UVC.Data;
|
||||
|
||||
@@ -46,22 +44,33 @@ namespace UVC.Tests.Data
|
||||
Setup();
|
||||
|
||||
Debug.Log("===== HttpPipeLine 테스트 시작 =====");
|
||||
RunTest(nameof(Add_NewInfo_AddedSuccessfully), Add_NewInfo_AddedSuccessfully);
|
||||
RunTest(nameof(Add_ExistingInfo_UpdatesExistingEntry), Add_ExistingInfo_UpdatesExistingEntry);
|
||||
await RunTestAsync(nameof(Remove_ExistingInfo_RemovedSuccessfullyAsync), Remove_ExistingInfo_RemovedSuccessfullyAsync);
|
||||
await RunTestAsync(nameof(Remove_NonExistingInfo_DoesNothing), Remove_NonExistingInfo_DoesNothing);
|
||||
RunTest(nameof(Excute_WithNonExistingKey_DoesNothing), Excute_WithNonExistingKey_DoesNothing);
|
||||
await RunTestAsync(nameof(Excute_WithJObjectResponse_ProcessesDataCorrectly), Excute_WithJObjectResponse_ProcessesDataCorrectly);
|
||||
await RunTestAsync(nameof(Excute_WithJArrayResponse_ProcessesDataCorrectly), Excute_WithJArrayResponse_ProcessesDataCorrectly);
|
||||
await RunTestAsync(nameof(Test_Excute_AgvDataParsing), Test_Excute_AgvDataParsing);
|
||||
await RunTestAsync(nameof(Test_Excute_AlarmDataParsing), Test_Excute_AlarmDataParsing);
|
||||
await RunTestAsync(nameof(Test_Excute_MultipleDataTypes), Test_Excute_MultipleDataTypes);
|
||||
await RunTestAsync(nameof(Test_Excute_CarrierDataParsing), Test_Excute_CarrierDataParsing);
|
||||
await RunTestAsync(nameof(Test_Excute_BaseInfoDataParsing), Test_Excute_BaseInfoDataParsing);
|
||||
await RunTestAsync(nameof(Test_Excute_WithRepeatExecution), Test_Excute_WithRepeatExecution);
|
||||
await RunTestAsync(nameof(Test_StopRepeat_StopsExecutionCorrectly), Test_StopRepeat_StopsExecutionCorrectly);
|
||||
await RunTestAsync(nameof(Test_MultipleRepeatingRequests_ManagedIndependently), Test_MultipleRepeatingRequests_ManagedIndependently);
|
||||
await RunTestAsync(nameof(Test_RepeatWithCount_StopsAutomatically), Test_RepeatWithCount_StopsAutomatically);
|
||||
//RunTest(nameof(Add_NewInfo_AddedSuccessfully), Add_NewInfo_AddedSuccessfully);
|
||||
//RunTest(nameof(Add_ExistingInfo_UpdatesExistingEntry), Add_ExistingInfo_UpdatesExistingEntry);
|
||||
//await RunTestAsync(nameof(Remove_ExistingInfo_RemovedSuccessfullyAsync), Remove_ExistingInfo_RemovedSuccessfullyAsync);
|
||||
//await RunTestAsync(nameof(Remove_NonExistingInfo_DoesNothing), Remove_NonExistingInfo_DoesNothing);
|
||||
//RunTest(nameof(Excute_WithNonExistingKey_DoesNothing), Excute_WithNonExistingKey_DoesNothing);
|
||||
//await RunTestAsync(nameof(Excute_WithJObjectResponse_ProcessesDataCorrectly), Excute_WithJObjectResponse_ProcessesDataCorrectly);
|
||||
//await RunTestAsync(nameof(Excute_WithJArrayResponse_ProcessesDataCorrectly), Excute_WithJArrayResponse_ProcessesDataCorrectly);
|
||||
//await RunTestAsync(nameof(Test_Excute_AgvDataParsing), Test_Excute_AgvDataParsing);
|
||||
//await RunTestAsync(nameof(Test_Excute_AlarmDataParsing), Test_Excute_AlarmDataParsing);
|
||||
//await RunTestAsync(nameof(Test_Excute_MultipleDataTypes), Test_Excute_MultipleDataTypes);
|
||||
//await RunTestAsync(nameof(Test_Excute_CarrierDataParsing), Test_Excute_CarrierDataParsing);
|
||||
//await RunTestAsync(nameof(Test_Excute_BaseInfoDataParsing), Test_Excute_BaseInfoDataParsing);
|
||||
//await RunTestAsync(nameof(Test_Excute_WithRepeatExecution), Test_Excute_WithRepeatExecution);
|
||||
//await RunTestAsync(nameof(Test_StopRepeat_StopsExecutionCorrectly), Test_StopRepeat_StopsExecutionCorrectly);
|
||||
//await RunTestAsync(nameof(Test_MultipleRepeatingRequests_ManagedIndependently), Test_MultipleRepeatingRequests_ManagedIndependently);
|
||||
//await RunTestAsync(nameof(Test_RepeatWithCount_StopsAutomatically), Test_RepeatWithCount_StopsAutomatically);
|
||||
|
||||
// HttpResponseMask 테스트 추가
|
||||
Debug.Log("===== HttpResponseMask 테스트 시작 =====");
|
||||
RunTest(nameof(HttpResponseMask_Apply_SuccessfulResponse_ReturnsSuccessWithData), HttpResponseMask_Apply_SuccessfulResponse_ReturnsSuccessWithData);
|
||||
RunTest(nameof(HttpResponseMask_Apply_FailedResponse_WrongSuccessValue_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_WrongSuccessValue_ReturnsFailWithMessage);
|
||||
RunTest(nameof(HttpResponseMask_Apply_FailedResponse_MissingSuccessKey_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_MissingSuccessKey_ReturnsFailWithMessage);
|
||||
RunTest(nameof(HttpResponseMask_Apply_FailedResponse_MissingDataKey_ReturnsFailWithMessage), HttpResponseMask_Apply_FailedResponse_MissingDataKey_ReturnsFailWithMessage);
|
||||
RunTest(nameof(HttpResponseMask_Apply_SuccessfulResponse_CustomKeys_ReturnsSuccessWithData), HttpResponseMask_Apply_SuccessfulResponse_CustomKeys_ReturnsSuccessWithData);
|
||||
RunTest(nameof(HttpResponseMask_Apply_InvalidJson_ThrowsException), HttpResponseMask_Apply_InvalidJson_ThrowsException);
|
||||
Debug.Log("===== HttpResponseMask 테스트 완료 =====");
|
||||
|
||||
Debug.Log("===== HttpPipeLine 테스트 완료 =====");
|
||||
}
|
||||
|
||||
@@ -234,7 +243,7 @@ namespace UVC.Tests.Data
|
||||
// HttpPipeLineInfo 설정
|
||||
var info = new HttpPipeLineInfo("http://test.com")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) =>
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
handlerCalled = true;
|
||||
if (data is DataObject dataObject)
|
||||
@@ -291,7 +300,7 @@ namespace UVC.Tests.Data
|
||||
// HttpPipeLineInfo 설정
|
||||
var info = new HttpPipeLineInfo("http://test.com")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) =>
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
handlerCalled = true;
|
||||
receivedData = data;
|
||||
@@ -337,7 +346,7 @@ namespace UVC.Tests.Data
|
||||
// Arrange - 의도적으로 아무 것도 설정하지 않음
|
||||
|
||||
// Act & Assert - 예외가 발생하지 않아야 함
|
||||
Assert.DoesNotThrow(() => pipeLine.Excute("nonExistingKey"));
|
||||
Assert.DoesNotThrow(() => pipeLine.Excute("nonExistingKey"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -368,7 +377,7 @@ namespace UVC.Tests.Data
|
||||
// HttpPipeLineInfo 설정
|
||||
var info = new HttpPipeLineInfo(agvUrl, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) =>
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
handlerCalled = true;
|
||||
receivedData = data;
|
||||
@@ -422,7 +431,7 @@ namespace UVC.Tests.Data
|
||||
// HttpPipeLineInfo 설정
|
||||
var info = new HttpPipeLineInfo(alarmUrl, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) =>
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
handlerCalled = true;
|
||||
receivedData = data;
|
||||
@@ -505,7 +514,7 @@ namespace UVC.Tests.Data
|
||||
string key = item.Key;
|
||||
var info = new HttpPipeLineInfo(item.Value, "get")
|
||||
.setDataMapper(new DataMapper(dataMasks[key]))
|
||||
.setHandler((data) =>
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
handlerCallCount++;
|
||||
results[key] = data;
|
||||
@@ -544,7 +553,7 @@ namespace UVC.Tests.Data
|
||||
|
||||
// DataMask와 DataMapper 설정
|
||||
var dataMask = new DataMask
|
||||
{
|
||||
{
|
||||
["MAIN_CARR_ID"] = "캐리어ID",
|
||||
["SUB_CARR_ID"] = "서브ID",
|
||||
["CARR_SEQ"] = "순번",
|
||||
@@ -556,7 +565,7 @@ namespace UVC.Tests.Data
|
||||
// HttpPipeLineInfo 설정
|
||||
var info = new HttpPipeLineInfo(testUrl, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) =>
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
handlerCalled = true;
|
||||
receivedData = data;
|
||||
@@ -634,7 +643,7 @@ namespace UVC.Tests.Data
|
||||
// HttpPipeLineInfo 설정
|
||||
var info = new HttpPipeLineInfo(baseInfoUrl, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) =>
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
handlerCalled = true;
|
||||
receivedData = data;
|
||||
@@ -720,7 +729,7 @@ namespace UVC.Tests.Data
|
||||
// 반복 실행 설정을 포함한 HttpPipeLineInfo 생성
|
||||
var info = new HttpPipeLineInfo(testUrl, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler(async (data) =>
|
||||
.setSuccessHandler(async (data) =>
|
||||
{
|
||||
handlerCallCount++;
|
||||
if (data is DataObject dataObject)
|
||||
@@ -794,7 +803,7 @@ namespace UVC.Tests.Data
|
||||
// 무한 반복 설정을 포함한 HttpPipeLineInfo 생성
|
||||
var info = new HttpPipeLineInfo(testUrl, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) => { handlerCallCount++;})
|
||||
.setSuccessHandler((data) => { handlerCallCount++; })
|
||||
.setRepeat(true, 0, repeatInterval, false); // 무한 반복 (repeatCount = 0)
|
||||
|
||||
pipeLine.UseMockup = true;
|
||||
@@ -862,12 +871,12 @@ namespace UVC.Tests.Data
|
||||
// 두 개의 반복 요청 설정
|
||||
var info1 = new HttpPipeLineInfo(testUrl1, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) => { handlerCallCount1++; })
|
||||
.setSuccessHandler((data) => { handlerCallCount1++; })
|
||||
.setRepeat(true, 0, repeatInterval1, false);
|
||||
|
||||
var info2 = new HttpPipeLineInfo(testUrl2, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) => { handlerCallCount2++; })
|
||||
.setSuccessHandler((data) => { handlerCallCount2++; })
|
||||
.setRepeat(true, 0, repeatInterval2, false);
|
||||
|
||||
pipeLine.UseMockup = true;
|
||||
@@ -944,7 +953,7 @@ namespace UVC.Tests.Data
|
||||
// 반복 횟수가 지정된 HttpPipeLineInfo 생성
|
||||
var info = new HttpPipeLineInfo(testUrl, "get")
|
||||
.setDataMapper(dataMapper)
|
||||
.setHandler((data) =>
|
||||
.setSuccessHandler((data) =>
|
||||
{
|
||||
handlerCallCount++;
|
||||
receivedData.Add(data);
|
||||
@@ -996,5 +1005,127 @@ namespace UVC.Tests.Data
|
||||
|
||||
return (Dictionary<string, CancellationTokenSource>)fieldInfo.GetValue(pipeLine);
|
||||
}
|
||||
|
||||
#region HttpResponseMask Tests
|
||||
|
||||
/// <summary>
|
||||
/// HttpResponseMask.Apply가 성공적인 응답을 올바르게 처리하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HttpResponseMask_Apply_SuccessfulResponse_ReturnsSuccessWithData()
|
||||
{
|
||||
// Arrange
|
||||
var responseMask = new HttpResponseMask(); // 기본값 사용 (successKey="message", successValue="success", dataKey="data")
|
||||
var jsonResponse = @"{""message"": ""success"", ""data"": {""key"":""value""}}";
|
||||
var expectedData = @"{""key"":""value""}";
|
||||
|
||||
// Act
|
||||
var result = responseMask.Apply(jsonResponse);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.IsSuccess, "결과가 성공이어야 합니다.");
|
||||
Assert.AreEqual(expectedData, result.Data, "추출된 데이터가 예상과 다릅니다.");
|
||||
Assert.IsNull(result.Message, "성공 시 메시지는 null이어야 합니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HttpResponseMask.Apply가 잘못된 성공 값으로 실패 응답을 올바르게 처리하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HttpResponseMask_Apply_FailedResponse_WrongSuccessValue_ReturnsFailWithMessage()
|
||||
{
|
||||
// Arrange
|
||||
var responseMask = new HttpResponseMask();
|
||||
var jsonResponse = @"{""message"": ""failed"", ""data"": {""key"": ""value""}}"; // successValue가 "success"가 아님
|
||||
|
||||
// Act
|
||||
var result = responseMask.Apply(jsonResponse);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result.IsSuccess, "결과가 실패여야 합니다.");
|
||||
Assert.IsNull(result.Data, "실패 시 데이터는 null이어야 합니다.");
|
||||
Assert.AreEqual(jsonResponse, result.Message, "실패 시 메시지는 원본 응답이어야 합니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HttpResponseMask.Apply가 성공 키가 없는 실패 응답을 올바르게 처리하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HttpResponseMask_Apply_FailedResponse_MissingSuccessKey_ReturnsFailWithMessage()
|
||||
{
|
||||
// Arrange
|
||||
var responseMask = new HttpResponseMask();
|
||||
var jsonResponse = @"{""error"": ""some error"", ""data"": {""key"": ""value""}}"; // "message" 키가 없음
|
||||
|
||||
// Act
|
||||
var result = responseMask.Apply(jsonResponse);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result.IsSuccess, "결과가 실패여야 합니다.");
|
||||
Assert.IsNull(result.Data, "실패 시 데이터는 null이어야 합니다.");
|
||||
Assert.AreEqual(jsonResponse, result.Message, "실패 시 메시지는 원본 응답이어야 합니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HttpResponseMask.Apply가 데이터 키가 없는 실패 응답을 올바르게 처리하는지 테스트합니다.
|
||||
/// (현재 구현상 successKey 조건만 만족하면 dataKey가 없어도 성공으로 간주하고 Data를 null로 반환할 수 있으므로, 이 테스트는 실패할 수 있습니다.
|
||||
/// 요구사항에 따라 이 부분의 동작을 명확히 하고 테스트를 조정해야 합니다.)
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HttpResponseMask_Apply_FailedResponse_MissingDataKey_ReturnsFailWithMessage()
|
||||
{
|
||||
// Arrange
|
||||
var responseMask = new HttpResponseMask();
|
||||
// 성공 키는 있지만 데이터 키가 없는 경우
|
||||
var jsonResponse = @"{""message"": ""success"", ""payload"": {""key"": ""value""}}"; // "data" 키가 없음
|
||||
|
||||
// Act
|
||||
var result = responseMask.Apply(jsonResponse);
|
||||
|
||||
// Assert
|
||||
// 현재 Apply 메서드 구현에 따르면, successKey/successValue가 일치하고 dataKey가 없으면
|
||||
// IsSuccess = false, Data = null 로 반환됩니다.
|
||||
// 만약 dataKey가 필수라면 Apply 메서드 수정 또는 이 테스트의 기대 결과 수정이 필요합니다.
|
||||
// 여기서는 현재 구현을 기준으로 테스트합니다.
|
||||
Assert.IsFalse(result.IsSuccess, "data 키가 없으면 success 조건 만족 시 실패여야 합니다 (현재 로직 기준).");
|
||||
Assert.IsNull(result.Data, "data 키가 없는 경우 Data는 null이어야 합니다.");
|
||||
Assert.IsNotNull(result.Message, "실패 시 메시지는 null이 아니어야 합니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HttpResponseMask.Apply가 사용자 정의 키를 사용하여 성공적인 응답을 올바르게 처리하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HttpResponseMask_Apply_SuccessfulResponse_CustomKeys_ReturnsSuccessWithData()
|
||||
{
|
||||
// Arrange
|
||||
var responseMask = new HttpResponseMask("status", "ok", "payload");
|
||||
var jsonResponse = @"{""status"": ""ok"", ""payload"": {""info"":""custom data""}}";
|
||||
var expectedData = @"{""info"":""custom data""}";
|
||||
|
||||
// Act
|
||||
var result = responseMask.Apply(jsonResponse);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.IsSuccess, "결과가 성공이어야 합니다.");
|
||||
Assert.AreEqual(expectedData, result.Data, "추출된 데이터가 예상과 다릅니다.");
|
||||
Assert.IsNull(result.Message, "성공 시 메시지는 null이어야 합니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HttpResponseMask.Apply가 잘못된 JSON 형식의 응답을 처리할 때 예외를 발생하는지 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HttpResponseMask_Apply_InvalidJson_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var responseMask = new HttpResponseMask();
|
||||
var invalidJsonResponse = @"{""message"": ""success"", ""data"": {""key"": ""value"""; // 닫는 중괄호 누락
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<Newtonsoft.Json.JsonReaderException>(() => responseMask.Apply(invalidJsonResponse), "잘못된 JSON 형식에 대해 JsonReaderException이 발생해야 합니다.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
8
Assets/Scripts/UVC/UI.meta
Normal file
8
Assets/Scripts/UVC/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95083d5099fc38b46bfbcec607ff6b31
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/UVC/UI/Commands.meta
Normal file
8
Assets/Scripts/UVC/UI/Commands.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8a3b1d9e4b79ba44b49f0703f4ff753
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
Assets/Scripts/UVC/UI/Commands/ICommand.cs
Normal file
7
Assets/Scripts/UVC/UI/Commands/ICommand.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace UVC.UI.Commands
|
||||
{
|
||||
public interface ICommand
|
||||
{
|
||||
void Execute();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Commands/ICommand.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Commands/ICommand.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f0eef464dda89d4b9f20e533843b83f
|
||||
62
Assets/Scripts/UVC/UI/Commands/MenuCommands.cs
Normal file
62
Assets/Scripts/UVC/UI/Commands/MenuCommands.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UVC.Locale;
|
||||
using UVC.Log;
|
||||
|
||||
namespace UVC.UI.Commands
|
||||
{
|
||||
// 간단한 디버그 로그 출력 커맨드
|
||||
public class DebugLogCommand : ICommand
|
||||
{
|
||||
private readonly string _message;
|
||||
|
||||
public DebugLogCommand(string message)
|
||||
{
|
||||
_message = message;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
ULog.Debug(_message);
|
||||
}
|
||||
}
|
||||
|
||||
// 애플리케이션 종료 커맨드
|
||||
public class QuitApplicationCommand : ICommand
|
||||
{
|
||||
public void Execute()
|
||||
{
|
||||
ULog.Debug("애플리케이션을 종료합니다.");
|
||||
Application.Quit();
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.isPlaying = false; // 에디터에서 실행 중일 경우 플레이 모드 종료
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// 언어 변경 커맨드
|
||||
public class ChangeLanguageCommand : ICommand
|
||||
{
|
||||
private readonly string _languageCode;
|
||||
private readonly LocalizationManager _localizationManager;
|
||||
|
||||
public ChangeLanguageCommand(string languageCode, LocalizationManager localizationManager)
|
||||
{
|
||||
_languageCode = languageCode;
|
||||
_localizationManager = localizationManager;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
if (_localizationManager != null)
|
||||
{
|
||||
_localizationManager.SetCurrentLanguage(_languageCode);
|
||||
ULog.Debug($"언어가 {_languageCode}(으)로 변경되었습니다. (Command)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Error("LocalizationManager가 ChangeLanguageCommand에 전달되지 않았습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Commands/MenuCommands.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Commands/MenuCommands.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c05439dd4537f6245a5cb70f56d372c7
|
||||
8
Assets/Scripts/UVC/UI/Menu.meta
Normal file
8
Assets/Scripts/UVC/UI/Menu.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f04f8d2773c12a41a7cbe1f676a20d2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
131
Assets/Scripts/UVC/UI/Menu/TopMenuController.cs
Normal file
131
Assets/Scripts/UVC/UI/Menu/TopMenuController.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using UnityEngine;
|
||||
using UVC.Locale;
|
||||
using System.Collections.Generic;
|
||||
using UVC.UI.Commands;
|
||||
using UVC.Log;
|
||||
using System;
|
||||
|
||||
namespace UVC.UI.Menu
|
||||
{
|
||||
public class TopMenuController : MonoBehaviour
|
||||
{
|
||||
protected TopMenuView view;
|
||||
protected TopMenuModel model;
|
||||
protected LocalizationManager _locManager;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// 1. 동일한 GameObject에서 TopMenuView 컴포넌트 검색
|
||||
view = GetComponent<TopMenuView>();
|
||||
|
||||
// 2. 동일한 GameObject에 없다면, 자식 GameObject에서 검색
|
||||
if (view == null)
|
||||
{
|
||||
view = GetComponentInChildren<TopMenuView>();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Start()
|
||||
{
|
||||
model = new TopMenuModel();
|
||||
_locManager = LocalizationManager.Instance;
|
||||
|
||||
if (view == null)
|
||||
{
|
||||
ULog.Error("TopMenuView가 Inspector에서 할당되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
if (_locManager == null)
|
||||
{
|
||||
ULog.Error("LocalizationManager 인스턴스를 찾을 수 없습니다. 메뉴 텍스트가 올바르게 표시되지 않을 수 있습니다.");
|
||||
// _locManager가 null이어도 메뉴 구조는 생성될 수 있도록 진행합니다.
|
||||
// TopMenuView에서 _locManager null 체크를 통해 텍스트 처리를 합니다.
|
||||
}
|
||||
|
||||
InitializeMenuItems();
|
||||
|
||||
view.ClearMenuItems();
|
||||
// menuContainer가 TopMenuView 내부에 public으로 노출되어 있다고 가정합니다.
|
||||
// 만약 private이라면, TopMenuView에 menuContainer를 전달받는 public 메서드가 필요할 수 있습니다.
|
||||
view.CreateMenuItems(model.MenuItems, view.MenuContainer);
|
||||
|
||||
view.OnMenuItemClicked += HandleMenuItemClicked;
|
||||
if (_locManager != null)
|
||||
{
|
||||
_locManager.OnLanguageChanged += HandleLanguageChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
if (view != null)
|
||||
{
|
||||
view.OnMenuItemClicked -= HandleMenuItemClicked;
|
||||
}
|
||||
if (_locManager != null)
|
||||
{
|
||||
_locManager.OnLanguageChanged -= HandleLanguageChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void InitializeMenuItems()
|
||||
{
|
||||
model.MenuItems.Clear();
|
||||
|
||||
model.MenuItems.Add(new MenuItemData("file", "menu_file", subMenuItems: new List<MenuItemData>
|
||||
{
|
||||
new MenuItemData("file_new", "menu_file_new", subMenuItems: new List<MenuItemData>
|
||||
{
|
||||
new MenuItemData("file_new_project", "menu_file_new_project", new DebugLogCommand("새 프로젝트 선택됨 (Command)")),
|
||||
new MenuItemData("file_new_file", "menu_file_new_file", new DebugLogCommand("새 파일 선택됨 (Command)"))
|
||||
}),
|
||||
new MenuItemData("file_open", "menu_file_open", new DebugLogCommand("파일 열기 선택됨 (Command)")),
|
||||
MenuItemData.CreateSeparator("file_sep1"), // 구분선 추가
|
||||
new MenuItemData("file_save", "menu_file_save", command: new DebugLogCommand("저장 선택됨 (Command)") , subMenuItems: new List<MenuItemData> // 저장 메뉴에 Command 추가
|
||||
{
|
||||
new MenuItemData("file_save_as", "menu_file_save_as", new DebugLogCommand("다른 이름으로 저장 선택됨 (Command)"))
|
||||
}),
|
||||
MenuItemData.CreateSeparator("file_sep2"), // 또 다른 구분선 추가
|
||||
new MenuItemData("file_exit", "menu_file_exit", new QuitApplicationCommand())
|
||||
}));
|
||||
|
||||
model.MenuItems.Add(new MenuItemData("edit", "menu_edit", subMenuItems: new List<MenuItemData>
|
||||
{
|
||||
new MenuItemData("edit_undo", "menu_edit_undo", new DebugLogCommand("실행 취소 선택됨 (Command)")),
|
||||
new MenuItemData("edit_redo", "menu_edit_redo", new DebugLogCommand("다시 실행 선택됨 (Command)")),
|
||||
MenuItemData.CreateSeparator("edit_sep1"),
|
||||
new MenuItemData("preferences", "menu_preferences", new DebugLogCommand("환경설정 선택됨 (Command)"))
|
||||
}));
|
||||
|
||||
if (_locManager != null)
|
||||
{
|
||||
model.MenuItems.Add(new MenuItemData("language", "menu_language", subMenuItems: new List<MenuItemData>
|
||||
{
|
||||
new MenuItemData("lang_ko", "menu_lang_korean", new ChangeLanguageCommand("ko-KR", _locManager)),
|
||||
new MenuItemData("lang_en", "menu_lang_english", new ChangeLanguageCommand("en-US", _locManager))
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Warning("LocalizationManager가 null이므로 언어 변경 메뉴를 초기화할 수 없습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void HandleMenuItemClicked(MenuItemData clickedItemData)
|
||||
{
|
||||
if (clickedItemData.IsSeparator) return; // 구분선은 클릭 액션이 없음
|
||||
|
||||
ULog.Debug($"메뉴 아이템 클릭됨: {clickedItemData.ItemId} (Key: {clickedItemData.DisplayNameKey})");
|
||||
clickedItemData.Command?.Execute();
|
||||
}
|
||||
|
||||
protected virtual void HandleLanguageChanged(string newLanguageCode)
|
||||
{
|
||||
ULog.Debug($"언어 변경 감지: {newLanguageCode}. 메뉴 텍스트 업데이트 중...");
|
||||
if (view != null && model != null)
|
||||
{
|
||||
view.UpdateAllMenuTexts(model.MenuItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Menu/TopMenuController.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Menu/TopMenuController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af02edfdfb8a7b847aed5397dd6dc4c9
|
||||
54
Assets/Scripts/UVC/UI/Menu/TopMenuModel.cs
Normal file
54
Assets/Scripts/UVC/UI/Menu/TopMenuModel.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UVC.UI.Commands;
|
||||
|
||||
namespace UVC.UI.Menu
|
||||
{
|
||||
// 메뉴 아이템 하나를 나타내는 데이터 구조체 또는 클래스
|
||||
public class MenuItemData
|
||||
{
|
||||
public string ItemId { get; private set; }
|
||||
public string DisplayNameKey { get; private set; } // 다국어 처리를 위한 키
|
||||
public ICommand Command { get; private set; } // Action 대신 ICommand 사용
|
||||
public List<MenuItemData> SubMenuItems { get; private set; } // 하위 메뉴 아이템 목록
|
||||
public bool IsSeparator { get; private set; } // 구분선 여부 플래그
|
||||
|
||||
public MenuItemData(string itemId, string displayNameKey, ICommand command = null, List<MenuItemData> subMenuItems = null, bool isSeparator = false)
|
||||
{
|
||||
ItemId = itemId;
|
||||
DisplayNameKey = displayNameKey;
|
||||
Command = command;
|
||||
SubMenuItems = subMenuItems ?? new List<MenuItemData>();
|
||||
IsSeparator = isSeparator;
|
||||
}
|
||||
|
||||
public void AddSubMenuItem(MenuItemData subItem)
|
||||
{
|
||||
if (IsSeparator) return; // 구분선에는 하위 메뉴를 추가할 수 없음
|
||||
SubMenuItems.Add(subItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 구분선 역할을 하는 MenuItemData 객체를 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="itemId">구분선의 고유 ID. null일 경우 GUID로 자동 생성됩니다.</param>
|
||||
/// <returns>구분선 MenuItemData 객체입니다.</returns>
|
||||
public static MenuItemData CreateSeparator(string itemId = null)
|
||||
{
|
||||
return new MenuItemData(itemId ?? $"separator_{Guid.NewGuid()}", string.Empty, null, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
public class TopMenuModel
|
||||
{
|
||||
public List<MenuItemData> MenuItems { get; private set; }
|
||||
|
||||
public TopMenuModel()
|
||||
{
|
||||
MenuItems = new List<MenuItemData>();
|
||||
}
|
||||
|
||||
// LoadMenuItems 메서드는 Controller에서 직접 데이터를 채움
|
||||
//public void LoadMenuItems(string jsonString) { }
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Menu/TopMenuModel.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Menu/TopMenuModel.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44674fdbeb5fa51409a23e6544757068
|
||||
603
Assets/Scripts/UVC/UI/Menu/TopMenuView.cs
Normal file
603
Assets/Scripts/UVC/UI/Menu/TopMenuView.cs
Normal file
@@ -0,0 +1,603 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Locale;
|
||||
using UVC.Log; // 다국어 처리 네임스페이스 사용
|
||||
|
||||
namespace UVC.UI.Menu
|
||||
{
|
||||
public class TopMenuView : MonoBehaviour
|
||||
{
|
||||
|
||||
// 프리팹의 Resources 폴더 내 경로 (예시 경로이므로 실제 프로젝트에 맞게 수정 필요)
|
||||
private const string MenuItemPrefabPath = "Prefabs/UI/Menu/MenuItem";
|
||||
private const string SubMenuItemPrefabPath = "Prefabs/UI/Menu/SubMenuItem";
|
||||
private const string MenuSeparatorPrefabPath = "Prefabs/UI/Menu/MenuSeparator";
|
||||
private const string MenuContainerName = "TopMenu";
|
||||
private const string SubMenuContainerName = "SubMenuContainer";
|
||||
private const string SubMenuArrowName = "SubMenuArrow";
|
||||
|
||||
protected GameObject menuItemPrefab;
|
||||
protected GameObject subMenuItemPrefab;
|
||||
protected GameObject menuSeparatorPrefab;
|
||||
protected Transform menuContainer;
|
||||
|
||||
protected int menuItemInitX = 0; // 1depth 메뉴 아이템의 초기 X 위치
|
||||
protected int menuItemInitY = 10; // 1depth 메뉴 아이템의 초기 Y 위치
|
||||
protected int menuItemWidth = 100;
|
||||
protected int menuItemHeight = 30;
|
||||
protected int subMenuItemWidth = 100;
|
||||
protected int subMenuItemHeight = 30;
|
||||
protected int menuItemSeparatorWidth = 100;
|
||||
protected int menuItemSeparatorHeight = 1;
|
||||
protected int menuItemSeparatorVerticalMargin = 4;
|
||||
protected int menuItemHorizontalGap = 30;
|
||||
protected int menuItemVerticalGap = 2; // 사용자가 제공한 컨텍스트 값 사용
|
||||
protected int menuDepthHorizontalGap = 100 - 5;
|
||||
protected int menuDepthVerticalGap = 10;
|
||||
protected int subContainerHorizontalPadding = 4;
|
||||
protected int subContainerVerticalPadding = 10;
|
||||
|
||||
public Transform MenuContainer { get => menuContainer; }
|
||||
|
||||
public event Action<MenuItemData> OnMenuItemClicked;
|
||||
|
||||
protected Dictionary<string, GameObject> _menuItemObjects = new Dictionary<string, GameObject>();
|
||||
protected LocalizationManager _locManager;
|
||||
protected GameObject uiBlockerInstance;
|
||||
protected bool isAnySubMenuOpen = false;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
_locManager = LocalizationManager.Instance;
|
||||
if (_locManager == null)
|
||||
{
|
||||
ULog.Error("LocalizationManager 인스턴스를 찾을 수 없습니다.", new InvalidOperationException("LocalizationManager 인스턴스를 찾을 수 없습니다."));
|
||||
}
|
||||
|
||||
menuItemPrefab = Resources.Load<GameObject>(MenuItemPrefabPath);
|
||||
if (menuItemPrefab == null)
|
||||
{
|
||||
ULog.Error($"메뉴 아이템 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {MenuItemPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{MenuItemPrefabPath}"));
|
||||
}
|
||||
|
||||
subMenuItemPrefab = Resources.Load<GameObject>(SubMenuItemPrefabPath);
|
||||
if (subMenuItemPrefab == null)
|
||||
{
|
||||
ULog.Error($"서브 메뉴 아이템 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {SubMenuItemPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{SubMenuItemPrefabPath}"));
|
||||
}
|
||||
|
||||
menuSeparatorPrefab = Resources.Load<GameObject>(MenuSeparatorPrefabPath);
|
||||
if (menuSeparatorPrefab == null)
|
||||
{
|
||||
ULog.Error($"메뉴 구분선 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {MenuSeparatorPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{MenuSeparatorPrefabPath}"));
|
||||
}
|
||||
|
||||
Transform containerTransform = transform.Find(MenuContainerName);
|
||||
if (containerTransform != null)
|
||||
{
|
||||
menuContainer = containerTransform;
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Warning($"'{MenuContainerName}'이라는 이름의 자식 GameObject를 찾을 수 없어 현재 Transform을 menuContainer로 사용합니다.");
|
||||
menuContainer = transform;
|
||||
}
|
||||
if (menuContainer == null)
|
||||
{
|
||||
ULog.Error("menuContainer를 설정할 수 없습니다.", new InvalidOperationException("menuContainer could not be set."));
|
||||
}
|
||||
|
||||
// UI 블로커 생성 및 설정
|
||||
CreateUIBlocker();
|
||||
}
|
||||
|
||||
protected virtual void CreateUIBlocker()
|
||||
{
|
||||
uiBlockerInstance = new GameObject("TopMenuUIBlocker");
|
||||
// Canvas를 찾아 그 자식으로 설정합니다. Canvas가 여러 개일 경우, 적절한 Canvas를 찾는 로직이 필요할 수 있습니다.
|
||||
Canvas canvas = FindFirstObjectByType<Canvas>();
|
||||
Transform blockerParent = canvas != null ? canvas.transform : transform.parent;
|
||||
|
||||
|
||||
if (blockerParent == null)
|
||||
{
|
||||
blockerParent = transform;
|
||||
ULog.Warning("TopMenuUIBlocker의 부모를 TopMenuView 자신으로 설정합니다. Canvas 하위에 배치하는 것을 권장합니다.");
|
||||
uiBlockerInstance.transform.SetParent(blockerParent, false);
|
||||
uiBlockerInstance.transform.SetAsFirstSibling();
|
||||
}
|
||||
else
|
||||
{
|
||||
uiBlockerInstance.transform.SetParent(blockerParent, false);
|
||||
// TopMenuView GameObject (또는 menuContainer) 바로 앞에 오도록 sibling index 설정
|
||||
// menuContainer가 Canvas의 직계 자식이 아닐 수 있으므로, menuContainer의 sibling index를 기준으로 합니다.
|
||||
if (menuContainer != null && menuContainer.parent == blockerParent)
|
||||
{
|
||||
uiBlockerInstance.transform.SetSiblingIndex(menuContainer.GetSiblingIndex());
|
||||
}
|
||||
else
|
||||
{
|
||||
// menuContainer가 blockerParent의 직계 자식이 아니거나 다른 복잡한 구조일 경우,
|
||||
// 블로커를 가장 뒤(UI 요소들 중에서는 가장 앞)로 보내는 것이 안전할 수 있습니다.
|
||||
// 또는, TopMenuView 자체가 Canvas의 자식이라면 그 기준으로 설정합니다.
|
||||
if (transform.parent == blockerParent)
|
||||
{
|
||||
uiBlockerInstance.transform.SetSiblingIndex(transform.GetSiblingIndex());
|
||||
}
|
||||
else
|
||||
{
|
||||
// 최후의 수단으로 가장 뒤로 보냅니다. (UI상 가장 앞으로)
|
||||
// 하지만 이렇게 되면 다른 UI를 가릴 수 있으므로 주의해야 합니다.
|
||||
// uiBlockerInstance.transform.SetAsLastSibling();
|
||||
// 더 나은 방법은 Canvas 내에서 TopMenu 시스템 전체의 루트 바로 앞에 두는 것입니다.
|
||||
// 여기서는 TopMenuView의 sibling index를 기준으로 합니다.
|
||||
uiBlockerInstance.transform.SetSiblingIndex(transform.GetSiblingIndex());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RectTransform blockerRect = uiBlockerInstance.AddComponent<RectTransform>();
|
||||
blockerRect.anchorMin = Vector2.zero;
|
||||
blockerRect.anchorMax = Vector2.one;
|
||||
blockerRect.offsetMin = Vector2.zero;
|
||||
blockerRect.offsetMax = Vector2.zero;
|
||||
|
||||
Image blockerImage = uiBlockerInstance.AddComponent<Image>();
|
||||
blockerImage.color = new Color(0, 0, 0, 0.001f);
|
||||
blockerImage.raycastTarget = true;
|
||||
|
||||
Button blockerButton = uiBlockerInstance.AddComponent<Button>();
|
||||
blockerButton.transition = Selectable.Transition.None;
|
||||
blockerButton.onClick.AddListener(CloseAllOpenSubMenus);
|
||||
|
||||
uiBlockerInstance.SetActive(false);
|
||||
}
|
||||
|
||||
public virtual Vector2 CreateMenuItems(List<MenuItemData> items, Transform parentContainer, int depth = 0)
|
||||
{
|
||||
if (items == null || parentContainer == null)
|
||||
{
|
||||
ULog.Error("메뉴 아이템 생성에 필요한 'items' 또는 'parentContainer'가 누락되었습니다.", new ArgumentNullException(items == null ? "items" : "parentContainer"));
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
float currentLevelCalculatedWidth = 0;
|
||||
float currentLevelCalculatedHeight = 0;
|
||||
|
||||
if (items.Count == 0 && depth > 0)
|
||||
{
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
MenuItemData itemData = items[i];
|
||||
GameObject instanceToLayout = null;
|
||||
GameObject prefabToUse = null;
|
||||
|
||||
if (itemData.IsSeparator)
|
||||
{
|
||||
if (menuSeparatorPrefab == null)
|
||||
{
|
||||
ULog.Error("menuSeparatorPrefab이 할당되지 않았습니다. 구분선을 생성할 수 없습니다.", new InvalidOperationException("menuSeparatorPrefab is not loaded."));
|
||||
continue;
|
||||
}
|
||||
GameObject separatorInstance = Instantiate(menuSeparatorPrefab, parentContainer);
|
||||
separatorInstance.name = $"Separator_{itemData.ItemId}_Depth{depth}";
|
||||
_menuItemObjects[itemData.ItemId] = separatorInstance;
|
||||
instanceToLayout = separatorInstance;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (depth == 0) // 1차 깊이 메뉴
|
||||
{
|
||||
prefabToUse = menuItemPrefab;
|
||||
if (prefabToUse == null)
|
||||
{
|
||||
ULog.Error("menuItemPrefab이 할당되지 않았습니다. 1차 메뉴 아이템을 생성할 수 없습니다.", new InvalidOperationException("menuItemPrefab is not loaded."));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else // 2차 깊이 이상 하위 메뉴
|
||||
{
|
||||
prefabToUse = subMenuItemPrefab;
|
||||
if (prefabToUse == null)
|
||||
{
|
||||
ULog.Error("subMenuItemPrefab이 할당되지 않았습니다. 하위 메뉴 아이템을 생성할 수 없습니다.", new InvalidOperationException("subMenuItemPrefab is not loaded."));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
GameObject menuItemInstance = Instantiate(prefabToUse, parentContainer);
|
||||
menuItemInstance.name = $"MenuItem_{itemData.ItemId}_Depth{depth}";
|
||||
_menuItemObjects[itemData.ItemId] = menuItemInstance;
|
||||
instanceToLayout = menuItemInstance;
|
||||
|
||||
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
Button button = menuItemInstance.GetComponent<Button>();
|
||||
|
||||
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayNameKey))
|
||||
{
|
||||
if (_locManager != null)
|
||||
{
|
||||
buttonText.text = _locManager.GetString(itemData.DisplayNameKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Warning($"LocalizationManager 인스턴스가 없어 메뉴 아이템 텍스트를 키 값으로 설정합니다: {itemData.DisplayNameKey}");
|
||||
buttonText.text = $"[{itemData.DisplayNameKey}]";
|
||||
}
|
||||
}
|
||||
|
||||
if (button != null)
|
||||
{
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() =>
|
||||
{
|
||||
OnMenuItemClicked?.Invoke(itemData);
|
||||
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
||||
{
|
||||
ToggleSubMenuDisplay(itemData, menuItemInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 하위 메뉴가 없는 아이템 클릭 시 모든 메뉴 닫기 (선택적)
|
||||
CloseAllOpenSubMenus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
||||
{
|
||||
Transform subMenuArrowTransform = menuItemInstance.transform.Find(SubMenuArrowName);
|
||||
if (subMenuArrowTransform != null)
|
||||
{
|
||||
subMenuArrowTransform.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
Transform subMenuContainerTransform = menuItemInstance.transform.Find(SubMenuContainerName);
|
||||
if (subMenuContainerTransform != null)
|
||||
{
|
||||
RectTransform subMenuRect = subMenuContainerTransform.GetComponent<RectTransform>();
|
||||
if (subMenuRect == null)
|
||||
{
|
||||
ULog.Warning($"{SubMenuContainerName} for '{menuItemInstance.name}' is missing RectTransform. Adding one.");
|
||||
subMenuRect = subMenuContainerTransform.gameObject.AddComponent<RectTransform>();
|
||||
}
|
||||
|
||||
subMenuRect.anchorMin = new Vector2(0, 1);
|
||||
subMenuRect.anchorMax = new Vector2(0, 1);
|
||||
subMenuRect.pivot = new Vector2(0, 1);
|
||||
|
||||
// 하위 메뉴 위치 조정: 1차 메뉴는 오른쪽, 2차 이상은 아래쪽으로 펼쳐지도록 수정
|
||||
if (depth == 0)
|
||||
{
|
||||
subMenuRect.anchoredPosition = new Vector2(menuDepthHorizontalGap, -menuDepthVerticalGap);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 2차 이상 메뉴의 하위 메뉴는 부모 아이템의 오른쪽에 표시되도록 anchoredPosition 조정
|
||||
// 부모 아이템의 너비만큼 이동 (subMenuItemWidth 사용)
|
||||
subMenuRect.anchoredPosition = new Vector2(subMenuItemWidth, 0);
|
||||
}
|
||||
|
||||
|
||||
Vector2 subContentSize = CreateMenuItems(itemData.SubMenuItems, subMenuContainerTransform, depth + 1);
|
||||
float containerWidth = subContentSize.x + (2 * subContainerHorizontalPadding);
|
||||
float containerHeight = subContentSize.y + (2 * subContainerVerticalPadding);
|
||||
subMenuRect.sizeDelta = new Vector2(containerWidth, containerHeight);
|
||||
subMenuContainerTransform.gameObject.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Warning($"'{menuItemInstance.name}' 내부에 '{SubMenuContainerName}'를 찾을 수 없습니다. 하위 메뉴가 표시되지 않습니다.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Transform existingArrow = menuItemInstance.transform.Find(SubMenuArrowName);
|
||||
if (existingArrow != null)
|
||||
{
|
||||
existingArrow.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (instanceToLayout != null)
|
||||
{
|
||||
LayoutMenuItem(instanceToLayout, itemData, depth, i, items);
|
||||
}
|
||||
|
||||
// 현재 레벨의 전체 너비와 높이 계산
|
||||
if (depth == 0) // 최상위 메뉴 (수평)
|
||||
{
|
||||
if (i > 0) currentLevelCalculatedWidth += menuItemHorizontalGap;
|
||||
currentLevelCalculatedWidth += itemData.IsSeparator ? menuItemSeparatorWidth : menuItemWidth;
|
||||
currentLevelCalculatedHeight = Mathf.Max(currentLevelCalculatedHeight, itemData.IsSeparator ? menuItemSeparatorHeight : menuItemHeight);
|
||||
}
|
||||
else // 하위 메뉴 (수직)
|
||||
{
|
||||
if (i > 0) currentLevelCalculatedHeight += menuItemVerticalGap;
|
||||
|
||||
if (itemData.IsSeparator)
|
||||
{
|
||||
currentLevelCalculatedHeight += menuItemSeparatorHeight + (2 * menuItemSeparatorVerticalMargin);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLevelCalculatedHeight += subMenuItemHeight; // 하위 메뉴 아이템 높이 사용
|
||||
}
|
||||
currentLevelCalculatedWidth = Mathf.Max(currentLevelCalculatedWidth, subMenuItemWidth); // 하위 메뉴 아이템 너비 사용
|
||||
}
|
||||
}
|
||||
return new Vector2(currentLevelCalculatedWidth, currentLevelCalculatedHeight);
|
||||
}
|
||||
|
||||
protected virtual void LayoutMenuItem(GameObject uiElement, MenuItemData itemData, int depth, int itemIndex, List<MenuItemData> siblingItems)
|
||||
{
|
||||
RectTransform rectTransform = uiElement.GetComponent<RectTransform>();
|
||||
if (rectTransform == null)
|
||||
{
|
||||
ULog.Error($"GameObject '{uiElement.name}' is missing a RectTransform component.");
|
||||
return;
|
||||
}
|
||||
|
||||
rectTransform.anchorMin = new Vector2(0, 1);
|
||||
rectTransform.anchorMax = new Vector2(0, 1);
|
||||
rectTransform.pivot = new Vector2(0, 1);
|
||||
|
||||
float currentX = 0;
|
||||
float currentY = 0;
|
||||
float currentItemWidth = (depth == 0) ? menuItemWidth : subMenuItemWidth;
|
||||
float currentItemHeight = (depth == 0) ? menuItemHeight : subMenuItemHeight;
|
||||
|
||||
if (depth == 0) // 1차 깊이 메뉴 (수평 레이아웃)
|
||||
{
|
||||
currentX = menuItemInitX;
|
||||
currentY = -menuItemInitY;
|
||||
|
||||
for (int k = 0; k < itemIndex; k++)
|
||||
{
|
||||
MenuItemData sibling = siblingItems[k];
|
||||
if (sibling.IsSeparator)
|
||||
{
|
||||
currentX += menuItemSeparatorWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentX += menuItemWidth; // 1차 메뉴 너비
|
||||
}
|
||||
currentX += menuItemHorizontalGap;
|
||||
}
|
||||
|
||||
if (itemData.IsSeparator)
|
||||
{
|
||||
rectTransform.sizeDelta = new Vector2(menuItemSeparatorWidth, menuItemSeparatorHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
rectTransform.sizeDelta = new Vector2(menuItemWidth, menuItemHeight);
|
||||
}
|
||||
}
|
||||
else // 2차 깊이 이상 하위 메뉴 (수직 레이아웃)
|
||||
{
|
||||
currentX = subContainerHorizontalPadding; // 하위 메뉴 컨테이너 내부 패딩 적용
|
||||
currentY = -subContainerVerticalPadding; // 하위 메뉴 컨테이너 내부 패딩 적용
|
||||
|
||||
for (int k = 0; k < itemIndex; k++)
|
||||
{
|
||||
MenuItemData sibling = siblingItems[k];
|
||||
if (sibling.IsSeparator)
|
||||
{
|
||||
currentY -= (menuItemSeparatorVerticalMargin + menuItemSeparatorHeight + menuItemSeparatorVerticalMargin);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentY -= subMenuItemHeight; // 하위 메뉴 아이템 높이
|
||||
}
|
||||
currentY -= menuItemVerticalGap;
|
||||
}
|
||||
|
||||
if (itemData.IsSeparator)
|
||||
{
|
||||
currentY -= menuItemSeparatorVerticalMargin; // 구분선 위쪽 마진
|
||||
// 하위 메뉴의 구분선 너비는 subMenuItemWidth를 따르도록 수정
|
||||
rectTransform.sizeDelta = new Vector2(subMenuItemWidth, menuItemSeparatorHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
rectTransform.sizeDelta = new Vector2(subMenuItemWidth, subMenuItemHeight);
|
||||
}
|
||||
}
|
||||
rectTransform.anchoredPosition = new Vector2(currentX, currentY);
|
||||
}
|
||||
|
||||
protected virtual void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance)
|
||||
{
|
||||
if (itemData.IsSeparator) return;
|
||||
|
||||
Transform subMenuContainer = menuItemInstance.transform.Find("SubMenuContainer");
|
||||
if (subMenuContainer != null)
|
||||
{
|
||||
bool isActive = subMenuContainer.gameObject.activeSelf;
|
||||
bool becomingActive = !isActive;
|
||||
|
||||
if (becomingActive)
|
||||
{
|
||||
CloseOtherSubMenusInParent(menuItemInstance.transform.parent, subMenuContainer);
|
||||
// 하위 메뉴를 포함하는 menuItemInstance를 가장 앞으로 가져와서 다른 1단계 메뉴에 가려지지 않도록 합니다.
|
||||
// 이는 menuItemInstance가 menuContainer의 직계 자식일 때 (depth 0 메뉴 아이템) 가장 효과적입니다.
|
||||
// 더 깊은 레벨의 하위 메뉴는 이미 부모 SubMenuContainer 내에 있으므로,
|
||||
// 해당 SubMenuContainer를 포함하는 상위 레벨의 menuItemInstance가 앞으로 와야 합니다.
|
||||
// 최상위 메뉴 아이템(depth 0)의 경우, 그 부모는 menuContainer입니다.
|
||||
// 하위 메뉴 아이템의 경우, 그 부모는 다른 SubMenuContainer입니다.
|
||||
// 현재 로직은 클릭된 menuItemInstance를 그 부모 컨테이너 내에서 맨 앞으로 가져옵니다.
|
||||
menuItemInstance.transform.SetAsLastSibling();
|
||||
}
|
||||
subMenuContainer.gameObject.SetActive(!isActive);
|
||||
|
||||
if (becomingActive)
|
||||
{
|
||||
isAnySubMenuOpen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckIfAnySubMenuRemainsOpen();
|
||||
}
|
||||
UpdateBlockerVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CloseOtherSubMenusInParent(Transform currentMenuItemsParent, Transform subMenuToExclude)
|
||||
{
|
||||
if (currentMenuItemsParent == null)
|
||||
{
|
||||
ULog.Warning("CloseOtherSubMenusInParent 호출 시 currentMenuItemsParent가 null입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < currentMenuItemsParent.childCount; i++)
|
||||
{
|
||||
Transform siblingMenuItemTransform = currentMenuItemsParent.GetChild(i);
|
||||
if (siblingMenuItemTransform == null || siblingMenuItemTransform == subMenuToExclude?.parent) continue;
|
||||
|
||||
Transform potentialSubMenu = siblingMenuItemTransform.Find("SubMenuContainer");
|
||||
|
||||
if (potentialSubMenu != null && potentialSubMenu != subMenuToExclude && potentialSubMenu.gameObject.activeSelf)
|
||||
{
|
||||
potentialSubMenu.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void CloseAllOpenSubMenus()
|
||||
{
|
||||
bool anyActuallyClosed = false;
|
||||
// _menuItemObjects에는 모든 레벨의 메뉴 아이템 GameObject가 포함될 수 있습니다.
|
||||
// 최상위 메뉴 아이템부터 순회하며 하위 메뉴를 닫는 것이 더 안전할 수 있습니다.
|
||||
// 또는, 모든 SubMenuContainer를 직접 찾아 닫습니다.
|
||||
// 현재 구현은 _menuItemObjects를 순회합니다.
|
||||
foreach (GameObject menuItemGO in _menuItemObjects.Values)
|
||||
{
|
||||
if (menuItemGO == null) continue;
|
||||
|
||||
// IsSeparator인 경우 SubMenuContainer가 없을 것이므로, itemData를 통해 확인하거나,
|
||||
// 단순히 Find로 null 체크만 해도 됩니다.
|
||||
MenuItemData data = null; // 실제로는 itemID로 _menuItemData 딕셔너리에서 찾아야 함 (현재 없음)
|
||||
// 여기서는 itemID로 _menuItemObjects에서 가져온 GO이므로, 해당 GO가 separator인지 알 수 없음.
|
||||
// 따라서, 그냥 Find로 처리합니다.
|
||||
// if(menuItemGO.name.StartsWith("Separator_")) continue; // 이름 기반으로 구분선 건너뛰기 (덜 안전함)
|
||||
|
||||
Transform subMenuContainer = menuItemGO.transform.Find("SubMenuContainer");
|
||||
if (subMenuContainer != null && subMenuContainer.gameObject.activeSelf)
|
||||
{
|
||||
subMenuContainer.gameObject.SetActive(false);
|
||||
anyActuallyClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyActuallyClosed || isAnySubMenuOpen)
|
||||
{
|
||||
isAnySubMenuOpen = false;
|
||||
UpdateBlockerVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CheckIfAnySubMenuRemainsOpen()
|
||||
{
|
||||
isAnySubMenuOpen = false;
|
||||
foreach (GameObject menuItemGO in _menuItemObjects.Values)
|
||||
{
|
||||
if (menuItemGO == null) continue;
|
||||
// if(menuItemGO.name.StartsWith("Separator_")) continue;
|
||||
|
||||
Transform subMenuContainer = menuItemGO.transform.Find("SubMenuContainer");
|
||||
if (subMenuContainer != null && subMenuContainer.gameObject.activeSelf)
|
||||
{
|
||||
isAnySubMenuOpen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateBlockerVisibility()
|
||||
{
|
||||
if (uiBlockerInstance != null)
|
||||
{
|
||||
uiBlockerInstance.SetActive(isAnySubMenuOpen);
|
||||
if (isAnySubMenuOpen)
|
||||
{
|
||||
// 블로커가 활성화될 때, 메뉴 시스템 바로 뒤(UI상 바로 앞)에 오도록 합니다.
|
||||
// menuContainer가 Canvas의 직계 자식이라고 가정하고, 그 앞에 블로커를 둡니다.
|
||||
// 더 복잡한 계층 구조에서는 이 부분이 조정되어야 할 수 있습니다.
|
||||
if (menuContainer != null && uiBlockerInstance.transform.parent == menuContainer.parent)
|
||||
{
|
||||
uiBlockerInstance.transform.SetSiblingIndex(menuContainer.GetSiblingIndex());
|
||||
}
|
||||
// 만약 uiBlockerInstance가 Canvas 직속이고, menuContainer는 다른 곳에 있다면,
|
||||
// menuContainer를 포함하는 최상위 UI 요소의 sibling index를 기준으로 해야 합니다.
|
||||
// 여기서는 CreateUIBlocker에서 설정한 부모와 sibling index를 유지하거나,
|
||||
// 필요시 menuContainer의 루트 캔버스 부모를 기준으로 다시 설정합니다.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public virtual void ClearMenuItems()
|
||||
{
|
||||
CloseAllOpenSubMenus();
|
||||
foreach (var pair in _menuItemObjects)
|
||||
{
|
||||
if (pair.Value != null) Destroy(pair.Value);
|
||||
}
|
||||
_menuItemObjects.Clear();
|
||||
|
||||
if (menuContainer != null)
|
||||
{
|
||||
foreach (Transform child in menuContainer)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void UpdateAllMenuTexts(List<MenuItemData> items)
|
||||
{
|
||||
if (_locManager == null)
|
||||
{
|
||||
ULog.Warning("LocalizationManager가 없어 메뉴 텍스트를 업데이트할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
UpdateMenuTextsRecursive(items);
|
||||
}
|
||||
|
||||
protected virtual void UpdateMenuTextsRecursive(List<MenuItemData> items)
|
||||
{
|
||||
if (items == null) return;
|
||||
|
||||
foreach (var itemData in items)
|
||||
{
|
||||
if (itemData.IsSeparator) continue;
|
||||
|
||||
if (_menuItemObjects.TryGetValue(itemData.ItemId, out GameObject menuItemInstance))
|
||||
{
|
||||
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayNameKey))
|
||||
{
|
||||
buttonText.text = _locManager.GetString(itemData.DisplayNameKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
||||
{
|
||||
UpdateMenuTextsRecursive(itemData.SubMenuItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Menu/TopMenuView.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Menu/TopMenuView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec70649e0f460ce458cf6d62498ecf20
|
||||
Reference in New Issue
Block a user