data pipeline 개발

This commit is contained in:
logonkhi
2025-06-05 20:09:28 +09:00
parent ef11fd4a14
commit 4db2791486
108 changed files with 62831 additions and 294 deletions

View File

@@ -0,0 +1,166 @@
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading;
using UVC.Network;
namespace AssetsUVC.Network
{
/// <summary>
/// HTTP 요청을 디바운스(debounce) 처리하기 위한 유틸리티 클래스
/// </summary>
/// <remarks>
/// 이 클래스는 짧은 시간 내에 반복적으로 발생하는 HTTP 요청을 제어하여
/// 마지막 요청만 실행되도록 합니다. 사용자 입력에 따른 API 호출이나
/// 실시간 검색과 같이 연속된 요청을 최적화할 때 유용합니다.
///
/// 기본 사용법:
/// <code>
/// // DebounceRequester 인스턴스 생성
/// var requester = new DebounceRequester();
///
/// // 연속된 검색 요청 예제
/// void OnSearchTextChanged(string searchText)
/// {
/// requester.Request<SearchResult>(
/// 300, // 300ms 디바운스
/// "/api/search",
/// "get",
/// $"{{\"query\": \"{searchText}\"}}",
/// null,
/// false,
/// result => {
/// // 검색 결과 처리
/// DisplaySearchResults(result);
/// }
/// );
/// }
/// </code>
/// </remarks>
public class DebounceRequester
{
/// <summary>
/// 취소 토큰 소스. 이전 요청을 취소하는 데 사용됩니다.
/// </summary>
private CancellationTokenSource cts = new CancellationTokenSource();
/// <summary>
/// 취소 토큰을 재설정하여 이전 요청을 취소합니다.
/// </summary>
/// <remarks>
/// 이 메소드는 새로운 요청이 시작될 때마다 호출되어 이전에 예약된 요청을 취소합니다.
/// </remarks>
private void resetCancellationToken()
{
cts.Cancel();
cts = new CancellationTokenSource();
}
/// <summary>
/// DebounceRequester가 사용한 리소스를 해제합니다.
/// </summary>
/// <remarks>
/// 이 클래스의 사용이 끝나면 항상 Dispose를 호출하여 리소스 누수를 방지해야 합니다.
/// </remarks>
/// <example>
/// <code>
/// // 리소스 해제 예제
/// public class SearchManager : IDisposable
/// {
/// private DebounceRequester requester = new DebounceRequester();
///
/// public void Dispose()
/// {
/// requester.Dispose();
/// }
/// }
/// </code>
/// </example>
public void Dispose()
{
cts.Cancel();
cts.Dispose();
}
/// <summary>
/// 지정된 시간(밀리초) 동안 디바운스된 HTTP 요청을 수행합니다.
/// </summary>
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
/// <param name="milliseconds">디바운스 시간(밀리초)</param>
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
/// <param name="method">HTTP 메소드 (get, post 등)</param>
/// <param name="body">요청 본문으로 전송할 JSON 문자열 (null 가능)</param>
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
/// <param name="useAuth">인증 토큰 사용 여부</param>
/// <param name="action">요청 완료 후 실행할 콜백 함수 (null 가능)</param>
/// <remarks>
/// 이 메소드는 호출된 후 지정된 밀리초 동안 대기하고, 그 시간 내에 다시 호출되면
/// 이전 요청을 취소하고 새로운 타이머를 시작합니다. 디바운스 시간이 경과한 후에만
/// 실제 HTTP 요청이 수행됩니다.
/// </remarks>
/// <example>
/// <code>
/// // 실시간 타이핑 검색 예제
/// private DebounceRequester searchRequester = new DebounceRequester();
///
/// public void OnSearchInputChanged(string searchText)
/// {
/// // 사용자가 타이핑을 멈춘 후 500ms 후에 검색 요청
/// searchRequester.Request<List<ProductData>>(
/// 500,
/// "/api/products/search",
/// "get",
/// $"{{\"query\": \"{searchText}\"}}",
/// null,
/// true,
/// results => {
/// // 검색 결과 UI 업데이트
/// UpdateSearchResultsUI(results);
/// }
/// );
/// }
///
/// // 자동 저장 기능 예제
/// private DebounceRequester autoSaveRequester = new DebounceRequester();
///
/// public void OnDocumentChanged(string documentId, string content)
/// {
/// // 문서 변경 후 2초 동안 추가 변경이 없으면 저장
/// var body = $"{{\"id\": \"{documentId}\", \"content\": \"{content}\"}}";
///
/// autoSaveRequester.Request<SaveResult>(
/// 2000,
/// "/api/documents/save",
/// "post",
/// body,
/// null,
/// true,
/// result => {
/// if (result.success) {
/// ShowSavedNotification();
/// }
/// }
/// );
/// }
/// </code>
/// </example>
public void Request<T>(int milliseconds, string url, string method, string body = null, Dictionary<string, string> header = null, bool useAuth = false, Action<T> action = null)
{
resetCancellationToken();
UniTask.Delay(TimeSpan.FromMilliseconds(milliseconds), cancellationToken: cts.Token, ignoreTimeScale: false).ContinueWith(async () =>
{
if (action != null)
{
T result = await HttpRequester.Request<T>(url, method, body, header, useAuth);
action.Invoke(result);
}
else
{
HttpRequester.Request<T>(url, method, body, header, useAuth).Forget();
}
});
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 84bd91fe0c1db234ea850ff5a747f322

View File

@@ -21,13 +21,88 @@ namespace UVC.Network
/// 이 클래스는 REST API 호출을 위한 메소드들을 제공합니다.
/// GET, POST 요청 및 파일 다운로드를 지원하며 요청/응답 로깅 기능도 포함합니다.
/// 요청은 비동기(UniTask)로 처리됩니다.
///
/// 기본 사용법:
/// <code>
/// // 도메인 설정
/// HttpRequester.Domain = "https://api.example.com";
///
/// // GET 요청 예제
/// var products = await HttpRequester.RequestGet<List<Product>>("/api/products");
///
/// // POST 요청 예제
/// var loginData = new Dictionary<string, object>
/// {
/// { "username", "user@example.com" },
/// { "password", "password123" }
/// };
/// var response = await HttpRequester.RequestPost<LoginResponse>("/api/login", loginData);
/// </code>
/// </remarks>
public static class HttpRequester
public class HttpRequester
{
/// <summary>
/// 기본 도메인 주소입니다.
/// </summary>
public static string Domain {get; set; } = ""; // Replace with your actual API domain
public static string Domain { get; set; } = "";
/// <summary>
/// Dictionary 형태의 본문을 가진 사용자 정의 HTTP 메소드로 API 요청을 수행합니다.
/// </summary>
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
/// <param name="method">HTTP 메소드 (get, post, put 등)</param>
/// <param name="body">요청 본문으로 전송할 Dictionary 데이터 (null 가능)</param>
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
/// <param name="useAuth">인증 토큰 사용 여부</param>
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
/// <example>
/// <code>
/// // PUT 요청 예제
/// public async UniTask<ProductResponse> UpdateProductAsync(int productId, string name, float price)
/// {
/// var body = new Dictionary<string, object>
/// {
/// { "id", productId },
/// { "name", name },
/// { "price", price }
/// };
///
/// return await HttpRequester.Request<ProductResponse>("/api/products", "put", body);
/// }
/// </code>
/// </example>
public static async UniTask<T> Request<T>(string url, string method, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
{
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), method, header, useAuth);
}
/// <summary>
/// 문자열 형태의 본문을 가진 사용자 정의 HTTP 메소드로 API 요청을 수행합니다.
/// </summary>
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
/// <param name="method">HTTP 메소드 (get, post, put 등)</param>
/// <param name="body">요청 본문으로 전송할 JSON 문자열 (null 가능)</param>
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
/// <param name="useAuth">인증 토큰 사용 여부</param>
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
/// <example>
/// <code>
/// // DELETE 요청 예제
/// public async UniTask<bool> DeleteResourceAsync(string resourceId)
/// {
/// string jsonBody = $"{{\"id\": \"{resourceId}\"}}";
///
/// return await HttpRequester.Request<bool>("/api/resources", "delete", jsonBody);
/// }
/// </code>
/// </example>
public static async UniTask<T> Request<T>(string url, string method, string body = null, Dictionary<string, string> header = null, bool useAuth = false)
{
return await Request_<T>(url, body, method, header, useAuth);
}
/// <summary>
/// Dictionary를 JSON body로 변환하여 POST 요청을 수행합니다.
@@ -63,7 +138,7 @@ namespace UVC.Network
/// </example>
public static async UniTask<T> RequestPost<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
{
return await Request<T>(url, body, HTTPMethods.Post, header, useAuth);
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), "post", header, useAuth);
}
/// <summary>
@@ -88,7 +163,7 @@ namespace UVC.Network
/// </example>
public static async UniTask<T> RequestPost<T>(string url, string body, Dictionary<string, string> header = null, bool useAuth = false)
{
return await Request<T>(url, body, HTTPMethods.Post, header, useAuth);
return await Request_<T>(url, body, "post", header, useAuth);
}
/// <summary>
@@ -126,26 +201,31 @@ namespace UVC.Network
/// </example>
public static async UniTask<T> RequestGet<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
{
return await Request<T>(url, body, HTTPMethods.Get, header, useAuth);
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), "get", header, useAuth);
}
/// <summary>
/// HTTP 요청을 처리하는 내부 메소드
/// </summary>
private static async UniTask<T> Request<T>(string url, object body = null, HTTPMethods method = HTTPMethods.Post, Dictionary<string, string> header = null, bool useAuth = false)
/// <remarks>
/// 이 메소드는 모든 HTTP 요청의 공통 로직을 처리합니다:
/// - URL 구성 (Domain과 상대 경로 결합)
/// - HTTP 메소드 설정
/// - 헤더 설정
/// - 요청 로깅
/// - 응답 처리 및 역직렬화
/// </remarks>
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
/// <param name="url">요청할 URL</param>
/// <param name="body">요청 본문 (JSON 문자열)</param>
/// <param name="methodString">HTTP 메소드 문자열</param>
/// <param name="header">추가할 헤더 정보</param>
/// <param name="useAuth">인증 토큰 사용 여부</param>
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
private static async UniTask<T> Request_<T>(string url, string body = null, string methodString = "post", Dictionary<string, string> header = null, bool useAuth = false)
{
string bodyString = "";
if (body != null)
{
if (body is string)
{
bodyString = body.ToString();
}
else if (body is Dictionary<string, object>)
{
bodyString = JsonHelper.ToJson(body);
}
}
HTTPMethods method = StringToMethod(methodString);
if (!url.Contains("http")) url = $"{Domain}{url}";
var request = SelectHTTPRequest(method, url);
@@ -165,20 +245,12 @@ namespace UVC.Network
headerObject.Add(kvp.Key, kvp.Value);
}
}
HttpLogEntry log = ServerLog.LogHttpRequest(url, method.ToString(), headerObject.ToString(Formatting.None), bodyString, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
HttpLogEntry log = ServerLog.LogHttpRequest(url, methodString, headerObject.ToString(Formatting.None), body, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
//Debug.Log($"Request APIToken :{AuthService.Instance.Entiti.accessToken}");
if (body != null)
{
if (body is string)
{
request.UploadSettings.UploadStream =
new MemoryStream(Encoding.UTF8.GetBytes(body as string));
}
else if (body is Dictionary<string, object>)
{
request.UploadSettings.UploadStream =
new MemoryStream(Encoding.UTF8.GetBytes(JsonHelper.ToJson(body)));
}
request.UploadSettings.UploadStream =
new MemoryStream(Encoding.UTF8.GetBytes(body));
}
//var response = await request.GetFromJsonResultAsync<T>();
var response = await request.GetAsStringAsync();
@@ -224,7 +296,7 @@ namespace UVC.Network
/// </example>
public static async UniTask<(T, TimeSpan)> RequestPostWithDuration<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = true)
{
return await RequestWithDuration<T>(url, body, HTTPMethods.Post, header, useAuth);
return await RequestWithDuration<T>(url, body == null ? null : JsonHelper.ToJson(body), "post", header, useAuth);
}
/// <summary>
@@ -256,7 +328,7 @@ namespace UVC.Network
/// </example>
public static async UniTask<(T, TimeSpan)> RequestPostWithDuration<T>(string url, string body, Dictionary<string, string> header = null, bool useAuth = true)
{
return await RequestWithDuration<T>(url, body, HTTPMethods.Post, header, useAuth);
return await RequestWithDuration<T>(url, body, "post", header, useAuth);
}
/// <summary>
@@ -295,26 +367,31 @@ namespace UVC.Network
/// </example>
public static async UniTask<(T, TimeSpan)> RequestGetWithDuration<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = true)
{
return await RequestWithDuration<T>(url, body, HTTPMethods.Get, header, useAuth);
return await RequestWithDuration<T>(url, body == null ? null : JsonHelper.ToJson(body), "get", header, useAuth);
}
/// <summary>
/// HTTP 요청을 처리하고 처리 시간을 측정하는 내부 메소드
/// </summary>
private static async UniTask<(T, TimeSpan)> RequestWithDuration<T>(string url, object body = null, HTTPMethods method = HTTPMethods.Post, Dictionary<string, string> header = null, bool useAuth = true)
/// <remarks>
/// 이 메소드는 모든 성능 측정이 필요한 HTTP 요청의 공통 로직을 처리합니다:
/// - 요청 처리 시간 측정
/// - URL 구성
/// - HTTP 메소드 설정
/// - 헤더 설정
/// - 요청 로깅
/// - 응답 처리 및 역직렬화
/// </remarks>
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
/// <param name="url">요청할 URL</param>
/// <param name="body">요청 본문 (JSON 문자열)</param>
/// <param name="methodString">HTTP 메소드 문자열</param>
/// <param name="header">추가할 헤더 정보</param>
/// <param name="useAuth">인증 토큰 사용 여부</param>
/// <returns>지정된 타입으로 변환된 응답 데이터와 요청 처리 시간의 튜플</returns>
private static async UniTask<(T, TimeSpan)> RequestWithDuration<T>(string url, string body = null, string methodString = "", Dictionary<string, string> header = null, bool useAuth = true)
{
string bodyString = "";
if (body != null)
{
if (body is string)
{
bodyString = body.ToString();
}
else if (body is Dictionary<string, object>)
{
bodyString = JsonHelper.ToJson(body);
}
}
HTTPMethods method = StringToMethod(methodString);
if (!url.Contains("http")) url = $"{Domain}{url}";
var request = SelectHTTPRequest(method, url);
request.DownloadSettings = new Best.HTTP.Request.Settings.DownloadSettings() { ContentStreamMaxBuffered = 1024 * 1024 * 200 };
@@ -334,19 +411,11 @@ namespace UVC.Network
//if (useAuth) request.SetHeader("access-token", AuthService.Instance.Entiti.accessToken);
if (body != null)
{
if (body is string)
{
request.UploadSettings.UploadStream =
new MemoryStream(Encoding.UTF8.GetBytes(body as string));
}
else if (body is Dictionary<string, object>)
{
request.UploadSettings.UploadStream =
new MemoryStream(Encoding.UTF8.GetBytes(JsonHelper.ToJson(body)));
}
request.UploadSettings.UploadStream =
new MemoryStream(Encoding.UTF8.GetBytes(body));
}
HttpLogEntry log = ServerLog.LogHttpRequest(url, method.ToString(), headerObject.ToString(Formatting.None), bodyString, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
HttpLogEntry log = ServerLog.LogHttpRequest(url, methodString, headerObject.ToString(Formatting.None), body, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
//var response = await request.GetFromJsonResultAsync<T>();
var now = DateTime.UtcNow;
var response = await request.GetAsStringAsync();
@@ -415,7 +484,7 @@ namespace UVC.Network
req.DownloadSettings.OnDownloadStarted = null;
req.DownloadSettings.OnDownloadProgress = null;
req.DownloadSettings.DownloadStreamFactory = null;
switch (req.State)
{
case HTTPRequestStates.Finished:
@@ -463,6 +532,20 @@ namespace UVC.Network
/// <summary>
/// 다운로드 스트림을 처리하여 파일로 저장합니다.
/// </summary>
/// <remarks>
/// 이 메소드는 스트림 데이터를 지정된 파일로 저장하며, 다운로드가 완료되거나 중단될 때까지 블로킹 방식으로 데이터를 처리합니다.
/// 다운로드에 실패한 경우 (길이가 0인 경우) 빈 파일은 삭제됩니다.
/// </remarks>
/// <param name="savePath">다운로드한 데이터를 저장할 파일 경로</param>
/// <param name="blockingStream">다운로드 콘텐츠 스트림</param>
/// <returns>저장된 파일의 크기(바이트)</returns>
/// <example>
/// <code>
/// // 내부적으로 Download 메소드에서 사용됩니다
/// long fileSize = await UniTask.RunOnThreadPool<long>(() =>
/// ConsumeDownloadStream(savePath, downloadStream as BlockingDownloadContentStream));
/// </code>
/// </example>
private static long ConsumeDownloadStream(string savePath, BlockingDownloadContentStream blockingStream)
{
long len = 0;
@@ -502,6 +585,18 @@ namespace UVC.Network
/// <summary>
/// HTTP 메소드와 URL을 기반으로 적절한 HTTPRequest 객체를 생성합니다.
/// </summary>
/// <param name="methods">요청에 사용할 HTTP 메소드</param>
/// <param name="url">요청할 URL</param>
/// <param name="onRequest">요청 완료 시 호출될 콜백 (선택 사항)</param>
/// <returns>생성된 HTTPRequest 객체</returns>
/// <example>
/// <code>
/// // 내부 사용 예제 (직접 사용하지 않음)
/// HTTPRequest request = SelectHTTPRequest(HTTPMethods.Get, "https://api.example.com/data");
/// request.SetHeader("Custom-Header", "HeaderValue");
/// request.Send();
/// </code>
/// </example>
private static HTTPRequest SelectHTTPRequest(HTTPMethods methods, string url, OnRequestFinishedDelegate onRequest = null)
{
switch (methods)
@@ -518,5 +613,22 @@ namespace UVC.Network
return null;
}
/// <summary>
/// 문자열 형태의 HTTP 메소드를 Best.HTTP.HTTPMethods 열거형으로 변환합니다.
/// </summary>
/// <param name="method">변환할 HTTP 메소드 문자열 ("get", "post" 등)</param>
/// <returns>해당하는 HTTPMethods 열거형 값</returns>
/// <exception cref="ArgumentException">지원하지 않는 HTTP 메소드인 경우 발생</exception>
private static HTTPMethods StringToMethod(string method)
{
return method.ToLower() switch
{
"get" => HTTPMethods.Get,
"post" => HTTPMethods.Post,
_ => throw new ArgumentException($"Unsupported HTTP method: {method}"),
};
}
}
}