diff --git a/Assets/NuGet.config.meta b/Assets/NuGet.config.meta
index 4c59ae08..c507c5de 100644
--- a/Assets/NuGet.config.meta
+++ b/Assets/NuGet.config.meta
@@ -1,2 +1,28 @@
fileFormatVersion: 2
-guid: ea8e3b9f995342c4b839ece2b6547a20
\ No newline at end of file
+guid: ea8e3b9f995342c4b839ece2b6547a20
+labels:
+- NuGetForUnity
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 3
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ Any:
+ enabled: 0
+ settings: {}
+ Editor:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ WindowsStoreApps:
+ enabled: 0
+ settings: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/UVC/Data/Core/DataArray.cs b/Assets/Scripts/UVC/Data/Core/DataArray.cs
index 039093d4..be91fe45 100644
--- a/Assets/Scripts/UVC/Data/Core/DataArray.cs
+++ b/Assets/Scripts/UVC/Data/Core/DataArray.cs
@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
-using UnityEngine;
namespace UVC.Data.Core
{
diff --git a/Assets/Scripts/UVC/Data/Core/DataObject.cs b/Assets/Scripts/UVC/Data/Core/DataObject.cs
index a6f5aff8..fd34aca7 100644
--- a/Assets/Scripts/UVC/Data/Core/DataObject.cs
+++ b/Assets/Scripts/UVC/Data/Core/DataObject.cs
@@ -383,13 +383,14 @@ namespace UVC.Data.Core
/// 변환된 정수 값 또는 기본값
public int? GetInt(string propertyName, int? defaultValue = null)
{
- if (TryGetValue(propertyName, out object? value) && value != null)
+ if (!TryGetValue(propertyName, out var v) || v == null) return defaultValue;
+ return v switch
{
- if (value is int intValue)
- return intValue;
- return Convert.ToInt32(value);
- }
- return defaultValue;
+ int i => i,
+ long l when l >= int.MinValue && l <= int.MaxValue => (int)l,
+ string s when int.TryParse(s, out var i2) => i2,
+ _ => defaultValue
+ };
}
///
@@ -691,7 +692,13 @@ namespace UVC.Data.Core
///
public void RemoveAll()
{
- foreach (var value in Values.ToList())
+ if (Count == 0)
+ {
+ changedProperies.Clear();
+ return;
+ }
+
+ foreach (var value in Values)
{
if (value is DataObject dataObject)
{
@@ -841,7 +848,7 @@ namespace UVC.Data.Core
}
if (updatedDataOnly) return;
-
+
// 현재 객체에만 있는 속성은 제거합니다.
var keysToRemove = this.Keys.Except(otherDataObject.Keys).ToList();
foreach (var key in keysToRemove)
@@ -849,7 +856,7 @@ namespace UVC.Data.Core
this.Remove(key);
changedProperies.Remove(key);
}
-
+
}
///
@@ -905,11 +912,12 @@ namespace UVC.Data.Core
public void ReturnToPool()
{
+ if (IsInPool) return; // 중복 반환 방지
if (CreatedFromPool)
{
Reset();
}
- else
+ else
{
DataObjectPool.Return(this);
}
diff --git a/Assets/Scripts/UVC/Data/Http/HttpDataFetcher.cs b/Assets/Scripts/UVC/Data/Http/HttpDataFetcher.cs
index c458f464..e79b67df 100644
--- a/Assets/Scripts/UVC/Data/Http/HttpDataFetcher.cs
+++ b/Assets/Scripts/UVC/Data/Http/HttpDataFetcher.cs
@@ -194,8 +194,13 @@ namespace UVC.Data.Http
{
if (!info.Repeat)
{
+ // WebGL 환경에서는 ThreadPool 사용 불가 -> 메인 스레드에서 비동기 실행
+#if UNITY_WEBGL && !UNITY_EDITOR
+ await ExecuteSingle(key, info);
+#else
// 단일 실행 로직 호출
await UniTask.RunOnThreadPool(() => ExecuteSingle(key, info));
+#endif
Debug.Log($"HTTP request '{key}' executed successfully.");
}
else
@@ -340,9 +345,12 @@ namespace UVC.Data.Http
{
try
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ await ExecuteSingle(key, info, cts.Token);
+#else
// 단일 실행 로직 호출
await UniTask.RunOnThreadPool(() => ExecuteSingle(key, info, cts.Token));
-
+#endif
// 지정된 횟수만큼 반복한 경우 중지
if (info.RepeatCount > 0)
{
diff --git a/Assets/Scripts/UVC/Data/Mqtt/MqttDataReceiver.cs b/Assets/Scripts/UVC/Data/Mqtt/MqttDataReceiver.cs
index 95f2a2ca..1128fc72 100644
--- a/Assets/Scripts/UVC/Data/Mqtt/MqttDataReceiver.cs
+++ b/Assets/Scripts/UVC/Data/Mqtt/MqttDataReceiver.cs
@@ -107,6 +107,13 @@ namespace UVC.Data.Mqtt
private MqttDataPicker? defaultDataPicker;
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: Thread 기반 MqttWorker 대체
+ private MQTTService? webGlMqttService;
+ // WebGL 대용량(JSON 배열) 처리 시 프레임 분할 청크 크기
+ private const int WebGlArrayChunkSize = 256;
+#endif
+
///
/// MqttDataReceiver 인스턴스를 생성합니다.
///
@@ -125,7 +132,11 @@ namespace UVC.Data.Mqtt
{
this.domain = string.IsNullOrEmpty(domain) ? Constants.MQTT_DOMAIN : domain;
this.port = port;
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL에서는 mqttWorker 사용 안 함
+#else
mqttWorker.SetDomainPort(this.domain, this.port);
+#endif
}
///
@@ -136,7 +147,11 @@ namespace UVC.Data.Mqtt
public void SetDataPicker(MqttDataPicker dataPicker)
{
defaultDataPicker = dataPicker;
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // MessagePack 설정은 필요 시 MQTTService 확장
+#else
mqttWorker.SetEnableMessagePack(dataPicker.EnableMessagePack);
+#endif
}
///
@@ -179,6 +194,20 @@ namespace UVC.Data.Mqtt
///
public void Start()
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ if (UseMockup)
+ {
+ ULog.Warn("WebGL Mockup 모드는 아직 구현되지 않았습니다.");
+ return;
+ }
+ if (webGlMqttService != null) return;
+ webGlMqttService = new MQTTService(domain, port, autoReconnect: true, onBackground: false);
+ foreach (var topic in topics)
+ {
+ webGlMqttService.AddTopicHandler(topic, OnWebGlRawTopicMessage);
+ }
+ webGlMqttService.Connect();
+#else
if (!UseMockup)
{
if (mqttWorker.IsRunning) return;
@@ -198,8 +227,35 @@ namespace UVC.Data.Mqtt
//}
//mockupMQTT.Connect();
}
+#endif
}
+ ///
+ /// WebGL 환경에서 MQTTService가 직접 호출하는 Raw 메시지 처리 진입점
+ ///
+ private void OnWebGlRawTopicMessage(string topic, string message)
+ {
+ // WebGL: 스레드풀 없음 → 메인 스레드에서 부하 분산을 위해 한 프레임 양보
+ RunBackground(() => OnTopicMessageLogic(topic, message));
+ }
+
+ ///
+ /// 플랫폼별 백그라운드 실행 셔임
+ ///
+ private static void RunBackground(Action action)
+ {
+#if UNITY_WEBGL && !UNITY_EDITOR
+ UniTask.Void(async () =>
+ {
+ await UniTask.NextFrame();
+ action();
+ });
+#else
+ UniTask.RunOnThreadPool(action).Forget();
+#endif
+ }
+
+
///
/// 토픽에서 수신된 MQTT 데이터 패킷 목록을 처리합니다.
///
@@ -317,11 +373,21 @@ namespace UVC.Data.Mqtt
}
}
+
///
/// 파이프라인을 중지하고 모든 토픽 구독을 해제한 후 MQTT 브로커와의 연결을 종료합니다.
///
public void Stop()
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ if (webGlMqttService != null)
+ {
+ webGlMqttService.Disconnect();
+ webGlMqttService.ClearTopicHandlers();
+ webGlMqttService = null;
+ }
+ firstMessageReceived.Clear();
+#else
if (!UseMockup)
{
if (!mqttWorker.IsRunning) return;
@@ -337,6 +403,7 @@ namespace UVC.Data.Mqtt
// Mockup 모드인 경우 MockMQTTService를 사용하여 연결을 종료합니다.
//mockupMQTT?.Disconnect();
}
+#endif
}
///
@@ -346,10 +413,15 @@ namespace UVC.Data.Mqtt
/// 를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.
public void Dispose()
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ webGlMqttService?.Dispose();
+ webGlMqttService = null;
+#else
if (!UseMockup) mqttWorker.Dispose();
- //else mockupMQTT?.Disconnect();
+#endif
configList.Clear();
firstMessageReceived.Clear();
+ topics.Clear();
}
}
diff --git a/Assets/Scripts/UVC/Data/UserSetting.cs b/Assets/Scripts/UVC/Data/UserSetting.cs
index 7c4e4b8d..a898f9c5 100644
--- a/Assets/Scripts/UVC/Data/UserSetting.cs
+++ b/Assets/Scripts/UVC/Data/UserSetting.cs
@@ -77,6 +77,25 @@ namespace UVC.Data
public static async UniTask LoadFromAppData()
{
string persistentDataPath = UnityEngine.Application.persistentDataPath;
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: 스레드 사용 불가. 메인 스레드에서 파일 작업 수행하되 프레임 양보로 스톨 방지.
+ string folderPath = System.IO.Path.Combine(persistentDataPath, "user");
+ DirectoryInfo directory = new DirectoryInfo(folderPath);
+ if (!directory.Exists) return;
+
+ FileInfo[] files = directory.GetFiles("*.json");
+ foreach (FileInfo file in files)
+ {
+ // 프레임 양보
+ await UniTask.Yield();
+
+ string filePath = file.FullName;
+ string fileName = System.IO.Path.GetFileNameWithoutExtension(filePath);
+ if (string.IsNullOrEmpty(fileName)) continue;
+ string jsonString = System.IO.File.ReadAllText(filePath);
+ AddSetting(fileName, new UserSetting(jsonString));
+ }
+#else
await UniTask.RunOnThreadPool(() =>
{
string folderPath = System.IO.Path.Combine(persistentDataPath, "user");
@@ -92,6 +111,7 @@ namespace UVC.Data
AddSetting(fileName, new UserSetting(jsonString));
}
});
+#endif
}
///
@@ -104,7 +124,30 @@ namespace UVC.Data
///
public static async UniTask SaveToAppData()
{
+
string persistentDataPath = UnityEngine.Application.persistentDataPath;
+#if UNITY_WEBGL && !UNITY_EDITOR
+ foreach (var kv in _userDatas)
+ {
+ await UniTask.Yield(); // 긴 루프 스톨 방지
+
+ string key = kv.Key;
+ if (string.IsNullOrEmpty(key)) continue;
+ if (!_userDatas.TryGetValue(key, out var setting)) continue;
+
+ try
+ {
+ string folderPath = Path.Combine(persistentDataPath, "user");
+ if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath);
+ string filePath = Path.Combine(folderPath, $"{key}.json");
+ File.WriteAllText(filePath, setting.ToJsonString());
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"SaveToAppData Error(WebGL): {ex.Message}");
+ }
+ }
+#else
await UniTask.RunOnThreadPool(() =>
{
foreach (var keyValue in _userDatas)
@@ -127,7 +170,7 @@ namespace UVC.Data
}
});
}
-
+#endif
public static UserSetting? FromDataObject(DataObject dataObject)
{
if (dataObject == null) return null;
diff --git a/Assets/Scripts/UVC/Factory/Playback/PlaybackRepository.cs b/Assets/Scripts/UVC/Factory/Playback/PlaybackRepository.cs
index 1b21ca9e..fb313295 100644
--- a/Assets/Scripts/UVC/Factory/Playback/PlaybackRepository.cs
+++ b/Assets/Scripts/UVC/Factory/Playback/PlaybackRepository.cs
@@ -43,6 +43,15 @@ namespace UVC.Factory.Playback
try
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: ThreadPool 사용 없이 직접 실행
+ var response = await HttpRequester.RequestGet>>>(URLList.Get("playbackList"));
+ if (string.Equals(response.message, "success", StringComparison.OrdinalIgnoreCase))
+ {
+ return new Dictionary>(response.data);
+ }
+ return null;
+#else
return await UniTask.RunOnThreadPool>?>(async () =>
{
var response = await HttpRequester.RequestGet>>>(URLList.Get("playbackList"));
@@ -52,7 +61,7 @@ namespace UVC.Factory.Playback
}
return null;
});
-
+#endif
}
catch (Exception e)
{
diff --git a/Assets/Scripts/UVC/Factory/Playback/PlaybackSQLiteService.cs b/Assets/Scripts/UVC/Factory/Playback/PlaybackSQLiteService.cs
index d8bca4a7..d9565802 100644
--- a/Assets/Scripts/UVC/Factory/Playback/PlaybackSQLiteService.cs
+++ b/Assets/Scripts/UVC/Factory/Playback/PlaybackSQLiteService.cs
@@ -96,6 +96,32 @@ namespace UVC.Factory
/// 조회된 데이터 리스트
public async UniTask> SelectBySecond(string selectTime, int second, bool orderAsc = true, int limit = 0)
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ DateTime target = DateTimeUtil.UtcParse(selectTime).AddSeconds(second);
+ string targetTime = DateTimeUtil.FormatTime(target);
+
+ queryBuilder.Append(queryParts[0]);
+ if (second > 0)
+ {
+ queryBuilder.Append($"{queryParts[1]}{selectTime}{queryParts[2]}{targetTime}'");
+ }
+ else
+ {
+ queryBuilder.Append($"{queryParts[3]}{selectTime}{queryParts[4]}{targetTime}'");
+ }
+ queryBuilder.Append($"{queryParts[5]}{(orderAsc ? "asc" : "desc")}");
+ if (limit > 0)
+ queryBuilder.Append($"{queryParts[6]}{limit}");
+ queryBuilder.Append(";");
+
+ var query = queryBuilder.ToString();
+ queryBuilder.Clear();
+
+ // 동기 실행 (WebGL은 ThreadPool 사용 불가)
+ var list = dbConnection.Query(query);
+ return list;
+#else
+
bool isMainThread = PlayerLoopHelper.IsMainThread;
List result = await UniTask.RunOnThreadPool(() =>
{
@@ -126,6 +152,7 @@ namespace UVC.Factory
});
if (!isMainThread) await UniTask.SwitchToThreadPool();
return result;
+#endif
}
StringBuilder queryBuilder = new();
@@ -143,6 +170,29 @@ namespace UVC.Factory
/// 조회된 데이터 리스트
public async UniTask> SelectBySecondBaseInfo(string selectTime, int second, bool orderAsc = false, int limit = 1)
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ DateTime target = DateTimeUtil.UtcParse(selectTime).AddSeconds(second);
+ string targetTime = DateTimeUtil.FormatTime(target);
+
+ queryBuilder.Append("SELECT * FROM baseInfo WHERE ");
+ if (second > 0)
+ {
+ queryBuilder.Append($"timestamp >= '{selectTime}' AND timestamp < '{targetTime}'");
+ }
+ else
+ {
+ queryBuilder.Append($"timestamp <= '{selectTime}' AND timestamp > '{targetTime}'");
+ }
+ queryBuilder.Append($" ORDER BY timestamp {(orderAsc ? "asc" : "desc")}");
+ if (limit > 0) queryBuilder.Append($" LIMIT {limit}");
+ queryBuilder.Append(";");
+
+ var query = queryBuilder.ToString();
+ queryBuilder.Clear();
+
+ var list = dbConnection.Query(query);
+ return list;
+#else
bool isMainThread = PlayerLoopHelper.IsMainThread;
List result = await UniTask.RunOnThreadPool(() =>
{
@@ -171,6 +221,7 @@ namespace UVC.Factory
});
if (!isMainThread) await UniTask.SwitchToThreadPool();
return result;
+#endif
}
}
diff --git a/Assets/Scripts/UVC/Factory/Playback/PlaybackService.cs b/Assets/Scripts/UVC/Factory/Playback/PlaybackService.cs
index 6dd18e7b..94011fb6 100644
--- a/Assets/Scripts/UVC/Factory/Playback/PlaybackService.cs
+++ b/Assets/Scripts/UVC/Factory/Playback/PlaybackService.cs
@@ -125,6 +125,28 @@ namespace UVC.Factory.Playback
///
public async UniTask DispatchBaseInfoData(string date, string time, string fileName, string minute = "00", string second = "00")
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: ThreadPool 미사용, 직접 실행
+ this.date = date;
+ this.time = time;
+ this.fileName = fileName;
+ DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:{minute}:{second}.000Z");
+ string formatTime = DateTimeUtil.FormatTime(dateTime);
+ List list = await repository.SelectBySecondBaseInfo(date, fileName, formatTime);
+ if (list.Count > 0)
+ {
+ OnChangedTime?.Invoke();
+ HttpRequestConfig httpRequestConfig = new HttpRequestConfig(string.Empty);
+ httpRequestConfig.SetUpdatedDataOnly(true);
+ httpRequestConfig.SetSplitResponseByKey(true);
+ httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
+ httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
+ foreach (var item in list)
+ {
+ HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data, null, true);
+ }
+ }
+#else
await UniTask.RunOnThreadPool(async () =>
{
//헝가리 시간임
@@ -149,6 +171,7 @@ namespace UVC.Factory.Playback
}
}
});
+#endif
}
///
@@ -163,6 +186,25 @@ namespace UVC.Factory.Playback
///
public async UniTask DispatchRealTimeData(int second, int speed)
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ int newSecond = second;
+ if (newSecond > 36000) newSecond = 36000;
+ DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:00:00.000Z").AddSeconds(newSecond);
+ string formatTime = DateTimeUtil.FormatTime(dateTime);
+ List list = await repository.SelectBySecondAsync(date, fileName, formatTime, 1);
+ if (list.Count > 0)
+ {
+ HttpRequestConfig httpRequestConfig = new HttpRequestConfig(string.Empty);
+ httpRequestConfig.SetUpdatedDataOnly(true);
+ httpRequestConfig.SetSplitResponseByKey(true);
+ httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
+ httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
+ foreach (var item in list)
+ {
+ HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
+ }
+ }
+#else
await UniTask.RunOnThreadPool(async () =>
{
int newSecond = second;
@@ -186,6 +228,7 @@ namespace UVC.Factory.Playback
}
}
});
+#endif
}
///
@@ -333,6 +376,16 @@ namespace UVC.Factory.Playback
// Debug.Log($"zipper3 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
//}
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: 메인 스레드에서 직접 파일 이동/삭제
+ if (File.Exists(utcSqlFilePath))
+ {
+ if (File.Exists(sqlFilePath)) File.Delete(sqlFilePath);
+ File.Copy(utcSqlFilePath, sqlFilePath);
+ File.Delete(utcSqlFilePath);
+ }
+ if (File.Exists(utcZipFilePath)) File.Delete(utcZipFilePath);
+#else
await UniTask.RunOnThreadPool(() =>
{
//압축해제 한 파일 이동
@@ -346,7 +399,7 @@ namespace UVC.Factory.Playback
//zip 파일 삭제
File.Delete(utcZipFilePath);
});
-
+#endif
if (OnComplete != null) OnComplete.Invoke(errorMessage);
}
},
diff --git a/Assets/Scripts/UVC/Log/ServerLog.cs b/Assets/Scripts/UVC/Log/ServerLog.cs
index 657781c6..fb07b273 100644
--- a/Assets/Scripts/UVC/Log/ServerLog.cs
+++ b/Assets/Scripts/UVC/Log/ServerLog.cs
@@ -27,6 +27,11 @@ namespace UVC.Log
{
try
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: 백그라운드(ThreadPool) 사용 불가 → 동기 초기화
+ EnsureDatabaseConnectionWebGL();
+ CleanupOldLogsWebGL();
+#else
CheckDatabaseConnectionAsync().ContinueWith((_) =>
{
lock (_dbLock)
@@ -45,6 +50,7 @@ namespace UVC.Log
}
}
});
+#endif
}
catch (Exception ex)
{
@@ -53,6 +59,56 @@ namespace UVC.Log
}
}
+#if UNITY_WEBGL && !UNITY_EDITOR
+ ///
+ /// WebGL 환경에서 데이터베이스 연결을 동기적으로 초기화합니다.
+ ///
+ private static void EnsureDatabaseConnectionWebGL()
+ {
+ if (db != null) return;
+
+ string appDataPath = Application.persistentDataPath;
+ DirectoryInfo di = new DirectoryInfo(Path.Combine(appDataPath, "unityLogs"));
+ if (!di.Exists) di.Create();
+
+ // WebGL Player 환경 구분
+ string dbPath = Application.platform == RuntimePlatform.WebGLPlayer
+ ? Path.Combine(di.FullName, "serverWebGLLog.db")
+ : Path.Combine(di.FullName, "serverEditorLog.db");
+
+ db = new SQLiteConnection(dbPath);
+ try
+ {
+ db.CreateTable();
+ db.CreateTable();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ }
+ }
+
+ ///
+ /// WebGL에서 오래된 로그를 동기적으로 정리합니다.
+ ///
+ private static void CleanupOldLogsWebGL()
+ {
+ lock (_dbLock)
+ {
+ if (!doDeletedHttp)
+ {
+ db?.Execute("DELETE FROM HttpLogEntry WHERE RequestDate < datetime('now', '-1 month', 'localtime');");
+ doDeletedHttp = true;
+ }
+ if (!doDeletedMqtt)
+ {
+ db?.Execute("DELETE FROM MqttLogEntry WHERE Date < datetime('now', '-1 month', 'localtime');");
+ doDeletedMqtt = true;
+ }
+ }
+ }
+#endif
+
///
/// HTTP 요청을 로깅하여 데이터베이스에 로그 항목을 생성하고 저장합니다.
///
@@ -67,6 +123,10 @@ namespace UVC.Log
/// 요청 URL, 메소드, 헤더, 본문, 날짜와 같은 세부 정보를 포함합니다.
public static HttpLogEntry LogHttpRequest(string url, string method, string headerJson, string bodyString, string date)
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: 초기화 보장
+ if (db == null) EnsureDatabaseConnectionWebGL();
+#endif
var logEntry = new HttpLogEntry
{
RequestURL = url,
@@ -97,6 +157,10 @@ namespace UVC.Log
/// langword="null"/>일 수 없습니다.
public static void LogHttpResponse(HttpLogEntry httpLogEntry)
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ if (db == null) EnsureDatabaseConnectionWebGL();
+#endif
+
lock (_dbLock)
{
try
@@ -123,6 +187,9 @@ namespace UVC.Log
/// 기록된 MQTT 메시지를 나타내는 객체입니다.
public static MqttLogEntry LogMqtt(string url, string port, string topic, string payload, string date, string? exception = null)
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ if (db == null) EnsureDatabaseConnectionWebGL();
+#endif
var logEntry = new MqttLogEntry
{
URL = url,
@@ -150,6 +217,11 @@ namespace UVC.Log
protected static async Task CheckDatabaseConnectionAsync()
{
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: 비동기 + 스레드 전환 제거, 동기 루틴 사용
+ EnsureDatabaseConnectionWebGL();
+ return;
+#else
if (db == null)
{
string dbPath = string.Empty;
@@ -181,6 +253,7 @@ namespace UVC.Log
Debug.LogException(ex);
}
}
+#endif
}
}
diff --git a/Assets/Scripts/UVC/Network/HttpRequester.cs b/Assets/Scripts/UVC/Network/HttpRequester.cs
index 6abbce36..0a45762d 100644
--- a/Assets/Scripts/UVC/Network/HttpRequester.cs
+++ b/Assets/Scripts/UVC/Network/HttpRequester.cs
@@ -256,9 +256,11 @@ namespace UVC.Network
bool isMainThread = PlayerLoopHelper.IsMainThread;
//var response = await request.GetFromJsonResultAsync();
var response = await request.GetAsStringAsync();
- if(!isMainThread) await UniTask.SwitchToThreadPool();
+#if !UNITY_WEBGL || UNITY_EDITOR
+ if (!isMainThread) await UniTask.SwitchToThreadPool();
log.ResponseData = response;
log.ResponseDate = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
+#endif
ServerLog.LogHttpResponse(log);
//T가 string이면
if (typeof(T) == typeof(string))
@@ -423,7 +425,9 @@ namespace UVC.Network
var now = DateTime.UtcNow;
bool isMainThread = PlayerLoopHelper.IsMainThread;
var response = await request.GetAsStringAsync();
+#if !UNITY_WEBGL || UNITY_EDITOR
if (!isMainThread) await UniTask.SwitchToThreadPool();
+#endif
var diff = DateTime.UtcNow - now;
log.ResponseData = response;
log.ResponseDate = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
@@ -495,8 +499,22 @@ namespace UVC.Network
case HTTPRequestStates.Finished:
if (resp.IsSuccess)
{
- //System.IO.File.WriteAllBytes(savePath, resp.Data);
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: 백그라운드 쓰레드 불가 -> 완료 시 메모리 데이터로 파일 저장
+ try
+ {
+ System.IO.File.WriteAllBytes(savePath, resp.Data);
+ OnComplete?.Invoke();
+ }
+ catch (Exception ex)
+ {
+ ULog.Error($"Failed to write file on WebGL: {ex.Message}", ex);
+ OnError?.Invoke($"Failed to write file: {ex.Message}");
+ }
+#else
+ //스트리밍 파일 기록은 OnDownloadStarted에서 처리됨
OnComplete.Invoke();
+#endif
}
else
{
@@ -515,6 +533,15 @@ namespace UVC.Network
};
var request = SelectHTTPRequest(HTTPMethods.Get, url, onRequest);
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: 쓰레드 기반 스트리밍 미사용. 진행률 콜백만 연결.
+ request.DownloadSettings.OnDownloadProgress += (HTTPRequest req, long progress, long length) =>
+ {
+ ULog.Debug($"Download Progress! progress:{progress} length:{length}");
+ OnProgress?.Invoke(progress, length);
+ };
+ // DownloadStreamFactory/BlockingDownloadContentStream 사용 금지
+#else
request.DownloadSettings.OnDownloadStarted += async (HTTPRequest req, HTTPResponse resp, DownloadContentStream stream) =>
{
@@ -533,7 +560,7 @@ namespace UVC.Network
};
request.DownloadSettings.DownloadStreamFactory = (req, resp, bufferAvailableHandler)
=> new BlockingDownloadContentStream(resp, req.DownloadSettings.ContentStreamMaxBuffered, bufferAvailableHandler);
-
+#endif
return request.Send();
}
diff --git a/Assets/Scripts/UVC/Network/MQTTService.cs b/Assets/Scripts/UVC/Network/MQTTService.cs
index a3c86fa8..c7f199fa 100644
--- a/Assets/Scripts/UVC/Network/MQTTService.cs
+++ b/Assets/Scripts/UVC/Network/MQTTService.cs
@@ -1,4 +1,4 @@
-using Best.MQTT;
+using Best.MQTT;
using Best.MQTT.Packets.Builders;
using Cysharp.Threading.Tasks;
using System;
@@ -16,6 +16,7 @@ namespace UVC.network
///
/// 이 클래스는 스레드 안전한 방식으로 토픽 핸들러를 관리하며, 연결 끊김 시 자동 재연결 기능을 제공합니다.
/// 내부적으로 Best.MQTT 라이브러리를 사용하여 MQTT 프로토콜 통신을 구현합니다.
+ /// WebGL 플랫폼에서는 스레드풀이 지원되지 않으므로, 메시지 핸들러는 메인 스레드에서 실행됩니다.
///
public class MQTTService
{
@@ -321,26 +322,32 @@ namespace UVC.network
///
private void OnTopic(MQTTClient client, SubscriptionTopic topic, string topicName, ApplicationMessage message)
{
- // 메인 스레드에서 실행 중인지 확인합니다.
- bool isMainThread = PlayerLoopHelper.IsMainThread;
- //Debug.Log($"MQTT OnTopic isMainThread={isMainThread}, onBackgroundThread:{onBackgroundThread}, {topic.Filter.OriginalFilter}");
- if (isMainThread && onBackgroundThread)
+ DispatchTopic(() => OnTopicLogic(client, topic, topicName, message));
+ }
+
+ // 웹GL 대응: 스레드풀 미지원 시 메인 스레드로 포스트
+ private void DispatchTopic(Action work)
+ {
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: 스레드풀 없음 → 다음 프레임에 메인 스레드로 실행
+ UniTask.Post(work);
+#else
+
+ if (onBackgroundThread)
{
- // 백그라운드 스레드에서 실행
- UniTask.RunOnThreadPool(() => OnTopicLogic(client, topic, topicName, message)).Forget();
- }
- else if (!isMainThread && !onBackgroundThread)
- {
- // 메인 스레드에서 실행
- UniTask.Post(() => OnTopicLogic(client, topic, topicName, message));
+ UniTask.RunOnThreadPool(work).Forget();
}
else
{
- // 메인 스레드에서 실행
- OnTopicLogic(client, topic, topicName, message);
+ if (PlayerLoopHelper.IsMainThread)
+ work();
+ else
+ UniTask.Post(work);
}
+#endif
}
+
private void OnTopicLogic(MQTTClient client, SubscriptionTopic topic, string topicName, ApplicationMessage message)
{
//Debug.Log($"MQTT OnTopicLogic isMainThread={PlayerLoopHelper.IsMainThread}");
diff --git a/Assets/Scripts/UVC/UI/Window/HierarchyWindow.cs b/Assets/Scripts/UVC/UI/Window/HierarchyWindow.cs
index 4109c0ef..cda60108 100644
--- a/Assets/Scripts/UVC/UI/Window/HierarchyWindow.cs
+++ b/Assets/Scripts/UVC/UI/Window/HierarchyWindow.cs
@@ -328,7 +328,7 @@ namespace UVC.UI.Window
searchProgress =1f;
// UI 반영은 메인 스레드에서
- await UniTask.SwitchToMainThread();
+ if (!PlayerLoopHelper.IsMainThread) await UniTask.SwitchToMainThread();
try
{
diff --git a/Assets/Scripts/UVC/util/Zipper.cs b/Assets/Scripts/UVC/util/Zipper.cs
index 8d9bbdd1..a5e1c565 100644
--- a/Assets/Scripts/UVC/util/Zipper.cs
+++ b/Assets/Scripts/UVC/util/Zipper.cs
@@ -108,12 +108,23 @@ namespace UVC.Util
{
//getFileInfo zip 내의 콘텐츠의 총 압축되지 않은 바이트를 반환한다.
ulong totalBytes = lzip.getFileInfo(zipFilePath);
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: ThreadPool 사용 불가 → 동기 처리
+ OnProgress?.Invoke(0, (long)totalBytes, 0f);
+ int result = lzip.decompress_File(zipFilePath, decompressFolderPath, progress, null, progress2);
+ isComplete = true;
+ percent = 1f;
+ OnProgress?.Invoke((long)totalBytes, (long)totalBytes, 1f);
+#else
+
CountPercentZipAsync(totalBytes).Forget();
int result = await UniTask.RunOnThreadPool(() =>
{
return lzip.decompress_File(zipFilePath, decompressFolderPath, progress, null, progress2);
});
isComplete = true;
+#endif
+
if (result == 1) //success
{
return null;
@@ -193,12 +204,23 @@ namespace UVC.Util
//fsecurity.AddAccessRule(new FileSystemAccessRule(new NTAccount(name[1]), FileSystemRights.FullControl, InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit, PropagationFlags.None, AccessControlType.Allow));
//fileInfo.SetAccessControl(fsecurity);
long totalBytes = lzma.getFileSize(zipFilePath);
+
+#if UNITY_WEBGL && !UNITY_EDITOR
+ // WebGL: ThreadPool 미사용, 동기 처리
+ OnProgress?.Invoke(0, totalBytes, 0f);
+ int result = lzma.doDecompress7zip(zipFilePath, decompressFolderPath, progress, true, true);
+ isComplete = true;
+ percent = 1f;
+ OnProgress?.Invoke(totalBytes, totalBytes, 1f);
+#else
CountPercent7ZipAsync(totalBytes, true).Forget();
int result = await UniTask.RunOnThreadPool(() =>
{
return lzma.doDecompress7zip(zipFilePath, decompressFolderPath, progress, true, true);
});
isComplete = true;
+#endif
+
if (result == 1) //success
{
return null;