From 9277bab6dd0ebefa9df9eefb6af1d1722c68d67e Mon Sep 17 00:00:00 2001 From: logonkhi Date: Mon, 10 Nov 2025 16:38:43 +0900 Subject: [PATCH] =?UTF-8?q?WebGL=20=EC=9A=A9=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/NuGet.config.meta | 28 ++++++- Assets/Scripts/UVC/Data/Core/DataArray.cs | 1 - Assets/Scripts/UVC/Data/Core/DataObject.cs | 28 ++++--- .../Scripts/UVC/Data/Http/HttpDataFetcher.cs | 10 ++- .../Scripts/UVC/Data/Mqtt/MqttDataReceiver.cs | 74 ++++++++++++++++++- Assets/Scripts/UVC/Data/UserSetting.cs | 45 ++++++++++- .../Factory/Playback/PlaybackRepository.cs | 11 ++- .../Factory/Playback/PlaybackSQLiteService.cs | 51 +++++++++++++ .../UVC/Factory/Playback/PlaybackService.cs | 55 +++++++++++++- Assets/Scripts/UVC/Log/ServerLog.cs | 73 ++++++++++++++++++ Assets/Scripts/UVC/Network/HttpRequester.cs | 33 ++++++++- Assets/Scripts/UVC/Network/MQTTService.cs | 35 +++++---- .../Scripts/UVC/UI/Window/HierarchyWindow.cs | 2 +- Assets/Scripts/UVC/util/Zipper.cs | 22 ++++++ 14 files changed, 433 insertions(+), 35 deletions(-) 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;