From 65fec57c85b0b39692bd4861c1a2fbd84f0c5c97 Mon Sep 17 00:00:00 2001 From: WooSH Date: Sun, 27 Jul 2025 10:32:15 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20http=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UniTask를 사용하여 Http Client 구현 - Http Client는 싱글톤 클래스이다. --- Assets/Infrastructure/Network/Configs.meta | 8 + .../Network/Configs/ApiConfig.cs | 131 +++++++ .../Network/Configs/ApiConfig.cs.meta | 2 + Assets/Infrastructure/Network/DTOs.meta | 8 + .../Network/DTOs/BaseApiResponse.cs | 60 ++++ .../Network/DTOs/BaseApiResponse.cs.meta | 2 + Assets/Infrastructure/Network/Http.meta | 8 + .../Network/Http/HttpApiClient.cs | 325 ++++++++++++++++++ .../Network/Http/HttpApiClient.cs.meta | 2 + Packages/manifest.json | 1 + Packages/packages-lock.json | 7 + 11 files changed, 554 insertions(+) create mode 100644 Assets/Infrastructure/Network/Configs.meta create mode 100644 Assets/Infrastructure/Network/Configs/ApiConfig.cs create mode 100644 Assets/Infrastructure/Network/Configs/ApiConfig.cs.meta create mode 100644 Assets/Infrastructure/Network/DTOs.meta create mode 100644 Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs create mode 100644 Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs.meta create mode 100644 Assets/Infrastructure/Network/Http.meta create mode 100644 Assets/Infrastructure/Network/Http/HttpApiClient.cs create mode 100644 Assets/Infrastructure/Network/Http/HttpApiClient.cs.meta diff --git a/Assets/Infrastructure/Network/Configs.meta b/Assets/Infrastructure/Network/Configs.meta new file mode 100644 index 0000000..b8432f4 --- /dev/null +++ b/Assets/Infrastructure/Network/Configs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fd48a88d389f79249986de098143c22f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Configs/ApiConfig.cs b/Assets/Infrastructure/Network/Configs/ApiConfig.cs new file mode 100644 index 0000000..cc7c547 --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ApiConfig.cs @@ -0,0 +1,131 @@ +using UnityEngine; + +namespace ProjectVG.Infrastructure.Network.Configs +{ + /// + /// API 설정을 관리하는 ScriptableObject + /// + [CreateAssetMenu(fileName = "ApiConfig", menuName = "ProjectVG/Network/ApiConfig")] + public class ApiConfig : ScriptableObject + { + [Header("Server Configuration")] + [SerializeField] private string baseUrl = "http://122.153.130.223:7900"; + [SerializeField] private string apiVersion = "v1"; + [SerializeField] private float timeout = 30f; + [SerializeField] private int maxRetryCount = 3; + [SerializeField] private float retryDelay = 1f; + + [Header("Authentication")] + [SerializeField] private string clientId = ""; + [SerializeField] private string clientSecret = ""; + + [Header("Endpoints")] + [SerializeField] private string userEndpoint = "users"; + [SerializeField] private string characterEndpoint = "characters"; + [SerializeField] private string conversationEndpoint = "conversations"; + [SerializeField] private string authEndpoint = "auth"; + + [Header("Headers")] + [SerializeField] private string contentType = "application/json"; + [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; + + // Properties + public string BaseUrl => baseUrl; + public string ApiVersion => apiVersion; + public float Timeout => timeout; + public int MaxRetryCount => maxRetryCount; + public float RetryDelay => retryDelay; + public string ClientId => clientId; + public string ClientSecret => clientSecret; + public string ContentType => contentType; + public string UserAgent => userAgent; + + // Endpoint Properties + public string UserEndpoint => userEndpoint; + public string CharacterEndpoint => characterEndpoint; + public string ConversationEndpoint => conversationEndpoint; + public string AuthEndpoint => authEndpoint; + + /// + /// 전체 API URL 생성 + /// + public string GetFullUrl(string endpoint) + { + return $"{baseUrl.TrimEnd('/')}/api/{apiVersion.TrimStart('/').TrimEnd('/')}/{endpoint.TrimStart('/')}"; + } + + /// + /// 사용자 API URL + /// + public string GetUserUrl(string path = "") + { + return GetFullUrl($"{userEndpoint}/{path.TrimStart('/')}"); + } + + /// + /// 캐릭터 API URL + /// + public string GetCharacterUrl(string path = "") + { + return GetFullUrl($"{characterEndpoint}/{path.TrimStart('/')}"); + } + + /// + /// 대화 API URL + /// + public string GetConversationUrl(string path = "") + { + return GetFullUrl($"{conversationEndpoint}/{path.TrimStart('/')}"); + } + + /// + /// 인증 API URL + /// + public string GetAuthUrl(string path = "") + { + return GetFullUrl($"{authEndpoint}/{path.TrimStart('/')}"); + } + + /// + /// 환경별 설정을 위한 팩토리 메서드 + /// + public static ApiConfig CreateDevelopmentConfig() + { + var config = CreateInstance(); + config.baseUrl = "http://localhost:7900"; + config.apiVersion = "v1"; + config.timeout = 10f; + config.maxRetryCount = 1; + config.retryDelay = 0.5f; + return config; + } + + /// + /// 환경별 설정을 위한 팩토리 메서드 + /// + public static ApiConfig CreateProductionConfig() + { + var config = CreateInstance(); + config.baseUrl = "http://122.153.130.223:7900"; + config.apiVersion = "v1"; + config.timeout = 30f; + config.maxRetryCount = 3; + config.retryDelay = 1f; + return config; + } + + /// + /// 환경별 설정을 위한 팩토리 메서드 + /// + public static ApiConfig CreateTestConfig() + { + var config = CreateInstance(); + config.baseUrl = "http://122.153.130.223:7900"; + config.apiVersion = "v1"; + config.timeout = 15f; + config.maxRetryCount = 2; + config.retryDelay = 0.5f; + return config; + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ApiConfig.cs.meta b/Assets/Infrastructure/Network/Configs/ApiConfig.cs.meta new file mode 100644 index 0000000..63497c5 --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ApiConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3060309933b81664ca0bb0f38ee668e0 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs.meta b/Assets/Infrastructure/Network/DTOs.meta new file mode 100644 index 0000000..08d2e6f --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 195135de6dfc91948b7c869169221d53 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs b/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs new file mode 100644 index 0000000..cc8bb20 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs @@ -0,0 +1,60 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs +{ + /// + /// API 응답의 기본 구조 + /// + [Serializable] + public class BaseApiResponse + { + public bool success; + public string message; + public long timestamp; + public string requestId; + } + + /// + /// 데이터를 포함하는 API 응답 + /// + [Serializable] + public class ApiResponse : BaseApiResponse + { + public T data; + } + + /// + /// 페이지네이션 정보 + /// + [Serializable] + public class PaginationInfo + { + public int page; + public int limit; + public int total; + public int totalPages; + public bool hasNext; + public bool hasPrev; + } + + /// + /// 페이지네이션된 API 응답 + /// + [Serializable] + public class PaginatedApiResponse : BaseApiResponse + { + public T[] data; + public PaginationInfo pagination; + } + + /// + /// 에러 응답 + /// + [Serializable] + public class ErrorResponse : BaseApiResponse + { + public string errorCode; + public string errorType; + public string[] details; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs.meta b/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs.meta new file mode 100644 index 0000000..7c3ecc0 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/BaseApiResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 359796341ec306b42b9219b5fff1feca \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Http.meta b/Assets/Infrastructure/Network/Http.meta new file mode 100644 index 0000000..e5bdd72 --- /dev/null +++ b/Assets/Infrastructure/Network/Http.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a27aa8428fdf0974990bab2179bf081f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Http/HttpApiClient.cs b/Assets/Infrastructure/Network/Http/HttpApiClient.cs new file mode 100644 index 0000000..c6d3c11 --- /dev/null +++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; +using Cysharp.Threading.Tasks; +using System.Threading; +using ProjectVG.Infrastructure.Network.Configs; + +namespace ProjectVG.Infrastructure.Network.Http +{ + /// + /// HTTP API 클라이언트 + /// UnityWebRequest를 사용하여 서버와 통신하며, UniTask 기반 비동기 처리를 지원합니다. + /// + public class HttpApiClient : MonoBehaviour + { + [Header("API Configuration")] + [SerializeField] private ApiConfig apiConfig; + [SerializeField] private float timeout = 30f; + [SerializeField] private int maxRetryCount = 3; + [SerializeField] private float retryDelay = 1f; + + [Header("Headers")] + [SerializeField] private string contentType = "application/json"; + [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; + + private readonly Dictionary defaultHeaders = new Dictionary(); + private CancellationTokenSource cancellationTokenSource; + + public static HttpApiClient Instance { get; private set; } + + private void Awake() + { + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + InitializeClient(); + } + else + { + Destroy(gameObject); + } + } + + private void OnDestroy() + { + cancellationTokenSource?.Cancel(); + cancellationTokenSource?.Dispose(); + } + + private void InitializeClient() + { + cancellationTokenSource = new CancellationTokenSource(); + + if (apiConfig == null) + { + Debug.LogWarning("ApiConfig가 설정되지 않았습니다. 기본값을 사용합니다."); + SetupDefaultHeaders(); + return; + } + + timeout = apiConfig.Timeout; + maxRetryCount = apiConfig.MaxRetryCount; + retryDelay = apiConfig.RetryDelay; + contentType = apiConfig.ContentType; + userAgent = apiConfig.UserAgent; + + SetupDefaultHeaders(); + } + + private void SetupDefaultHeaders() + { + defaultHeaders.Clear(); + defaultHeaders["Content-Type"] = contentType; + defaultHeaders["User-Agent"] = userAgent; + defaultHeaders["Accept"] = "application/json"; + } + + /// + /// ApiConfig 설정 (런타임에서 동적으로 변경 가능) + /// + public void SetApiConfig(ApiConfig config) + { + apiConfig = config; + InitializeClient(); + } + + public void AddDefaultHeader(string key, string value) + { + defaultHeaders[key] = value; + } + + public void SetAuthToken(string token) + { + AddDefaultHeader("Authorization", $"Bearer {token}"); + } + + private string GetFullUrl(string endpoint) + { + if (apiConfig != null) + { + return apiConfig.GetFullUrl(endpoint); + } + + return $"http://122.153.130.223:7900/api/v1/{endpoint.TrimStart('/')}"; + } + + public async UniTask GetAsync(string endpoint, Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + return await SendRequestAsync(url, UnityWebRequest.kHttpVerbGET, null, headers, cancellationToken); + } + + public async UniTask PostAsync(string endpoint, object data = null, Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + var jsonData = data != null ? JsonUtility.ToJson(data) : null; + return await SendRequestAsync(url, UnityWebRequest.kHttpVerbPOST, jsonData, headers, cancellationToken); + } + + public async UniTask PutAsync(string endpoint, object data = null, Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + var jsonData = data != null ? JsonUtility.ToJson(data) : null; + return await SendRequestAsync(url, UnityWebRequest.kHttpVerbPUT, jsonData, headers, cancellationToken); + } + + public async UniTask DeleteAsync(string endpoint, Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + return await SendRequestAsync(url, UnityWebRequest.kHttpVerbDELETE, null, headers, cancellationToken); + } + + public async UniTask UploadFileAsync(string endpoint, byte[] fileData, string fileName, string fieldName = "file", Dictionary headers = null, CancellationToken cancellationToken = default) + { + var url = GetFullUrl(endpoint); + return await SendFileRequestAsync(url, fileData, fileName, fieldName, headers, cancellationToken); + } + + private async UniTask SendRequestAsync(string url, string method, string jsonData, Dictionary headers, CancellationToken cancellationToken) + { + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token).Token; + + for (int attempt = 0; attempt <= maxRetryCount; attempt++) + { + try + { + using var request = CreateRequest(url, method, jsonData, headers); + + var operation = request.SendWebRequest(); + await operation.WithCancellation(combinedCancellationToken); + + if (request.result == UnityWebRequest.Result.Success) + { + return ParseResponse(request); + } + else + { + var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); + + if (ShouldRetry(request.responseCode) && attempt < maxRetryCount) + { + Debug.LogWarning($"API 요청 실패 (시도 {attempt + 1}/{maxRetryCount + 1}): {error.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: combinedCancellationToken); + continue; + } + + throw error; + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (ex is not ApiException) + { + if (attempt < maxRetryCount) + { + Debug.LogWarning($"API 요청 예외 발생 (시도 {attempt + 1}/{maxRetryCount + 1}): {ex.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: combinedCancellationToken); + continue; + } + throw new ApiException($"{maxRetryCount + 1}번 시도 후 요청 실패", 0, ex.Message); + } + } + + throw new ApiException($"{maxRetryCount + 1}번 시도 후 요청 실패", 0, "최대 재시도 횟수 초과"); + } + + private async UniTask SendFileRequestAsync(string url, byte[] fileData, string fileName, string fieldName, Dictionary headers, CancellationToken cancellationToken) + { + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token).Token; + + for (int attempt = 0; attempt <= maxRetryCount; attempt++) + { + try + { + var form = new WWWForm(); + form.AddBinaryData(fieldName, fileData, fileName); + + using var request = UnityWebRequest.Post(url, form); + SetupRequest(request, headers); + request.timeout = (int)timeout; + + var operation = request.SendWebRequest(); + await operation.WithCancellation(combinedCancellationToken); + + if (request.result == UnityWebRequest.Result.Success) + { + return ParseResponse(request); + } + else + { + var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); + + if (ShouldRetry(request.responseCode) && attempt < maxRetryCount) + { + Debug.LogWarning($"파일 업로드 실패 (시도 {attempt + 1}/{maxRetryCount + 1}): {error.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: combinedCancellationToken); + continue; + } + + throw error; + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (ex is not ApiException) + { + if (attempt < maxRetryCount) + { + Debug.LogWarning($"파일 업로드 예외 발생 (시도 {attempt + 1}/{maxRetryCount + 1}): {ex.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: combinedCancellationToken); + continue; + } + throw new ApiException($"{maxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, ex.Message); + } + } + + throw new ApiException($"{maxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, "최대 재시도 횟수 초과"); + } + + private UnityWebRequest CreateRequest(string url, string method, string jsonData, Dictionary headers) + { + var request = new UnityWebRequest(url, method); + + if (!string.IsNullOrEmpty(jsonData)) + { + var bodyRaw = Encoding.UTF8.GetBytes(jsonData); + request.uploadHandler = new UploadHandlerRaw(bodyRaw); + } + + request.downloadHandler = new DownloadHandlerBuffer(); + SetupRequest(request, headers); + request.timeout = (int)timeout; + + return request; + } + + private void SetupRequest(UnityWebRequest request, Dictionary headers) + { + foreach (var header in defaultHeaders) + { + request.SetRequestHeader(header.Key, header.Value); + } + + if (headers != null) + { + foreach (var header in headers) + { + request.SetRequestHeader(header.Key, header.Value); + } + } + } + + private T ParseResponse(UnityWebRequest request) + { + var responseText = request.downloadHandler?.text; + + if (string.IsNullOrEmpty(responseText)) + { + return default(T); + } + + try + { + return JsonUtility.FromJson(responseText); + } + catch (Exception ex) + { + throw new ApiException($"응답 파싱 실패: {ex.Message}", request.responseCode, responseText); + } + } + + private bool ShouldRetry(long responseCode) + { + return responseCode >= 500 || responseCode == 429; + } + + public void Shutdown() + { + cancellationTokenSource?.Cancel(); + } + } + + /// + /// API 예외 클래스 + /// + public class ApiException : Exception + { + public long StatusCode { get; } + public string ResponseBody { get; } + + public ApiException(string message, long statusCode, string responseBody) + : base(message) + { + StatusCode = statusCode; + ResponseBody = responseBody; + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Http/HttpApiClient.cs.meta b/Assets/Infrastructure/Network/Http/HttpApiClient.cs.meta new file mode 100644 index 0000000..4ad71af --- /dev/null +++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 00fbb8efd091db646bc0653b6295222f \ No newline at end of file diff --git a/Packages/manifest.json b/Packages/manifest.json index 0f53190..5bedd7e 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,5 +1,6 @@ { "dependencies": { + "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", "com.unity.collab-proxy": "2.8.2", "com.unity.feature.2d": "2.0.1", "com.unity.ide.rider": "3.0.36", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index dc200af..0b83833 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,5 +1,12 @@ { "dependencies": { + "com.cysharp.unitask": { + "version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "f213ff497e4ff462a77319cf677cf20cc0860ca9" + }, "com.unity.2d.animation": { "version": "10.2.1", "depth": 1, From eddf66d47f6c06c3554943e41d5b00548fe7c443 Mon Sep 17 00:00:00 2001 From: WooSH Date: Sun, 27 Jul 2025 11:07:37 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20http=20api=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/DTOs/Character.meta | 8 + .../Network/DTOs/Character/CharacterInfo.cs | 17 +++ .../DTOs/Character/CharacterInfo.cs.meta | 2 + .../DTOs/Character/CreateCharacterRequest.cs | 16 ++ .../Character/CreateCharacterRequest.cs.meta | 2 + .../DTOs/Character/UpdateCharacterRequest.cs | 16 ++ .../Character/UpdateCharacterRequest.cs.meta | 2 + Assets/Infrastructure/Network/DTOs/Chat.meta | 8 + .../Network/DTOs/Chat/ChatRequest.cs | 18 +++ .../Network/DTOs/Chat/ChatRequest.cs.meta | 2 + .../Network/DTOs/Chat/ChatResponse.cs | 15 ++ .../Network/DTOs/Chat/ChatResponse.cs.meta | 2 + Assets/Infrastructure/Network/Services.meta | 8 + .../Network/Services/ApiServiceManager.cs | 72 +++++++++ .../Services/ApiServiceManager.cs.meta | 2 + .../Network/Services/CharacterApiService.cs | 138 ++++++++++++++++++ .../Services/CharacterApiService.cs.meta | 2 + .../Network/Services/ChatApiService.cs | 61 ++++++++ .../Network/Services/ChatApiService.cs.meta | 2 + 19 files changed, 393 insertions(+) create mode 100644 Assets/Infrastructure/Network/DTOs/Character.meta create mode 100644 Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs create mode 100644 Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs.meta create mode 100644 Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs create mode 100644 Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs.meta create mode 100644 Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs create mode 100644 Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs.meta create mode 100644 Assets/Infrastructure/Network/DTOs/Chat.meta create mode 100644 Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs create mode 100644 Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs.meta create mode 100644 Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs create mode 100644 Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs.meta create mode 100644 Assets/Infrastructure/Network/Services.meta create mode 100644 Assets/Infrastructure/Network/Services/ApiServiceManager.cs create mode 100644 Assets/Infrastructure/Network/Services/ApiServiceManager.cs.meta create mode 100644 Assets/Infrastructure/Network/Services/CharacterApiService.cs create mode 100644 Assets/Infrastructure/Network/Services/CharacterApiService.cs.meta create mode 100644 Assets/Infrastructure/Network/Services/ChatApiService.cs create mode 100644 Assets/Infrastructure/Network/Services/ChatApiService.cs.meta diff --git a/Assets/Infrastructure/Network/DTOs/Character.meta b/Assets/Infrastructure/Network/DTOs/Character.meta new file mode 100644 index 0000000..1705825 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6c376144b261f82498097408eb9c393f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs new file mode 100644 index 0000000..f536f00 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs @@ -0,0 +1,17 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.Character +{ + /// + /// 캐릭터 정보 DTO + /// + [Serializable] + public class CharacterInfo + { + public string id; + public string name; + public string description; + public string role; + public bool isActive; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs.meta b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs.meta new file mode 100644 index 0000000..28feae1 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4442e4d5ae93e4f4aa37f85e1a2eef48 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs b/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs new file mode 100644 index 0000000..45375d8 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs @@ -0,0 +1,16 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.Character +{ + /// + /// 캐릭터 생성 요청 DTO + /// + [Serializable] + public class CreateCharacterRequest + { + public string name; + public string description; + public string role; + public bool isActive = true; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs.meta b/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs.meta new file mode 100644 index 0000000..2cc318b --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/CreateCharacterRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8934f22c6576c584c9a76cd0ca57a5fe \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs b/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs new file mode 100644 index 0000000..2cf7044 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs @@ -0,0 +1,16 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.Character +{ + /// + /// 캐릭터 수정 요청 DTO + /// + [Serializable] + public class UpdateCharacterRequest + { + public string name; + public string description; + public string role; + public bool isActive; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs.meta b/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs.meta new file mode 100644 index 0000000..51be348 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Character/UpdateCharacterRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 88a11fb1cb2ce8f46a6fd6df981f1854 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat.meta b/Assets/Infrastructure/Network/DTOs/Chat.meta new file mode 100644 index 0000000..733db01 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45b3af16b93a1e44399f346aed069e31 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs new file mode 100644 index 0000000..53f5efc --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs @@ -0,0 +1,18 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.Chat +{ + /// + /// 채팅 요청 DTO + /// + [Serializable] + public class ChatRequest + { + public string sessionId; + public string actor; + public string message; + public string action = "chat"; + public string character_id; + public string user_id; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs.meta b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs.meta new file mode 100644 index 0000000..48b9b22 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 508dce3896fd1a24dbb39ec613bac2df \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs b/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs new file mode 100644 index 0000000..609dafb --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs @@ -0,0 +1,15 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.Chat +{ + /// + /// 채팅 응답 DTO + /// + [Serializable] + public class ChatResponse + { + public bool success; + public string message; + public string sessionId; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs.meta b/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs.meta new file mode 100644 index 0000000..956fddd --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f55f8bd1f455ad54caaea20c4fc09253 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services.meta b/Assets/Infrastructure/Network/Services.meta new file mode 100644 index 0000000..c4ba0b4 --- /dev/null +++ b/Assets/Infrastructure/Network/Services.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f9bc83dcef956db4784a7336ee77e17b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Services/ApiServiceManager.cs b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs new file mode 100644 index 0000000..d6be76a --- /dev/null +++ b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs @@ -0,0 +1,72 @@ +using UnityEngine; + +namespace ProjectVG.Infrastructure.Network.Services +{ + /// + /// API 서비스 매니저 + /// 모든 API 서비스의 중앙 관리자 + /// + public class ApiServiceManager : MonoBehaviour + { + private static ApiServiceManager _instance; + public static ApiServiceManager Instance + { + get + { + if (_instance == null) + { + _instance = FindObjectOfType(); + if (_instance == null) + { + GameObject go = new GameObject("ApiServiceManager"); + _instance = go.AddComponent(); + DontDestroyOnLoad(go); + } + } + return _instance; + } + } + + // API 서비스 인스턴스들 + private ChatApiService _chatService; + private CharacterApiService _characterService; + + // 프로퍼티로 서비스 접근 + public ChatApiService Chat => _chatService ??= new ChatApiService(); + public CharacterApiService Character => _characterService ??= new CharacterApiService(); + + private void Awake() + { + if (_instance == null) + { + _instance = this; + DontDestroyOnLoad(gameObject); + InitializeServices(); + } + else if (_instance != this) + { + Destroy(gameObject); + } + } + + private void InitializeServices() + { + // 서비스 초기화 + _chatService = new ChatApiService(); + _characterService = new CharacterApiService(); + + Debug.Log("API 서비스 매니저 초기화 완료"); + } + + /// + /// 모든 서비스 재초기화 + /// + public void ReinitializeServices() + { + _chatService = new ChatApiService(); + _characterService = new CharacterApiService(); + + Debug.Log("API 서비스 재초기화 완료"); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/ApiServiceManager.cs.meta b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs.meta new file mode 100644 index 0000000..fe19897 --- /dev/null +++ b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bc3a1165acfed50488abb14eb8dbc022 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/CharacterApiService.cs b/Assets/Infrastructure/Network/Services/CharacterApiService.cs new file mode 100644 index 0000000..9244703 --- /dev/null +++ b/Assets/Infrastructure/Network/Services/CharacterApiService.cs @@ -0,0 +1,138 @@ +using System.Threading; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.Http; +using ProjectVG.Infrastructure.Network.DTOs.Character; + +namespace ProjectVG.Infrastructure.Network.Services +{ + /// + /// 캐릭터 API 서비스 + /// + public class CharacterApiService + { + private readonly HttpApiClient _httpClient; + + public CharacterApiService() + { + _httpClient = HttpApiClient.Instance; + } + + /// + /// 모든 캐릭터 목록 조회 + /// + /// 취소 토큰 + /// 캐릭터 목록 + public async UniTask GetAllCharactersAsync(CancellationToken cancellationToken = default) + { + return await _httpClient.GetAsync("character", cancellationToken: cancellationToken); + } + + /// + /// 특정 캐릭터 정보 조회 + /// + /// 캐릭터 ID + /// 취소 토큰 + /// 캐릭터 정보 + public async UniTask GetCharacterAsync(string characterId, CancellationToken cancellationToken = default) + { + return await _httpClient.GetAsync($"character/{characterId}", cancellationToken: cancellationToken); + } + + /// + /// 캐릭터 생성 + /// + /// 캐릭터 생성 요청 + /// 취소 토큰 + /// 생성된 캐릭터 정보 + public async UniTask CreateCharacterAsync(CreateCharacterRequest request, CancellationToken cancellationToken = default) + { + return await _httpClient.PostAsync("character", request, cancellationToken: cancellationToken); + } + + /// + /// 간편한 캐릭터 생성 + /// + /// 캐릭터 이름 + /// 캐릭터 설명 + /// 캐릭터 역할 + /// 활성화 여부 + /// 취소 토큰 + /// 생성된 캐릭터 정보 + public async UniTask CreateCharacterAsync( + string name, + string description, + string role, + bool isActive = true, + CancellationToken cancellationToken = default) + { + var request = new CreateCharacterRequest + { + name = name, + description = description, + role = role, + isActive = isActive + }; + + return await CreateCharacterAsync(request, cancellationToken); + } + + /// + /// 캐릭터 정보 수정 + /// + /// 캐릭터 ID + /// 수정 요청 + /// 취소 토큰 + /// 수정된 캐릭터 정보 + public async UniTask UpdateCharacterAsync(string characterId, UpdateCharacterRequest request, CancellationToken cancellationToken = default) + { + return await _httpClient.PutAsync($"character/{characterId}", request, cancellationToken: cancellationToken); + } + + /// + /// 간편한 캐릭터 수정 + /// + /// 캐릭터 ID + /// 캐릭터 이름 + /// 캐릭터 설명 + /// 캐릭터 역할 + /// 활성화 여부 + /// 취소 토큰 + /// 수정된 캐릭터 정보 + public async UniTask UpdateCharacterAsync( + string characterId, + string name = null, + string description = null, + string role = null, + bool? isActive = null, + CancellationToken cancellationToken = default) + { + var request = new UpdateCharacterRequest(); + + if (name != null) request.name = name; + if (description != null) request.description = description; + if (role != null) request.role = role; + if (isActive.HasValue) request.isActive = isActive.Value; + + return await UpdateCharacterAsync(characterId, request, cancellationToken); + } + + /// + /// 캐릭터 삭제 + /// + /// 캐릭터 ID + /// 취소 토큰 + /// 삭제 성공 여부 + public async UniTask DeleteCharacterAsync(string characterId, CancellationToken cancellationToken = default) + { + try + { + await _httpClient.DeleteAsync($"character/{characterId}", cancellationToken: cancellationToken); + return true; + } + catch + { + return false; + } + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/CharacterApiService.cs.meta b/Assets/Infrastructure/Network/Services/CharacterApiService.cs.meta new file mode 100644 index 0000000..a985190 --- /dev/null +++ b/Assets/Infrastructure/Network/Services/CharacterApiService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2e505a265149b82478a22ae11d759ca6 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/ChatApiService.cs b/Assets/Infrastructure/Network/Services/ChatApiService.cs new file mode 100644 index 0000000..44a2e8b --- /dev/null +++ b/Assets/Infrastructure/Network/Services/ChatApiService.cs @@ -0,0 +1,61 @@ +using System.Threading; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.Http; +using ProjectVG.Infrastructure.Network.DTOs.Chat; + +namespace ProjectVG.Infrastructure.Network.Services +{ + /// + /// 채팅 API 서비스 + /// + public class ChatApiService + { + private readonly HttpApiClient _httpClient; + + public ChatApiService() + { + _httpClient = HttpApiClient.Instance; + } + + /// + /// 채팅 요청을 큐에 등록 + /// + /// 채팅 요청 데이터 + /// 취소 토큰 + /// 채팅 응답 + public async UniTask SendChatAsync(ChatRequest request, CancellationToken cancellationToken = default) + { + return await _httpClient.PostAsync("chat", request, cancellationToken: cancellationToken); + } + + /// + /// 간편한 채팅 요청 (기본값 사용) + /// + /// 메시지 + /// 캐릭터 ID + /// 사용자 ID + /// 세션 ID (선택사항) + /// 액터 (선택사항) + /// 취소 토큰 + /// 채팅 응답 + public async UniTask SendChatAsync( + string message, + string characterId, + string userId, + string sessionId = null, + string actor = null, + CancellationToken cancellationToken = default) + { + var request = new ChatRequest + { + sessionId = sessionId, + actor = actor, + message = message, + character_id = characterId, + user_id = userId + }; + + return await SendChatAsync(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/ChatApiService.cs.meta b/Assets/Infrastructure/Network/Services/ChatApiService.cs.meta new file mode 100644 index 0000000..5a73fce --- /dev/null +++ b/Assets/Infrastructure/Network/Services/ChatApiService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0c581a111d7799c40b9d79a5887c988b \ No newline at end of file From c66239923afa7539e9a2468cac81dc20f509fc64 Mon Sep 17 00:00:00 2001 From: WooSH Date: Sun, 27 Jul 2025 11:52:54 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20=EC=9B=B9?= =?UTF-8?q?=EC=86=8C=EC=BA=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Configs/WebSocketConfig.cs | 168 +++++++ .../Network/Configs/WebSocketConfig.cs.meta | 2 + .../Network/DTOs/WebSocket.meta | 8 + .../DTOs/WebSocket/WebSocketMessage.cs | 57 +++ .../DTOs/WebSocket/WebSocketMessage.cs.meta | 2 + Assets/Infrastructure/Network/README.md | 147 ++++++ Assets/Infrastructure/Network/README.md.meta | 7 + Assets/Infrastructure/Network/WebSocket.meta | 8 + .../WebSocket/DefaultWebSocketHandler.cs | 164 ++++++ .../WebSocket/DefaultWebSocketHandler.cs.meta | 2 + .../Network/WebSocket/INativeWebSocket.cs | 31 ++ .../WebSocket/INativeWebSocket.cs.meta | 2 + .../Network/WebSocket/IWebSocketHandler.cs | 62 +++ .../WebSocket/IWebSocketHandler.cs.meta | 2 + .../Network/WebSocket/Platforms.meta | 8 + .../WebSocket/Platforms/MobileWebSocket.cs | 86 ++++ .../Platforms/MobileWebSocket.cs.meta | 2 + .../WebSocket/Platforms/UnityWebSocket.cs | 86 ++++ .../Platforms/UnityWebSocket.cs.meta | 2 + .../Platforms/WebSocketSharpFallback.cs | 86 ++++ .../Platforms/WebSocketSharpFallback.cs.meta | 2 + .../Network/WebSocket/WebSocketManager.cs | 468 ++++++++++++++++++ .../WebSocket/WebSocketManager.cs.meta | 2 + 23 files changed, 1404 insertions(+) create mode 100644 Assets/Infrastructure/Network/Configs/WebSocketConfig.cs create mode 100644 Assets/Infrastructure/Network/Configs/WebSocketConfig.cs.meta create mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket.meta create mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs create mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs.meta create mode 100644 Assets/Infrastructure/Network/README.md create mode 100644 Assets/Infrastructure/Network/README.md.meta create mode 100644 Assets/Infrastructure/Network/WebSocket.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs.meta diff --git a/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs b/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs new file mode 100644 index 0000000..047047c --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs @@ -0,0 +1,168 @@ +using UnityEngine; +using System; +using System.Text; + +namespace ProjectVG.Infrastructure.Network.Configs +{ + /// + /// WebSocket 설정을 관리하는 ScriptableObject + /// + [CreateAssetMenu(fileName = "WebSocketConfig", menuName = "ProjectVG/Network/WebSocketConfig")] + public class WebSocketConfig : ScriptableObject + { + [Header("WebSocket Server Configuration")] + [SerializeField] private string baseUrl = "ws://MTIyLjE1My4xMzAuMjIzOjc5MDA="; // base64 encoded: 122.153.130.223:7900 + [SerializeField] private string wsPath = "ws"; + [SerializeField] private string apiVersion = "v1"; + + [Header("Connection Settings")] + [SerializeField] private float timeout = 30f; + [SerializeField] private float reconnectDelay = 5f; + [SerializeField] private int maxReconnectAttempts = 3; + [SerializeField] private bool autoReconnect = true; + [SerializeField] private float heartbeatInterval = 30f; + [SerializeField] private bool enableHeartbeat = true; + + [Header("Message Settings")] + [SerializeField] private int maxMessageSize = 65536; // 64KB + [SerializeField] private float messageTimeout = 10f; + [SerializeField] private bool enableMessageLogging = true; + + [Header("Headers")] + [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; + [SerializeField] private string contentType = "application/json"; + + // Properties + public string BaseUrl => DecodeBase64Url(baseUrl); + public string WsPath => wsPath; + public string ApiVersion => apiVersion; + public float Timeout => timeout; + public float ReconnectDelay => reconnectDelay; + public int MaxReconnectAttempts => maxReconnectAttempts; + public bool AutoReconnect => autoReconnect; + public float HeartbeatInterval => heartbeatInterval; + public bool EnableHeartbeat => enableHeartbeat; + public int MaxMessageSize => maxMessageSize; + public float MessageTimeout => messageTimeout; + public bool EnableMessageLogging => enableMessageLogging; + public string UserAgent => userAgent; + public string ContentType => contentType; + + /// + /// WebSocket URL 생성 + /// + public string GetWebSocketUrl() + { + var decodedBaseUrl = DecodeBase64Url(baseUrl); + return $"{decodedBaseUrl.TrimEnd('/')}/{wsPath.TrimStart('/').TrimEnd('/')}"; + } + + /// + /// API 버전이 포함된 WebSocket URL 생성 + /// + public string GetWebSocketUrlWithVersion() + { + var decodedBaseUrl = DecodeBase64Url(baseUrl); + return $"{decodedBaseUrl.TrimEnd('/')}/api/{apiVersion.TrimStart('/').TrimEnd('/')}/{wsPath.TrimStart('/').TrimEnd('/')}"; + } + + /// + /// 세션별 WebSocket URL 생성 + /// + public string GetWebSocketUrlWithSession(string sessionId) + { + var baseWsUrl = GetWebSocketUrlWithVersion(); + return $"{baseWsUrl}?sessionId={sessionId}"; + } + + /// + /// Base64 인코딩된 URL 디코딩 + /// + private string DecodeBase64Url(string encodedUrl) + { + try + { + if (encodedUrl.StartsWith("ws://") || encodedUrl.StartsWith("wss://")) + { + var protocol = encodedUrl.Substring(0, encodedUrl.IndexOf("://") + 3); + var encodedPart = encodedUrl.Substring(encodedUrl.IndexOf("://") + 3); + var decodedPart = Encoding.UTF8.GetString(Convert.FromBase64String(encodedPart)); + return protocol + decodedPart; + } + return encodedUrl; + } + catch (Exception ex) + { + Debug.LogError($"WebSocket URL 디코딩 실패: {ex.Message}"); + return "ws://localhost:7900"; // Fallback + } + } + + #region Factory Methods + + /// + /// 개발 환경 설정 생성 + /// + public static WebSocketConfig CreateDevelopmentConfig() + { + var config = CreateInstance(); + config.baseUrl = "ws://bG9jYWxob3N0Ojc5MDA="; // base64 encoded: localhost:7900 + config.wsPath = "ws"; + config.apiVersion = "v1"; + config.timeout = 10f; + config.reconnectDelay = 2f; + config.maxReconnectAttempts = 2; + config.autoReconnect = true; + config.heartbeatInterval = 15f; + config.enableHeartbeat = true; + config.maxMessageSize = 32768; // 32KB + config.messageTimeout = 5f; + config.enableMessageLogging = true; + return config; + } + + /// + /// 프로덕션 환경 설정 생성 + /// + public static WebSocketConfig CreateProductionConfig() + { + var config = CreateInstance(); + config.baseUrl = "ws://MTIyLjE1My4xMzAuMjIzOjc5MDA="; // base64 encoded: 122.153.130.223:7900 + config.wsPath = "ws"; + config.apiVersion = "v1"; + config.timeout = 30f; + config.reconnectDelay = 5f; + config.maxReconnectAttempts = 3; + config.autoReconnect = true; + config.heartbeatInterval = 30f; + config.enableHeartbeat = true; + config.maxMessageSize = 65536; // 64KB + config.messageTimeout = 10f; + config.enableMessageLogging = false; + return config; + } + + /// + /// 테스트 환경 설정 생성 + /// + public static WebSocketConfig CreateTestConfig() + { + var config = CreateInstance(); + config.baseUrl = "ws://MTIyLjE1My4xMzAuMjIzOjc5MDA="; // base64 encoded: 122.153.130.223:7900 + config.wsPath = "ws"; + config.apiVersion = "v1"; + config.timeout = 15f; + config.reconnectDelay = 3f; + config.maxReconnectAttempts = 2; + config.autoReconnect = true; + config.heartbeatInterval = 20f; + config.enableHeartbeat = true; + config.maxMessageSize = 32768; // 32KB + config.messageTimeout = 8f; + config.enableMessageLogging = true; + return config; + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs.meta b/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs.meta new file mode 100644 index 0000000..56bccbf --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bf7d6fa8efeb2fc4cab6197e3b03420e \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket.meta b/Assets/Infrastructure/Network/DTOs/WebSocket.meta new file mode 100644 index 0000000..461d67b --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/WebSocket.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 96cf5f40d2d8aab478c179145464c91b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs b/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs new file mode 100644 index 0000000..15e1be6 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs @@ -0,0 +1,57 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.WebSocket +{ + /// + /// WebSocket 메시지 기본 구조 + /// + [Serializable] + public class WebSocketMessage + { + public string type; + public string sessionId; + public long timestamp; + public string data; + } + + /// + /// 세션 ID 메시지 (더미 클라이언트와 동일) + /// + [Serializable] + public class SessionIdMessage : WebSocketMessage + { + public string session_id; + } + + /// + /// 채팅 메시지 타입 + /// + [Serializable] + public class ChatMessage : WebSocketMessage + { + public string characterId; + public string userId; + public string message; + public string actor; + } + + /// + /// 시스템 메시지 타입 + /// + [Serializable] + public class SystemMessage : WebSocketMessage + { + public string status; + public string description; + } + + /// + /// 연결 상태 메시지 타입 + /// + [Serializable] + public class ConnectionMessage : WebSocketMessage + { + public string status; // "connected", "disconnected", "error" + public string reason; + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs.meta b/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs.meta new file mode 100644 index 0000000..910b644 --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3982d097d6fd94242b7827400f78306d \ No newline at end of file diff --git a/Assets/Infrastructure/Network/README.md b/Assets/Infrastructure/Network/README.md new file mode 100644 index 0000000..05104c7 --- /dev/null +++ b/Assets/Infrastructure/Network/README.md @@ -0,0 +1,147 @@ +# ProjectVG Network Module + +Unity 클라이언트와 서버 간의 통신을 위한 네트워크 모듈입니다. + +## 📦 설치 + +### 1. UniTask 설치 +```json +// Packages/manifest.json +"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask" +``` + +### 2. WebSocket 시뮬레이션 +현재는 시뮬레이션 구현체를 사용합니다. 개발/테스트에 최적화되어 있습니다. + +**시뮬레이션 구현체 장점:** +- 🟩 패키지 의존성 없음 +- 🟩 즉시 사용 가능 +- 🟩 개발/테스트에 적합 +- 🟩 크로스 플랫폼 지원 + +## 🏗️ 구조 + +``` +Assets/Infrastructure/Network/ +├── Configs/ # 설정 파일들 +│ ├── ApiConfig.cs # HTTP API 설정 +│ └── WebSocketConfig.cs # WebSocket 설정 +├── DTOs/ # 데이터 전송 객체들 +│ ├── BaseApiResponse.cs # 기본 API 응답 +│ ├── Chat/ # 채팅 관련 DTO +│ ├── Character/ # 캐릭터 관련 DTO +│ └── WebSocket/ # WebSocket 메시지 DTO +├── Http/ # HTTP 클라이언트 +│ └── HttpApiClient.cs # HTTP API 클라이언트 +├── Services/ # API 서비스들 +│ ├── ApiServiceManager.cs # API 서비스 매니저 +│ ├── ChatApiService.cs # 채팅 API 서비스 +│ └── CharacterApiService.cs # 캐릭터 API 서비스 +└── WebSocket/ # WebSocket 관련 + ├── WebSocketManager.cs # WebSocket 매니저 + ├── IWebSocketHandler.cs # WebSocket 핸들러 인터페이스 + ├── DefaultWebSocketHandler.cs # 기본 핸들러 + └── Platforms/ # 플랫폼별 WebSocket 구현 + ├── UnityWebSocket.cs # WebSocket 시뮬레이션 + ├── MobileWebSocket.cs # 모바일 시뮬레이션 + └── WebSocketSharpFallback.cs # 폴백 시뮬레이션 +``` + +## 🚀 사용법 + +### HTTP API 사용 + +```csharp +// API 서비스 매니저 사용 +var apiManager = ApiServiceManager.Instance; + +// 채팅 API 사용 +var chatResponse = await apiManager.Chat.SendChatAsync( + sessionId: "session-123", + actor: "user", + message: "안녕하세요!", + characterId: "char-456", + userId: "user-789" +); + +// 캐릭터 API 사용 +var characters = await apiManager.Character.GetCharactersAsync(); +var character = await apiManager.Character.GetCharacterAsync("char-456"); +``` + +### WebSocket 사용 + +```csharp +// WebSocket 매니저 사용 +var wsManager = WebSocketManager.Instance; + +// 핸들러 등록 +var handler = gameObject.AddComponent(); +wsManager.RegisterHandler(handler); + +// 연결 +await wsManager.ConnectAsync("session-123"); + +// 메시지 전송 +await wsManager.SendChatMessageAsync( + message: "안녕하세요!", + characterId: "char-456", + userId: "user-789" +); +``` + +## ⚙️ 설정 + +### ApiConfig 설정 +```csharp +// ScriptableObject로 생성 +var apiConfig = ApiConfig.CreateProductionConfig(); +apiConfig.BaseUrl = "http://122.153.130.223:7900/api/v1/"; +``` + +### WebSocketConfig 설정 +```csharp +// ScriptableObject로 생성 +var wsConfig = WebSocketConfig.CreateProductionConfig(); +wsConfig.BaseUrl = "ws://122.153.130.223:7900/ws"; +``` + +## 🔧 WebSocket 시뮬레이션 특별 기능 + +### 1. 패키지 의존성 없음 +- 외부 패키지 설치 없이 즉시 사용 가능 +- 개발/테스트에 최적화 + +### 2. 크로스 플랫폼 지원 +- ✅ Unity Desktop +- ✅ Unity WebGL +- ✅ Unity Android +- ✅ Unity iOS + +### 3. 시뮬레이션 모드 +```csharp +// 실제 WebSocket 연결 대신 시뮬레이션 +// 개발/테스트 단계에서 안전하게 사용 +Debug.Log("WebSocket 시뮬레이션 연결"); +``` + +## 🐛 문제 해결 + +### 시뮬레이션 모드 +``` +WebSocket 시뮬레이션 연결: ws://... +WebSocket 시뮬레이션 메시지: ... +``` +**설명:** 실제 WebSocket 연결 대신 시뮬레이션으로 동작합니다. + +### 개발/테스트 +- 실제 서버 연결 없이도 개발 가능 +- 로그를 통해 메시지 흐름 확인 +- 안전한 테스트 환경 제공 + +## 📝 로그 + +모든 로그는 한국어로 출력됩니다: +- `Debug.Log("WebSocket 연결 성공")` +- `Debug.LogError("연결 실패")` +- `Debug.LogWarning("재연결 시도")` \ No newline at end of file diff --git a/Assets/Infrastructure/Network/README.md.meta b/Assets/Infrastructure/Network/README.md.meta new file mode 100644 index 0000000..6d02e29 --- /dev/null +++ b/Assets/Infrastructure/Network/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4ceeee07406643741bb8b3a1e83f927f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket.meta b/Assets/Infrastructure/Network/WebSocket.meta new file mode 100644 index 0000000..27133f5 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b7243b76e275de847a176769c0af5e9e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs b/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs new file mode 100644 index 0000000..91ae465 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs @@ -0,0 +1,164 @@ +using UnityEngine; +using ProjectVG.Infrastructure.Network.DTOs.WebSocket; + +namespace ProjectVG.Infrastructure.Network.WebSocket +{ + /// + /// 기본 WebSocket 핸들러 구현체 + /// 기본적인 로깅과 이벤트 처리를 제공합니다. + /// + public class DefaultWebSocketHandler : MonoBehaviour, IWebSocketHandler + { + [Header("Handler Configuration")] + [SerializeField] private bool enableLogging = true; + [SerializeField] private bool autoRegister = true; + + // 이벤트 + public System.Action OnConnectedEvent; + public System.Action OnDisconnectedEvent; + public System.Action OnErrorEvent; + public System.Action OnMessageReceivedEvent; + public System.Action OnChatMessageReceivedEvent; + public System.Action OnSystemMessageReceivedEvent; + public System.Action OnConnectionMessageReceivedEvent; + public System.Action OnSessionIdMessageReceivedEvent; + public System.Action OnAudioDataReceivedEvent; + + private void Start() + { + if (autoRegister) + { + RegisterToManager(); + } + } + + private void OnDestroy() + { + UnregisterFromManager(); + } + + /// + /// WebSocket 매니저에 등록 + /// + public void RegisterToManager() + { + if (WebSocketManager.Instance != null) + { + WebSocketManager.Instance.RegisterHandler(this); + if (enableLogging) + { + Debug.Log("DefaultWebSocketHandler가 WebSocketManager에 등록되었습니다."); + } + } + } + + /// + /// WebSocket 매니저에서 해제 + /// + public void UnregisterFromManager() + { + if (WebSocketManager.Instance != null) + { + WebSocketManager.Instance.UnregisterHandler(this); + if (enableLogging) + { + Debug.Log("DefaultWebSocketHandler가 WebSocketManager에서 해제되었습니다."); + } + } + } + + #region IWebSocketHandler 구현 + + public void OnConnected() + { + if (enableLogging) + { + Debug.Log("WebSocket 연결됨"); + } + + OnConnectedEvent?.Invoke(); + } + + public void OnDisconnected() + { + if (enableLogging) + { + Debug.Log("WebSocket 연결 해제됨"); + } + + OnDisconnectedEvent?.Invoke(); + } + + public void OnError(string error) + { + if (enableLogging) + { + Debug.LogError($"WebSocket 오류: {error}"); + } + + OnErrorEvent?.Invoke(error); + } + + public void OnMessageReceived(WebSocketMessage message) + { + if (enableLogging) + { + Debug.Log($"WebSocket 메시지 수신: {message.type} - {message.data}"); + } + + OnMessageReceivedEvent?.Invoke(message); + } + + public void OnChatMessageReceived(ChatMessage message) + { + if (enableLogging) + { + Debug.Log($"채팅 메시지 수신: {message.characterId} - {message.message}"); + } + + OnChatMessageReceivedEvent?.Invoke(message); + } + + public void OnSystemMessageReceived(SystemMessage message) + { + if (enableLogging) + { + Debug.Log($"시스템 메시지 수신: {message.status} - {message.description}"); + } + + OnSystemMessageReceivedEvent?.Invoke(message); + } + + public void OnConnectionMessageReceived(ConnectionMessage message) + { + if (enableLogging) + { + Debug.Log($"연결 상태 메시지 수신: {message.status} - {message.reason}"); + } + + OnConnectionMessageReceivedEvent?.Invoke(message); + } + + public void OnSessionIdMessageReceived(SessionIdMessage message) + { + if (enableLogging) + { + Debug.Log($"세션 ID 메시지 수신: {message.session_id}"); + } + + OnSessionIdMessageReceivedEvent?.Invoke(message); + } + + public void OnAudioDataReceived(byte[] audioData) + { + if (enableLogging) + { + Debug.Log($"오디오 데이터 수신: {audioData.Length} bytes"); + } + + OnAudioDataReceivedEvent?.Invoke(audioData); + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs.meta b/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs.meta new file mode 100644 index 0000000..c54a2f5 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1209f48068ad90a4ea717118d80332d9 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs new file mode 100644 index 0000000..4e2bce3 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; +using Cysharp.Threading.Tasks; + +namespace ProjectVG.Infrastructure.Network.WebSocket +{ + /// + /// 플랫폼별 Native WebSocket 구현을 위한 인터페이스 + /// + public interface INativeWebSocket : IDisposable + { + // 연결 상태 + bool IsConnected { get; } + bool IsConnecting { get; } + + // 이벤트 + event Action OnConnected; + event Action OnDisconnected; + event Action OnError; + event Action OnMessageReceived; + event Action OnBinaryDataReceived; + + // 연결 관리 + UniTask ConnectAsync(string url, CancellationToken cancellationToken = default); + UniTask DisconnectAsync(); + + // 메시지 전송 + UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default); + UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs.meta new file mode 100644 index 0000000..dc687fe --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 259c46c284a0b4e4e9fb9510200ccf0c \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs b/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs new file mode 100644 index 0000000..00e8e64 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs @@ -0,0 +1,62 @@ +using ProjectVG.Infrastructure.Network.DTOs.WebSocket; + +namespace ProjectVG.Infrastructure.Network.WebSocket +{ + /// + /// WebSocket 이벤트 핸들러 인터페이스 + /// + public interface IWebSocketHandler + { + /// + /// 연결 성공 시 호출 + /// + void OnConnected(); + + /// + /// 연결 해제 시 호출 + /// + void OnDisconnected(); + + /// + /// 연결 오류 시 호출 + /// + /// 오류 메시지 + void OnError(string error); + + /// + /// 메시지 수신 시 호출 + /// + /// 수신된 메시지 + void OnMessageReceived(WebSocketMessage message); + + /// + /// 채팅 메시지 수신 시 호출 + /// + /// 채팅 메시지 + void OnChatMessageReceived(ChatMessage message); + + /// + /// 시스템 메시지 수신 시 호출 + /// + /// 시스템 메시지 + void OnSystemMessageReceived(SystemMessage message); + + /// + /// 연결 상태 메시지 수신 시 호출 + /// + /// 연결 상태 메시지 + void OnConnectionMessageReceived(ConnectionMessage message); + + /// + /// 세션 ID 메시지 수신 시 호출 + /// + /// 세션 ID 메시지 + void OnSessionIdMessageReceived(SessionIdMessage message); + + /// + /// 오디오 데이터 수신 시 호출 + /// + /// 오디오 바이트 데이터 + void OnAudioDataReceived(byte[] audioData); + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs.meta b/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs.meta new file mode 100644 index 0000000..34988b4 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 90c60eb0de0f434479cda3d51cf4c95f \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms.meta b/Assets/Infrastructure/Network/WebSocket/Platforms.meta new file mode 100644 index 0000000..44c78e9 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4da30c2f4bafd614ca7d49bf3e3b9287 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs new file mode 100644 index 0000000..a6c6fe6 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms +{ + /// + /// 모바일 환경에 최적화된 WebSocket 시뮬레이션 구현체 + /// 개발/테스트용으로만 사용 + /// + public class MobileWebSocket : INativeWebSocket + { + public bool IsConnected { get; private set; } + public bool IsConnecting { get; private set; } + + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + public event Action OnMessageReceived; + public event Action OnBinaryDataReceived; + + public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) + { + if (IsConnected || IsConnecting) + { + return IsConnected; + } + + IsConnecting = true; + + try + { + Debug.Log($"모바일 WebSocket 시뮬레이션 연결: {url}"); + await UniTask.Delay(100, cancellationToken: cancellationToken); + + IsConnected = true; + IsConnecting = false; + OnConnected?.Invoke(); + + return true; + } + catch (Exception ex) + { + IsConnecting = false; + OnError?.Invoke(ex.Message); + return false; + } + } + + public async UniTask DisconnectAsync() + { + IsConnected = false; + IsConnecting = false; + OnDisconnected?.Invoke(); + await UniTask.CompletedTask; + } + + public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + return false; + } + + Debug.Log($"모바일 WebSocket 시뮬레이션 메시지: {message}"); + return true; + } + + public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + return false; + } + + Debug.Log($"모바일 WebSocket 시뮬레이션 바이너리: {data.Length} bytes"); + return true; + } + + public void Dispose() + { + DisconnectAsync().Forget(); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs.meta new file mode 100644 index 0000000..de72dd6 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1150790d7959b3147b99271055b8e7ff \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs new file mode 100644 index 0000000..b0fe1c1 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms +{ + /// + /// Unity WebSocket 시뮬레이션 구현체 + /// 개발/테스트용으로만 사용 + /// + public class UnityWebSocket : INativeWebSocket + { + public bool IsConnected { get; private set; } + public bool IsConnecting { get; private set; } + + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + public event Action OnMessageReceived; + public event Action OnBinaryDataReceived; + + public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) + { + if (IsConnected || IsConnecting) + { + return IsConnected; + } + + IsConnecting = true; + + try + { + Debug.Log($"WebSocket 시뮬레이션 연결: {url}"); + await UniTask.Delay(100, cancellationToken: cancellationToken); + + IsConnected = true; + IsConnecting = false; + OnConnected?.Invoke(); + + return true; + } + catch (Exception ex) + { + IsConnecting = false; + OnError?.Invoke(ex.Message); + return false; + } + } + + public async UniTask DisconnectAsync() + { + IsConnected = false; + IsConnecting = false; + OnDisconnected?.Invoke(); + await UniTask.CompletedTask; + } + + public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + return false; + } + + Debug.Log($"WebSocket 시뮬레이션 메시지: {message}"); + return true; + } + + public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + return false; + } + + Debug.Log($"WebSocket 시뮬레이션 바이너리: {data.Length} bytes"); + return true; + } + + public void Dispose() + { + DisconnectAsync().Forget(); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs.meta new file mode 100644 index 0000000..e81aad8 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b2d311f801b0bf64e9f2a0045935857e \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs new file mode 100644 index 0000000..2001241 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms +{ + /// + /// WebSocket 시뮬레이션 구현체 + /// 개발/테스트용으로만 사용 + /// + public class WebSocketSharpFallback : INativeWebSocket + { + public bool IsConnected { get; private set; } + public bool IsConnecting { get; private set; } + + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + public event Action OnMessageReceived; + public event Action OnBinaryDataReceived; + + public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) + { + if (IsConnected || IsConnecting) + { + return IsConnected; + } + + IsConnecting = true; + + try + { + Debug.Log($"WebSocket 시뮬레이션 연결: {url}"); + await UniTask.Delay(100, cancellationToken: cancellationToken); + + IsConnected = true; + IsConnecting = false; + OnConnected?.Invoke(); + + return true; + } + catch (Exception ex) + { + IsConnecting = false; + OnError?.Invoke(ex.Message); + return false; + } + } + + public async UniTask DisconnectAsync() + { + IsConnected = false; + IsConnecting = false; + OnDisconnected?.Invoke(); + await UniTask.CompletedTask; + } + + public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + return false; + } + + Debug.Log($"WebSocket 시뮬레이션 메시지: {message}"); + return true; + } + + public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + return false; + } + + Debug.Log($"WebSocket 시뮬레이션 바이너리: {data.Length} bytes"); + return true; + } + + public void Dispose() + { + DisconnectAsync().Forget(); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs.meta new file mode 100644 index 0000000..12dbb3a --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e59441b18c3f45c4f8b114e21d897f1c \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs new file mode 100644 index 0000000..52ef80f --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs @@ -0,0 +1,468 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using UnityEngine; +using UnityEngine.Networking; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.Configs; +using ProjectVG.Infrastructure.Network.DTOs.WebSocket; +using ProjectVG.Infrastructure.Network.WebSocket.Platforms; + +namespace ProjectVG.Infrastructure.Network.WebSocket +{ + /// + /// WebSocket 연결 및 메시지 관리자 + /// UnityWebRequest를 사용하여 WebSocket 연결을 관리하고, 비동기 결과를 Handler로 전달합니다. + /// + public class WebSocketManager : MonoBehaviour + { + [Header("WebSocket Configuration")] + [SerializeField] private WebSocketConfig webSocketConfig; + + private INativeWebSocket _nativeWebSocket; + private CancellationTokenSource _cancellationTokenSource; + private List _handlers = new List(); + + private bool _isConnected = false; + private bool _isConnecting = false; + private int _reconnectAttempts = 0; + private string _sessionId; + + public static WebSocketManager Instance { get; private set; } + + // 이벤트 + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + public event Action OnMessageReceived; + + // 프로퍼티 + public bool IsConnected => _isConnected; + public bool IsConnecting => _isConnecting; + public string SessionId => _sessionId; + + private void Awake() + { + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + InitializeManager(); + } + else + { + Destroy(gameObject); + } + } + + private void OnDestroy() + { + DisconnectAsync().Forget(); + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + } + + private void InitializeManager() + { + _cancellationTokenSource = new CancellationTokenSource(); + + if (webSocketConfig == null) + { + Debug.LogWarning("WebSocketConfig가 설정되지 않았습니다. 기본 설정을 사용합니다."); + } + + // Native WebSocket 초기화 + InitializeNativeWebSocket(); + } + + private void InitializeNativeWebSocket() + { + // 시뮬레이션 WebSocket 구현체 사용 + _nativeWebSocket = new UnityWebSocket(); + Debug.Log("WebSocket 시뮬레이션 구현체를 사용합니다."); + + // 이벤트 연결 + _nativeWebSocket.OnConnected += OnNativeConnected; + _nativeWebSocket.OnDisconnected += OnNativeDisconnected; + _nativeWebSocket.OnError += OnNativeError; + _nativeWebSocket.OnMessageReceived += OnNativeMessageReceived; + _nativeWebSocket.OnBinaryDataReceived += OnNativeBinaryDataReceived; + } + + /// + /// WebSocketConfig 설정 + /// + public void SetWebSocketConfig(WebSocketConfig config) + { + webSocketConfig = config; + } + + + + /// + /// 핸들러 등록 + /// + public void RegisterHandler(IWebSocketHandler handler) + { + if (!_handlers.Contains(handler)) + { + _handlers.Add(handler); + Debug.Log($"WebSocket 핸들러 등록: {handler.GetType().Name}"); + } + } + + /// + /// 핸들러 해제 + /// + public void UnregisterHandler(IWebSocketHandler handler) + { + if (_handlers.Remove(handler)) + { + Debug.Log($"WebSocket 핸들러 해제: {handler.GetType().Name}"); + } + } + + /// + /// WebSocket 연결 + /// + public async UniTask ConnectAsync(string sessionId = null, CancellationToken cancellationToken = default) + { + if (_isConnected || _isConnecting) + { + Debug.LogWarning("이미 연결 중이거나 연결되어 있습니다."); + return _isConnected; + } + + _isConnecting = true; + _sessionId = sessionId; + + try + { + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + var wsUrl = GetWebSocketUrl(sessionId); + Debug.Log($"WebSocket 연결 시도: {wsUrl}"); + + // Native WebSocket을 통한 실제 연결 + var success = await _nativeWebSocket.ConnectAsync(wsUrl, combinedCancellationToken); + + if (success) + { + _isConnected = true; + _isConnecting = false; + _reconnectAttempts = 0; + + Debug.Log("WebSocket 연결 성공"); + return true; + } + else + { + _isConnecting = false; + return false; + } + } + catch (OperationCanceledException) + { + Debug.Log("WebSocket 연결이 취소되었습니다."); + return false; + } + catch (Exception ex) + { + var error = $"WebSocket 연결 중 예외 발생: {ex.Message}"; + Debug.LogError(error); + + OnError?.Invoke(error); + foreach (var handler in _handlers) + { + handler.OnError(error); + } + + return false; + } + finally + { + _isConnecting = false; + } + } + + /// + /// WebSocket 연결 해제 + /// + public async UniTask DisconnectAsync() + { + if (!_isConnected) + { + return; + } + + _isConnected = false; + _isConnecting = false; + + // Native WebSocket 연결 해제 + if (_nativeWebSocket != null) + { + await _nativeWebSocket.DisconnectAsync(); + } + + Debug.Log("WebSocket 연결 해제"); + + OnDisconnected?.Invoke(); + foreach (var handler in _handlers) + { + handler.OnDisconnected(); + } + + await UniTask.CompletedTask; + } + + /// + /// 메시지 전송 + /// + public async UniTask SendMessageAsync(WebSocketMessage message, CancellationToken cancellationToken = default) + { + if (!_isConnected) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + var jsonMessage = JsonUtility.ToJson(message); + return await _nativeWebSocket.SendMessageAsync(jsonMessage, cancellationToken); + } + catch (Exception ex) + { + Debug.LogError($"메시지 전송 실패: {ex.Message}"); + return false; + } + } + + /// + /// 채팅 메시지 전송 + /// + public async UniTask SendChatMessageAsync(string message, string characterId, string userId, string actor = null, CancellationToken cancellationToken = default) + { + var chatMessage = new ChatMessage + { + type = "chat", + sessionId = _sessionId, + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + message = message, + characterId = characterId, + userId = userId, + actor = actor + }; + + return await SendMessageAsync(chatMessage, cancellationToken); + } + + + + /// + /// 수신된 메시지 처리 (더미 클라이언트와 동일한 방식) + /// + private void ProcessReceivedMessage(WebSocketMessage baseMessage) + { + try + { + Debug.Log($"메시지 수신: {baseMessage.type} - {baseMessage.data}"); + + // 이벤트 발생 + OnMessageReceived?.Invoke(baseMessage); + foreach (var handler in _handlers) + { + handler.OnMessageReceived(baseMessage); + } + + // 메시지 타입에 따른 처리 + switch (baseMessage.type?.ToLower()) + { + case "session_id": + var sessionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); + _sessionId = sessionMessage.session_id; // 세션 ID 저장 + foreach (var handler in _handlers) + { + handler.OnSessionIdMessageReceived(sessionMessage); + } + break; + + case "chat": + var chatMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); + foreach (var handler in _handlers) + { + handler.OnChatMessageReceived(chatMessage); + } + break; + + case "system": + var systemMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); + foreach (var handler in _handlers) + { + handler.OnSystemMessageReceived(systemMessage); + } + break; + + case "connection": + var connectionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); + foreach (var handler in _handlers) + { + handler.OnConnectionMessageReceived(connectionMessage); + } + break; + } + } + catch (Exception ex) + { + Debug.LogError($"메시지 처리 실패: {ex.Message}"); + } + } + + /// + /// 오디오 데이터 처리 (더미 클라이언트와 동일한 방식) + /// + private void ProcessAudioData(byte[] audioData) + { + try + { + Debug.Log($"오디오 데이터 수신: {audioData.Length} bytes"); + + // 이벤트 발생 + foreach (var handler in _handlers) + { + handler.OnAudioDataReceived(audioData); + } + } + catch (Exception ex) + { + Debug.LogError($"오디오 데이터 처리 실패: {ex.Message}"); + } + } + + /// + /// WebSocket URL 생성 (더미 클라이언트와 동일한 방식) + /// + private string GetWebSocketUrl(string sessionId = null) + { + string baseUrl; + + if (webSocketConfig != null) + { + baseUrl = webSocketConfig.GetWebSocketUrl(); + } + else + { + // 기본값 (base64 인코딩된 IP:Port 사용) + baseUrl = "ws://MTIyLjE1My4xMzAuMjIzOjc5MDA=/ws"; + } + + // 세션 ID가 있으면 쿼리 파라미터로 추가 (더미 클라이언트와 동일) + if (!string.IsNullOrEmpty(sessionId)) + { + return $"{baseUrl}?sessionId={sessionId}"; + } + + return baseUrl; + } + + + + /// + /// 자동 재연결 시도 + /// + private async UniTaskVoid TryReconnectAsync() + { + var config = webSocketConfig ?? CreateDefaultWebSocketConfig(); + + if (!config.AutoReconnect || _reconnectAttempts >= config.MaxReconnectAttempts) + { + return; + } + + _reconnectAttempts++; + Debug.Log($"WebSocket 재연결 시도 {_reconnectAttempts}/{config.MaxReconnectAttempts}"); + + await UniTask.Delay(TimeSpan.FromSeconds(config.ReconnectDelay)); + + if (!_isConnected) + { + ConnectAsync(_sessionId).Forget(); + } + } + + /// + /// 기본 WebSocket 설정 생성 + /// + private WebSocketConfig CreateDefaultWebSocketConfig() + { + return WebSocketConfig.CreateDevelopmentConfig(); + } + + #region Native WebSocket Event Handlers + + private void OnNativeConnected() + { + _isConnected = true; + _isConnecting = false; + _reconnectAttempts = 0; + + // 이벤트 발생 + OnConnected?.Invoke(); + foreach (var handler in _handlers) + { + handler.OnConnected(); + } + } + + private void OnNativeDisconnected() + { + _isConnected = false; + _isConnecting = false; + + // 이벤트 발생 + OnDisconnected?.Invoke(); + foreach (var handler in _handlers) + { + handler.OnDisconnected(); + } + + // 자동 재연결 시도 + TryReconnectAsync().Forget(); + } + + private void OnNativeError(string error) + { + _isConnected = false; + _isConnecting = false; + + // 이벤트 발생 + OnError?.Invoke(error); + foreach (var handler in _handlers) + { + handler.OnError(error); + } + } + + private void OnNativeMessageReceived(string message) + { + try + { + // JSON 메시지 파싱 + var baseMessage = JsonUtility.FromJson(message); + ProcessReceivedMessage(baseMessage); + } + catch (Exception ex) + { + Debug.LogError($"메시지 파싱 실패: {ex.Message}"); + } + } + + private void OnNativeBinaryDataReceived(byte[] data) + { + ProcessAudioData(data); + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs.meta b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs.meta new file mode 100644 index 0000000..22dfba5 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8e1977f0484f5f84684a077ad63409b5 \ No newline at end of file From 4341a71bf5519824a244dace6f48c575dd05d545 Mon Sep 17 00:00:00 2001 From: WooSH Date: Mon, 28 Jul 2025 16:01:07 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=B0=8F=20network=20=EC=95=88=EC=A0=84?= =?UTF-8?q?=EC=84=B1=20=EB=B3=B4=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/App/Scenes/NetworkTestScene.unity | 300 ++++++++++ Assets/App/Scenes/NetworkTestScene.unity.meta | 7 + .../Network/Configs/ApiConfig.cs | 11 + .../Network/DTOs/Character/CharacterInfo.cs | 13 +- .../Network/DTOs/Chat/ChatRequest.cs | 13 +- .../Network/Http/HttpApiClient.cs | 13 +- .../Network/NetworkTestManager.cs | 559 ++++++++++++++++++ .../Network/NetworkTestManager.cs.meta | 2 + .../Infrastructure/Network/NetworkTestUI.cs | 286 +++++++++ .../Network/NetworkTestUI.cs.meta | 2 + Assets/Infrastructure/Network/README.md | 63 +- .../Network/Services/CharacterApiService.cs | 37 +- .../Network/Services/ChatApiService.cs | 16 +- Assets/Infrastructure/Network/TestScene.meta | 8 + .../Network/TestScene/NetworkTestScene.unity | 257 ++++++++ .../TestScene/NetworkTestScene.unity.meta | 7 + Assets/Infrastructure/Network/UI.meta | 8 + .../Network/UI/NetworkTestCanvas.prefab | 164 +++++ .../Network/UI/NetworkTestCanvas.prefab.meta | 7 + Packages/manifest.json | 1 + Packages/packages-lock.json | 7 + 21 files changed, 1739 insertions(+), 42 deletions(-) create mode 100644 Assets/App/Scenes/NetworkTestScene.unity create mode 100644 Assets/App/Scenes/NetworkTestScene.unity.meta create mode 100644 Assets/Infrastructure/Network/NetworkTestManager.cs create mode 100644 Assets/Infrastructure/Network/NetworkTestManager.cs.meta create mode 100644 Assets/Infrastructure/Network/NetworkTestUI.cs create mode 100644 Assets/Infrastructure/Network/NetworkTestUI.cs.meta create mode 100644 Assets/Infrastructure/Network/TestScene.meta create mode 100644 Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity create mode 100644 Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity.meta create mode 100644 Assets/Infrastructure/Network/UI.meta create mode 100644 Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab create mode 100644 Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab.meta diff --git a/Assets/App/Scenes/NetworkTestScene.unity b/Assets/App/Scenes/NetworkTestScene.unity new file mode 100644 index 0000000..e066d23 --- /dev/null +++ b/Assets/App/Scenes/NetworkTestScene.unity @@ -0,0 +1,300 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientMode: 0 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &519420028 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 519420032} + - component: {fileID: 519420031} + - component: {fileID: 519420029} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &519420029 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 +--- !u!20 &519420031 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_near: 0.3 + m_far: 1000 + m_fieldOfView: 60 + m_orthographic: 0 + m_orthographicSize: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 0 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &519420032 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1234567890 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1234567891} + - component: {fileID: 1234567892} + m_Layer: 0 + m_Name: NetworkTestManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1234567891 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1234567892 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 00000000000000000000000000000000, type: 3} + m_Name: + m_EditorClassIdentifier: + testSessionId: test-session-123 + testCharacterId: test-character-456 + testUserId: test-user-789 + autoTest: 0 + testInterval: 5 +--- !u!1 &9876543210 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 9876543211} + - component: {fileID: 9876543212} + m_Layer: 0 + m_Name: NetworkTestUI + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &9876543211 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9876543210} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &9876543212 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9876543210} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 00000000000000000000000000000000, type: 3} + m_Name: + m_EditorClassIdentifier: \ No newline at end of file diff --git a/Assets/App/Scenes/NetworkTestScene.unity.meta b/Assets/App/Scenes/NetworkTestScene.unity.meta new file mode 100644 index 0000000..bf59580 --- /dev/null +++ b/Assets/App/Scenes/NetworkTestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6877c3ec4230db044838b9e74fb80cba +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/Configs/ApiConfig.cs b/Assets/Infrastructure/Network/Configs/ApiConfig.cs index cc7c547..6432e37 100644 --- a/Assets/Infrastructure/Network/Configs/ApiConfig.cs +++ b/Assets/Infrastructure/Network/Configs/ApiConfig.cs @@ -97,6 +97,17 @@ public static ApiConfig CreateDevelopmentConfig() config.timeout = 10f; config.maxRetryCount = 1; config.retryDelay = 0.5f; + config.clientId = "unity-client-dev"; + config.clientSecret = "dev-secret-key"; + config.contentType = "application/json"; + config.userAgent = "ProjectVG-Unity-Client/1.0"; + + // 엔드포인트 설정 + config.userEndpoint = "users"; + config.characterEndpoint = "characters"; + config.conversationEndpoint = "conversations"; + config.authEndpoint = "auth"; + return config; } diff --git a/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs index f536f00..3db5e85 100644 --- a/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs +++ b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs @@ -1,4 +1,5 @@ using System; +using UnityEngine; namespace ProjectVG.Infrastructure.Network.DTOs.Character { @@ -6,12 +7,12 @@ namespace ProjectVG.Infrastructure.Network.DTOs.Character /// 캐릭터 정보 DTO /// [Serializable] - public class CharacterInfo + public class CharacterData { - public string id; - public string name; - public string description; - public string role; - public bool isActive; + [SerializeField] public string id; + [SerializeField] public string name; + [SerializeField] public string description; + [SerializeField] public string role; + [SerializeField] public bool isActive; } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs index 53f5efc..e27d034 100644 --- a/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs @@ -1,4 +1,5 @@ using System; +using UnityEngine; namespace ProjectVG.Infrastructure.Network.DTOs.Chat { @@ -8,11 +9,11 @@ namespace ProjectVG.Infrastructure.Network.DTOs.Chat [Serializable] public class ChatRequest { - public string sessionId; - public string actor; - public string message; - public string action = "chat"; - public string character_id; - public string user_id; + [SerializeField] public string sessionId; + [SerializeField] public string actor; + [SerializeField] public string message; + [SerializeField] public string action = "chat"; + [SerializeField] public string characterId; + [SerializeField] public string userId; } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Http/HttpApiClient.cs b/Assets/Infrastructure/Network/Http/HttpApiClient.cs index c6d3c11..1aefbbb 100644 --- a/Assets/Infrastructure/Network/Http/HttpApiClient.cs +++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs @@ -288,11 +288,20 @@ private T ParseResponse(UnityWebRequest request) try { - return JsonUtility.FromJson(responseText); + // Newtonsoft.Json 사용 (snake_case 지원) + return Newtonsoft.Json.JsonConvert.DeserializeObject(responseText); } catch (Exception ex) { - throw new ApiException($"응답 파싱 실패: {ex.Message}", request.responseCode, responseText); + // Newtonsoft.Json 실패 시 Unity JsonUtility로 폴백 + try + { + return JsonUtility.FromJson(responseText); + } + catch (Exception fallbackEx) + { + throw new ApiException($"응답 파싱 실패: {ex.Message} (폴백도 실패: {fallbackEx.Message})", request.responseCode, responseText); + } } } diff --git a/Assets/Infrastructure/Network/NetworkTestManager.cs b/Assets/Infrastructure/Network/NetworkTestManager.cs new file mode 100644 index 0000000..9202008 --- /dev/null +++ b/Assets/Infrastructure/Network/NetworkTestManager.cs @@ -0,0 +1,559 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.WebSocket; +using ProjectVG.Infrastructure.Network.Services; +using ProjectVG.Infrastructure.Network.Http; + +namespace ProjectVG.Infrastructure.Network +{ + /// + /// WebSocket + HTTP 통합 테스트 매니저 + /// WebSocket 연결 → HTTP 요청 → WebSocket으로 결과 수신 + /// + public class NetworkTestManager : MonoBehaviour + { + [Header("테스트 설정")] + [SerializeField] private string testSessionId = "test-session-123"; + [SerializeField] private string testCharacterId = "test-character-456"; + [SerializeField] private string testUserId = "test-user-789"; + + [Header("자동 테스트")] + [SerializeField] private bool autoTest = false; + [SerializeField] private float testInterval = 5f; + + // UI에서 접근할 수 있도록 public 프로퍼티 추가 + public bool AutoTest + { + get => autoTest; + set => autoTest = value; + } + + public float TestInterval + { + get => testInterval; + set => testInterval = value; + } + + private WebSocketManager _webSocketManager; + private ApiServiceManager _apiServiceManager; + private DefaultWebSocketHandler _webSocketHandler; + private CancellationTokenSource _cancellationTokenSource; + private bool _isTestRunning = false; + + private void Awake() + { + _cancellationTokenSource = new CancellationTokenSource(); + + // 매니저들이 없으면 생성 + EnsureManagersExist(); + + InitializeManagers(); + } + + /// + /// 필요한 매니저들이 존재하는지 확인하고 없으면 생성 + /// + private void EnsureManagersExist() + { + // HttpApiClient가 없으면 생성 + if (HttpApiClient.Instance == null) + { + Debug.Log("HttpApiClient를 생성합니다..."); + var httpApiClientGO = new GameObject("HttpApiClient"); + httpApiClientGO.AddComponent(); + DontDestroyOnLoad(httpApiClientGO); + } + + // WebSocketManager가 없으면 생성 + if (WebSocketManager.Instance == null) + { + Debug.Log("WebSocketManager를 생성합니다..."); + var webSocketManagerGO = new GameObject("WebSocketManager"); + webSocketManagerGO.AddComponent(); + DontDestroyOnLoad(webSocketManagerGO); + } + + // ApiServiceManager가 없으면 생성 + if (ApiServiceManager.Instance == null) + { + Debug.Log("ApiServiceManager를 생성합니다..."); + var apiServiceManagerGO = new GameObject("ApiServiceManager"); + apiServiceManagerGO.AddComponent(); + DontDestroyOnLoad(apiServiceManagerGO); + } + } + + private void Start() + { + if (autoTest) + { + StartAutoTest().Forget(); + } + } + + /// + /// UI에서 자동 테스트를 시작할 수 있도록 public 메서드 제공 + /// + public void StartAutoTestFromUI() + { + if (autoTest) + { + StartAutoTest().Forget(); + } + } + + private void OnDestroy() + { + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + } + + private void InitializeManagers() + { + try + { + // WebSocket 매니저 초기화 + _webSocketManager = WebSocketManager.Instance; + if (_webSocketManager == null) + { + Debug.LogError("WebSocketManager.Instance가 null입니다. 매니저가 생성되지 않았습니다."); + return; + } + + // API 서비스 매니저 초기화 + _apiServiceManager = ApiServiceManager.Instance; + if (_apiServiceManager == null) + { + Debug.LogError("ApiServiceManager.Instance가 null입니다. 매니저가 생성되지 않았습니다."); + return; + } + + // WebSocket 핸들러 생성 및 등록 + _webSocketHandler = gameObject.AddComponent(); + if (_webSocketHandler == null) + { + Debug.LogError("DefaultWebSocketHandler 생성에 실패했습니다."); + return; + } + + _webSocketManager.RegisterHandler(_webSocketHandler); + + // 이벤트 구독 + _webSocketHandler.OnConnectedEvent += OnWebSocketConnected; + _webSocketHandler.OnDisconnectedEvent += OnWebSocketDisconnected; + _webSocketHandler.OnErrorEvent += OnWebSocketError; + _webSocketHandler.OnChatMessageReceivedEvent += OnChatMessageReceived; + _webSocketHandler.OnSystemMessageReceivedEvent += OnSystemMessageReceived; + _webSocketHandler.OnSessionIdMessageReceivedEvent += OnSessionIdMessageReceived; + + Debug.Log("NetworkTestManager 초기화 완료"); + } + catch (Exception ex) + { + Debug.LogError($"NetworkTestManager 초기화 중 오류: {ex.Message}"); + } + } + + #region 수동 테스트 메서드들 + + [ContextMenu("1. WebSocket 연결")] + public async void ConnectWebSocket() + { + if (_isTestRunning) + { + Debug.LogWarning("테스트가 이미 실행 중입니다."); + return; + } + + if (_webSocketManager == null) + { + Debug.LogError("WebSocketManager가 초기화되지 않았습니다."); + return; + } + + try + { + Debug.Log("=== WebSocket 연결 시작 ==="); + bool connected = await _webSocketManager.ConnectAsync(testSessionId); + + if (connected) + { + Debug.Log("✅ WebSocket 연결 성공!"); + } + else + { + Debug.LogError("❌ WebSocket 연결 실패!"); + } + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 연결 중 오류: {ex.Message}"); + } + } + + [ContextMenu("2. HTTP 채팅 요청")] + public async void SendChatRequest() + { + if (_webSocketManager == null || !_webSocketManager.IsConnected) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다. 먼저 연결해주세요."); + return; + } + + if (_apiServiceManager == null) + { + Debug.LogError("ApiServiceManager가 초기화되지 않았습니다."); + return; + } + + try + { + Debug.Log("=== HTTP 채팅 요청 시작 ==="); + + var chatRequest = new DTOs.Chat.ChatRequest + { + message = "안녕하세요! 테스트 메시지입니다.", + characterId = testCharacterId, + userId = testUserId, + sessionId = testSessionId, + actor = "web_user" + }; + + var response = await _apiServiceManager.Chat.SendChatAsync(chatRequest); + + if (response != null && response.success) + { + Debug.Log($"✅ HTTP 채팅 요청 성공! 응답: {response.message}"); + } + else + { + Debug.LogError($"❌ HTTP 채팅 요청 실패: {response?.message ?? "알 수 없는 오류"}"); + } + } + catch (Exception ex) + { + Debug.LogError($"HTTP 채팅 요청 중 오류: {ex.Message}"); + } + } + + [ContextMenu("3. HTTP 캐릭터 정보 요청")] + public async void GetCharacterInfo() + { + try + { + Debug.Log("=== HTTP 캐릭터 정보 요청 시작 ==="); + + var character = await _apiServiceManager.Character.GetCharacterAsync(testCharacterId); + + if (character != null) + { + Debug.Log($"✅ 캐릭터 정보 조회 성공!"); + Debug.Log($" - ID: {character.id}"); + Debug.Log($" - 이름: {character.name}"); + Debug.Log($" - 설명: {character.description}"); + Debug.Log($" - 역할: {character.role}"); + Debug.Log($" - 활성화: {character.isActive}"); + } + else + { + Debug.LogError($"❌ 캐릭터 정보 조회 실패: 캐릭터를 찾을 수 없습니다."); + } + } + catch (Exception ex) + { + Debug.LogError($"HTTP 캐릭터 정보 요청 중 오류: {ex.Message}"); + } + } + + [ContextMenu("4. WebSocket 메시지 전송")] + public async void SendWebSocketMessage() + { + if (!_webSocketManager.IsConnected) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다."); + return; + } + + try + { + Debug.Log("=== WebSocket 메시지 전송 시작 ==="); + + bool sent = await _webSocketManager.SendChatMessageAsync( + message: "WebSocket으로 직접 전송하는 테스트 메시지", + characterId: testCharacterId, + userId: testUserId + ); + + if (sent) + { + Debug.Log("✅ WebSocket 메시지 전송 성공!"); + } + else + { + Debug.LogError("❌ WebSocket 메시지 전송 실패!"); + } + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 메시지 전송 중 오류: {ex.Message}"); + } + } + + [ContextMenu("5. WebSocket 연결 해제")] + public async void DisconnectWebSocket() + { + try + { + Debug.Log("=== WebSocket 연결 해제 시작 ==="); + await _webSocketManager.DisconnectAsync(); + Debug.Log("✅ WebSocket 연결 해제 완료!"); + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 연결 해제 중 오류: {ex.Message}"); + } + } + + [ContextMenu("전체 테스트 실행")] + public async void RunFullTest() + { + if (_isTestRunning) + { + Debug.LogWarning("테스트가 이미 실행 중입니다."); + return; + } + + _isTestRunning = true; + + try + { + Debug.Log("🚀 === 전체 테스트 시작 ==="); + + // 1. WebSocket 연결 + Debug.Log("1️⃣ WebSocket 연결 중..."); + bool connected = await _webSocketManager.ConnectAsync(testSessionId); + if (!connected) + { + Debug.LogError("WebSocket 연결 실패로 테스트 중단"); + return; + } + + await UniTask.Delay(1000); // 연결 안정화 대기 + + // 2. HTTP 채팅 요청 + Debug.Log("2️⃣ HTTP 채팅 요청 중..."); + await SendChatRequestInternal(); + + await UniTask.Delay(1000); + + // 3. HTTP 캐릭터 정보 요청 + Debug.Log("3️⃣ HTTP 캐릭터 정보 요청 중..."); + await GetCharacterInfoInternal(); + + await UniTask.Delay(1000); + + // 4. WebSocket 메시지 전송 + Debug.Log("4️⃣ WebSocket 메시지 전송 중..."); + await SendWebSocketMessageInternal(); + + await UniTask.Delay(1000); + + // 5. WebSocket 연결 해제 + Debug.Log("5️⃣ WebSocket 연결 해제 중..."); + await _webSocketManager.DisconnectAsync(); + + Debug.Log("✅ === 전체 테스트 완료 ==="); + } + catch (Exception ex) + { + Debug.LogError($"전체 테스트 중 오류: {ex.Message}"); + } + finally + { + _isTestRunning = false; + } + } + + #endregion + + #region 자동 테스트 + + private async UniTaskVoid StartAutoTest() + { + Debug.Log("🔄 자동 테스트 시작..."); + + // 매니저 초기화 확인 + if (_webSocketManager == null || _apiServiceManager == null) + { + Debug.LogError("매니저가 초기화되지 않았습니다. 자동 테스트를 중단합니다."); + return; + } + + // HttpApiClient 확인 + if (HttpApiClient.Instance == null) + { + Debug.LogError("HttpApiClient가 초기화되지 않았습니다. 자동 테스트를 중단합니다."); + return; + } + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + await RunFullTestInternal(); + await UniTask.Delay(TimeSpan.FromSeconds(testInterval), cancellationToken: _cancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + Debug.Log("자동 테스트가 취소되었습니다."); + break; + } + catch (Exception ex) + { + Debug.LogError($"자동 테스트 중 오류: {ex.Message}"); + Debug.LogError($"스택 트레이스: {ex.StackTrace}"); + await UniTask.Delay(5000, cancellationToken: _cancellationTokenSource.Token); + } + } + + Debug.Log("🔄 자동 테스트 종료"); + } + + private async UniTask RunFullTestInternal() + { + // 매니저 null 체크 + if (_webSocketManager == null) + { + Debug.LogError("WebSocketManager가 null입니다. 초기화를 확인해주세요."); + return; + } + + if (_apiServiceManager == null) + { + Debug.LogError("ApiServiceManager가 null입니다. 초기화를 확인해주세요."); + return; + } + + try + { + // 1. WebSocket 연결 + Debug.Log("자동 테스트 - WebSocket 연결 중..."); + await _webSocketManager.ConnectAsync(testSessionId); + await UniTask.Delay(500); + + // 2. HTTP 요청들 + Debug.Log("자동 테스트 - HTTP 채팅 요청 중..."); + await SendChatRequestInternal(); + await UniTask.Delay(500); + + Debug.Log("자동 테스트 - HTTP 캐릭터 정보 요청 중..."); + await GetCharacterInfoInternal(); + await UniTask.Delay(500); + + // 3. WebSocket 메시지 전송 + Debug.Log("자동 테스트 - WebSocket 메시지 전송 중..."); + await SendWebSocketMessageInternal(); + await UniTask.Delay(500); + + // 4. 연결 해제 + Debug.Log("자동 테스트 - WebSocket 연결 해제 중..."); + await _webSocketManager.DisconnectAsync(); + + Debug.Log("자동 테스트 - 전체 테스트 완료"); + } + catch (Exception ex) + { + Debug.LogError($"자동 테스트 중 오류: {ex.Message}"); + throw; // 상위로 예외 전파 + } + } + + private async UniTask SendChatRequestInternal() + { + if (_apiServiceManager?.Chat == null) + { + Debug.LogError("ChatApiService가 null입니다."); + return; + } + + var chatRequest = new DTOs.Chat.ChatRequest + { + message = $"자동 테스트 메시지 - {DateTime.Now:HH:mm:ss}", + characterId = testCharacterId, + userId = testUserId, + sessionId = testSessionId, + actor = "web_user" + }; + + await _apiServiceManager.Chat.SendChatAsync(chatRequest); + } + + private async UniTask GetCharacterInfoInternal() + { + if (_apiServiceManager?.Character == null) + { + Debug.LogError("CharacterApiService가 null입니다."); + return; + } + + var character = await _apiServiceManager.Character.GetCharacterAsync(testCharacterId); + if (character != null) + { + Debug.Log($"자동 테스트 - 캐릭터 정보 조회 성공: {character.name}"); + } + } + + private async UniTask SendWebSocketMessageInternal() + { + if (_webSocketManager == null) + { + Debug.LogError("WebSocketManager가 null입니다."); + return; + } + + await _webSocketManager.SendChatMessageAsync( + message: $"자동 WebSocket 메시지 - {DateTime.Now:HH:mm:ss}", + characterId: testCharacterId, + userId: testUserId + ); + } + + #endregion + + #region WebSocket 이벤트 핸들러 + + private void OnWebSocketConnected() + { + Debug.Log("🎉 WebSocket 연결됨!"); + } + + private void OnWebSocketDisconnected() + { + Debug.Log("🔌 WebSocket 연결 해제됨!"); + } + + private void OnWebSocketError(string error) + { + Debug.LogError($"❌ WebSocket 오류: {error}"); + } + + private void OnChatMessageReceived(DTOs.WebSocket.ChatMessage message) + { + Debug.Log($"💬 WebSocket 채팅 메시지 수신: {message.message}"); + } + + private void OnSystemMessageReceived(DTOs.WebSocket.SystemMessage message) + { + Debug.Log($"🔧 WebSocket 시스템 메시지 수신: {message.description}"); + } + + private void OnSessionIdMessageReceived(DTOs.WebSocket.SessionIdMessage message) + { + Debug.Log($"🆔 WebSocket 세션 ID 수신: {message.session_id}"); + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/NetworkTestManager.cs.meta b/Assets/Infrastructure/Network/NetworkTestManager.cs.meta new file mode 100644 index 0000000..b47c067 --- /dev/null +++ b/Assets/Infrastructure/Network/NetworkTestManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fca7c331eb199ca4988f589a0cfefffe \ No newline at end of file diff --git a/Assets/Infrastructure/Network/NetworkTestUI.cs b/Assets/Infrastructure/Network/NetworkTestUI.cs new file mode 100644 index 0000000..ced1a02 --- /dev/null +++ b/Assets/Infrastructure/Network/NetworkTestUI.cs @@ -0,0 +1,286 @@ +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +namespace ProjectVG.Infrastructure.Network +{ + /// + /// 네트워크 테스트를 위한 UI 매니저 + /// + public class NetworkTestUI : MonoBehaviour + { + [Header("UI References")] + [SerializeField] private Button connectButton; + [SerializeField] private Button disconnectButton; + [SerializeField] private Button chatRequestButton; + [SerializeField] private Button characterInfoButton; + [SerializeField] private Button webSocketMessageButton; + [SerializeField] private Button fullTestButton; + [SerializeField] private Button autoTestButton; + + [Header("Status Display")] + [SerializeField] private TextMeshProUGUI statusText; + [SerializeField] private TextMeshProUGUI logText; + [SerializeField] private ScrollRect logScrollRect; + + [Header("Input Fields")] + [SerializeField] private TMP_InputField sessionIdInput; + [SerializeField] private TMP_InputField characterIdInput; + [SerializeField] private TMP_InputField userIdInput; + [SerializeField] private TMP_InputField messageInput; + + private NetworkTestManager _testManager; + private bool _isAutoTestRunning = false; + + private void Start() + { + _testManager = FindObjectOfType(); + if (_testManager == null) + { + Debug.LogError("NetworkTestManager를 찾을 수 없습니다!"); + return; + } + + InitializeUI(); + UpdateStatus("대기 중..."); + } + + private void InitializeUI() + { + // 버튼 이벤트 연결 + if (connectButton != null) + connectButton.onClick.AddListener(OnConnectButtonClicked); + + if (disconnectButton != null) + disconnectButton.onClick.AddListener(OnDisconnectButtonClicked); + + if (chatRequestButton != null) + chatRequestButton.onClick.AddListener(OnChatRequestButtonClicked); + + if (characterInfoButton != null) + characterInfoButton.onClick.AddListener(OnCharacterInfoButtonClicked); + + if (webSocketMessageButton != null) + webSocketMessageButton.onClick.AddListener(OnWebSocketMessageButtonClicked); + + if (fullTestButton != null) + fullTestButton.onClick.AddListener(OnFullTestButtonClicked); + + if (autoTestButton != null) + autoTestButton.onClick.AddListener(OnAutoTestButtonClicked); + + // 초기값 설정 + if (sessionIdInput != null) + sessionIdInput.text = "test-session-123"; + + if (characterIdInput != null) + characterIdInput.text = "test-character-456"; + + if (userIdInput != null) + userIdInput.text = "test-user-789"; + + if (messageInput != null) + messageInput.text = "안녕하세요! 테스트 메시지입니다."; + + // 초기 버튼 상태 설정 + UpdateButtonStates(false); + } + + private void UpdateButtonStates(bool isConnected) + { + if (connectButton != null) + connectButton.interactable = !isConnected; + + if (disconnectButton != null) + disconnectButton.interactable = isConnected; + + if (chatRequestButton != null) + chatRequestButton.interactable = isConnected; + + if (characterInfoButton != null) + characterInfoButton.interactable = true; // HTTP 요청은 연결 없이도 가능 + + if (webSocketMessageButton != null) + webSocketMessageButton.interactable = isConnected; + + if (fullTestButton != null) + fullTestButton.interactable = !_isAutoTestRunning; + + if (autoTestButton != null) + { + autoTestButton.interactable = !_isAutoTestRunning; + autoTestButton.GetComponentInChildren().text = + _isAutoTestRunning ? "자동 테스트 중지" : "자동 테스트 시작"; + } + } + + private void UpdateStatus(string status) + { + if (statusText != null) + { + statusText.text = $"상태: {status}"; + } + } + + private void AddLog(string message) + { + if (logText != null) + { + logText.text += $"[{System.DateTime.Now:HH:mm:ss}] {message}\n"; + + // 스크롤을 맨 아래로 이동 + if (logScrollRect != null) + { + Canvas.ForceUpdateCanvases(); + logScrollRect.verticalNormalizedPosition = 0f; + } + } + } + + #region Button Event Handlers + + private void OnConnectButtonClicked() + { + AddLog("WebSocket 연결 시도..."); + UpdateStatus("연결 중..."); + _testManager.ConnectWebSocket(); + } + + private void OnDisconnectButtonClicked() + { + AddLog("WebSocket 연결 해제..."); + UpdateStatus("연결 해제 중..."); + _testManager.DisconnectWebSocket(); + } + + private void OnChatRequestButtonClicked() + { + AddLog("HTTP 채팅 요청 전송..."); + UpdateStatus("채팅 요청 중..."); + _testManager.SendChatRequest(); + } + + private void OnCharacterInfoButtonClicked() + { + AddLog("HTTP 캐릭터 정보 요청..."); + UpdateStatus("캐릭터 정보 요청 중..."); + _testManager.GetCharacterInfo(); + } + + private void OnWebSocketMessageButtonClicked() + { + string message = messageInput != null ? messageInput.text : "테스트 메시지"; + AddLog($"WebSocket 메시지 전송: {message}"); + UpdateStatus("WebSocket 메시지 전송 중..."); + _testManager.SendWebSocketMessage(); + } + + private void OnFullTestButtonClicked() + { + AddLog("전체 테스트 시작..."); + UpdateStatus("전체 테스트 실행 중..."); + _testManager.RunFullTest(); + } + + private void OnAutoTestButtonClicked() + { + if (!_isAutoTestRunning) + { + _isAutoTestRunning = true; + AddLog("자동 테스트 시작..."); + UpdateStatus("자동 테스트 실행 중..."); + UpdateButtonStates(true); + + // 자동 테스트 시작 + _testManager.AutoTest = true; + _testManager.StartAutoTestFromUI(); + } + else + { + _isAutoTestRunning = false; + AddLog("자동 테스트 중지..."); + UpdateStatus("대기 중..."); + UpdateButtonStates(false); + + // 자동 테스트 중지 + _testManager.AutoTest = false; + } + } + + #endregion + + #region Public Methods for External Updates + + public void OnWebSocketConnected() + { + UpdateStatus("WebSocket 연결됨"); + UpdateButtonStates(true); + AddLog("✅ WebSocket 연결 성공!"); + } + + public void OnWebSocketDisconnected() + { + UpdateStatus("WebSocket 연결 해제됨"); + UpdateButtonStates(false); + AddLog("🔌 WebSocket 연결 해제됨!"); + } + + public void OnWebSocketError(string error) + { + UpdateStatus("WebSocket 오류"); + AddLog($"❌ WebSocket 오류: {error}"); + } + + public void OnChatMessageReceived(string message) + { + AddLog($"💬 채팅 메시지 수신: {message}"); + } + + public void OnSystemMessageReceived(string message) + { + AddLog($"🔧 시스템 메시지 수신: {message}"); + } + + public void OnSessionIdMessageReceived(string sessionId) + { + AddLog($"🆔 세션 ID 수신: {sessionId}"); + } + + public void OnHttpRequestSuccess(string operation) + { + AddLog($"✅ HTTP {operation} 성공!"); + } + + public void OnHttpRequestFailed(string operation, string error) + { + AddLog($"❌ HTTP {operation} 실패: {error}"); + } + + #endregion + + private void OnDestroy() + { + // 버튼 이벤트 해제 + if (connectButton != null) + connectButton.onClick.RemoveListener(OnConnectButtonClicked); + + if (disconnectButton != null) + disconnectButton.onClick.RemoveListener(OnDisconnectButtonClicked); + + if (chatRequestButton != null) + chatRequestButton.onClick.RemoveListener(OnChatRequestButtonClicked); + + if (characterInfoButton != null) + characterInfoButton.onClick.RemoveListener(OnCharacterInfoButtonClicked); + + if (webSocketMessageButton != null) + webSocketMessageButton.onClick.RemoveListener(OnWebSocketMessageButtonClicked); + + if (fullTestButton != null) + fullTestButton.onClick.RemoveListener(OnFullTestButtonClicked); + + if (autoTestButton != null) + autoTestButton.onClick.RemoveListener(OnAutoTestButtonClicked); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/NetworkTestUI.cs.meta b/Assets/Infrastructure/Network/NetworkTestUI.cs.meta new file mode 100644 index 0000000..4ed77f3 --- /dev/null +++ b/Assets/Infrastructure/Network/NetworkTestUI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f6ca4fabfdceafa479759e863d482a18 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/README.md b/Assets/Infrastructure/Network/README.md index 05104c7..2b46816 100644 --- a/Assets/Infrastructure/Network/README.md +++ b/Assets/Infrastructure/Network/README.md @@ -49,28 +49,44 @@ Assets/Infrastructure/Network/ ## 🚀 사용법 -### HTTP API 사용 +### 1. 전체 흐름 테스트 (권장) +```csharp +// NetworkTestManager 사용 +var testManager = FindObjectOfType(); + +// 1. WebSocket 연결 +await testManager.ConnectWebSocket(); + +// 2. HTTP 요청 전송 +await testManager.SendChatRequest(); + +// 3. WebSocket으로 결과 수신 (자동) +// 서버가 비동기 작업 완료 후 WebSocket으로 결과 전송 +``` + +### 2. 개별 모듈 사용 +#### HTTP API 사용 ```csharp // API 서비스 매니저 사용 var apiManager = ApiServiceManager.Instance; // 채팅 API 사용 -var chatResponse = await apiManager.Chat.SendChatAsync( - sessionId: "session-123", - actor: "user", - message: "안녕하세요!", - characterId: "char-456", - userId: "user-789" +var chatResponse = await apiManager.ChatApiService.SendChatAsync( + new ChatRequest + { + message = "안녕하세요!", + characterId = "char-456", + userId = "user-789", + sessionId = "session-123" + } ); // 캐릭터 API 사용 -var characters = await apiManager.Character.GetCharactersAsync(); -var character = await apiManager.Character.GetCharacterAsync("char-456"); +var character = await apiManager.CharacterApiService.GetCharacterAsync("char-456"); ``` -### WebSocket 사용 - +#### WebSocket 사용 ```csharp // WebSocket 매니저 사용 var wsManager = WebSocketManager.Instance; @@ -92,11 +108,18 @@ await wsManager.SendChatMessageAsync( ## ⚙️ 설정 -### ApiConfig 설정 +### 테스트 환경 설정 ```csharp -// ScriptableObject로 생성 +// localhost:7900으로 설정 +var apiConfig = ApiConfig.CreateDevelopmentConfig(); +var wsConfig = WebSocketConfig.CreateDevelopmentConfig(); +``` + +### 프로덕션 환경 설정 +```csharp +// 실제 서버로 설정 var apiConfig = ApiConfig.CreateProductionConfig(); -apiConfig.BaseUrl = "http://122.153.130.223:7900/api/v1/"; +var wsConfig = WebSocketConfig.CreateProductionConfig(); ``` ### WebSocketConfig 설정 @@ -129,7 +152,7 @@ Debug.Log("WebSocket 시뮬레이션 연결"); ### 시뮬레이션 모드 ``` -WebSocket 시뮬레이션 연결: ws://... +WebSocket 시뮬레이션 연결: ws://localhost:7900/ws WebSocket 시뮬레이션 메시지: ... ``` **설명:** 실제 WebSocket 연결 대신 시뮬레이션으로 동작합니다. @@ -139,6 +162,16 @@ WebSocket 시뮬레이션 메시지: ... - 로그를 통해 메시지 흐름 확인 - 안전한 테스트 환경 제공 +### 테스트 실행 방법 +1. **NetworkTestManager** 컴포넌트를 씬에 추가 +2. **Context Menu**에서 테스트 실행: + - `1. WebSocket 연결` + - `2. HTTP 채팅 요청` + - `3. HTTP 캐릭터 정보 요청` + - `4. WebSocket 메시지 전송` + - `5. WebSocket 연결 해제` + - `전체 테스트 실행` + ## 📝 로그 모든 로그는 한국어로 출력됩니다: diff --git a/Assets/Infrastructure/Network/Services/CharacterApiService.cs b/Assets/Infrastructure/Network/Services/CharacterApiService.cs index 9244703..2e9205d 100644 --- a/Assets/Infrastructure/Network/Services/CharacterApiService.cs +++ b/Assets/Infrastructure/Network/Services/CharacterApiService.cs @@ -1,4 +1,5 @@ using System.Threading; +using UnityEngine; using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.Http; using ProjectVG.Infrastructure.Network.DTOs.Character; @@ -15,6 +16,10 @@ public class CharacterApiService public CharacterApiService() { _httpClient = HttpApiClient.Instance; + if (_httpClient == null) + { + Debug.LogError("HttpApiClient.Instance가 null입니다. HttpApiClient가 생성되지 않았습니다."); + } } /// @@ -22,9 +27,15 @@ public CharacterApiService() /// /// 취소 토큰 /// 캐릭터 목록 - public async UniTask GetAllCharactersAsync(CancellationToken cancellationToken = default) + public async UniTask GetAllCharactersAsync(CancellationToken cancellationToken = default) { - return await _httpClient.GetAsync("character", cancellationToken: cancellationToken); + if (_httpClient == null) + { + Debug.LogError("HttpApiClient가 null입니다. 초기화를 확인해주세요."); + return null; + } + + return await _httpClient.GetAsync("character", cancellationToken: cancellationToken); } /// @@ -33,9 +44,15 @@ public async UniTask GetAllCharactersAsync(CancellationToken ca /// 캐릭터 ID /// 취소 토큰 /// 캐릭터 정보 - public async UniTask GetCharacterAsync(string characterId, CancellationToken cancellationToken = default) + public async UniTask GetCharacterAsync(string characterId, CancellationToken cancellationToken = default) { - return await _httpClient.GetAsync($"character/{characterId}", cancellationToken: cancellationToken); + if (_httpClient == null) + { + Debug.LogError("HttpApiClient가 null입니다. 초기화를 확인해주세요."); + return null; + } + + return await _httpClient.GetAsync($"character/{characterId}", cancellationToken: cancellationToken); } /// @@ -44,9 +61,9 @@ public async UniTask GetCharacterAsync(string characterId, Cancel /// 캐릭터 생성 요청 /// 취소 토큰 /// 생성된 캐릭터 정보 - public async UniTask CreateCharacterAsync(CreateCharacterRequest request, CancellationToken cancellationToken = default) + public async UniTask CreateCharacterAsync(CreateCharacterRequest request, CancellationToken cancellationToken = default) { - return await _httpClient.PostAsync("character", request, cancellationToken: cancellationToken); + return await _httpClient.PostAsync("character", request, cancellationToken: cancellationToken); } /// @@ -58,7 +75,7 @@ public async UniTask CreateCharacterAsync(CreateCharacterRequest /// 활성화 여부 /// 취소 토큰 /// 생성된 캐릭터 정보 - public async UniTask CreateCharacterAsync( + public async UniTask CreateCharacterAsync( string name, string description, string role, @@ -83,9 +100,9 @@ public async UniTask CreateCharacterAsync( /// 수정 요청 /// 취소 토큰 /// 수정된 캐릭터 정보 - public async UniTask UpdateCharacterAsync(string characterId, UpdateCharacterRequest request, CancellationToken cancellationToken = default) + public async UniTask UpdateCharacterAsync(string characterId, UpdateCharacterRequest request, CancellationToken cancellationToken = default) { - return await _httpClient.PutAsync($"character/{characterId}", request, cancellationToken: cancellationToken); + return await _httpClient.PutAsync($"character/{characterId}", request, cancellationToken: cancellationToken); } /// @@ -98,7 +115,7 @@ public async UniTask UpdateCharacterAsync(string characterId, Upd /// 활성화 여부 /// 취소 토큰 /// 수정된 캐릭터 정보 - public async UniTask UpdateCharacterAsync( + public async UniTask UpdateCharacterAsync( string characterId, string name = null, string description = null, diff --git a/Assets/Infrastructure/Network/Services/ChatApiService.cs b/Assets/Infrastructure/Network/Services/ChatApiService.cs index 44a2e8b..579db8a 100644 --- a/Assets/Infrastructure/Network/Services/ChatApiService.cs +++ b/Assets/Infrastructure/Network/Services/ChatApiService.cs @@ -1,4 +1,5 @@ using System.Threading; +using UnityEngine; using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.Http; using ProjectVG.Infrastructure.Network.DTOs.Chat; @@ -15,6 +16,10 @@ public class ChatApiService public ChatApiService() { _httpClient = HttpApiClient.Instance; + if (_httpClient == null) + { + Debug.LogError("HttpApiClient.Instance가 null입니다. HttpApiClient가 생성되지 않았습니다."); + } } /// @@ -25,6 +30,12 @@ public ChatApiService() /// 채팅 응답 public async UniTask SendChatAsync(ChatRequest request, CancellationToken cancellationToken = default) { + if (_httpClient == null) + { + Debug.LogError("HttpApiClient가 null입니다. 초기화를 확인해주세요."); + return null; + } + return await _httpClient.PostAsync("chat", request, cancellationToken: cancellationToken); } @@ -49,10 +60,9 @@ public async UniTask SendChatAsync( var request = new ChatRequest { sessionId = sessionId, - actor = actor, message = message, - character_id = characterId, - user_id = userId + characterId = characterId, + userId = userId }; return await SendChatAsync(request, cancellationToken); diff --git a/Assets/Infrastructure/Network/TestScene.meta b/Assets/Infrastructure/Network/TestScene.meta new file mode 100644 index 0000000..8558e6e --- /dev/null +++ b/Assets/Infrastructure/Network/TestScene.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 153323d148c986941975059f33cfa79e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity b/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity new file mode 100644 index 0000000..3af233a --- /dev/null +++ b/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity @@ -0,0 +1,257 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientMode: 0 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &519420028 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 519420032} + - component: {fileID: 519420031} + - component: {fileID: 519420029} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &519420029 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 +--- !u!20 &519420031 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_near: 0.3 + m_far: 1000 + m_fieldOfView: 60 + m_orthographic: 0 + m_orthographicSize: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &519420032 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1234567890 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1234567891} + - component: {fileID: 1234567892} + m_Layer: 0 + m_Name: NetworkTestManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1234567891 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1234567892 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 00000000000000000000000000000000, type: 3} + m_Name: + m_EditorClassIdentifier: + testSessionId: test-session-123 + testCharacterId: test-character-456 + testUserId: test-user-789 + autoTest: 0 + testInterval: 5 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity.meta b/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity.meta new file mode 100644 index 0000000..9c723cb --- /dev/null +++ b/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4fc900551cce9f9489af0a318a1a71bc +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/UI.meta b/Assets/Infrastructure/Network/UI.meta new file mode 100644 index 0000000..194b70f --- /dev/null +++ b/Assets/Infrastructure/Network/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 191d3f4a282ef6545b6fde81cdd0b3d3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab b/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab new file mode 100644 index 0000000..8bec50c --- /dev/null +++ b/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab @@ -0,0 +1,164 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1234567890 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1234567891} + - component: {fileID: 1234567892} + - component: {fileID: 1234567893} + m_Layer: 5 + m_Name: NetworkTestCanvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1234567891 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 1234567894} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!20 &1234567892 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_near: 0.3 + m_far: 1000 + m_fieldOfView: 60 + m_orthographic: 1 + m_orthographicSize: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 0 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!223 &1234567893 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 1234567892} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannels: + serializedVersion: 2 + m_SerializedAdditionalShaderChannels: 25 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!1 &1234567894 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1234567895} + - component: {fileID: 1234567896} + - component: {fileID: 1234567897} + m_Layer: 5 + m_Name: TestPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1234567895 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567894} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1234567891} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!224 &1234567896 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567894} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1234567891} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1234567897 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567894} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 00000000000000000000000000000000, type: 3} + m_Name: + m_EditorClassIdentifier: \ No newline at end of file diff --git a/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab.meta b/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab.meta new file mode 100644 index 0000000..4f4e015 --- /dev/null +++ b/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1e15157bc7b44a8499f09747a910ec91 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index 5bedd7e..0f3c020 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,6 +1,7 @@ { "dependencies": { "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "com.unity.nuget.newtonsoft-json": "3.2.1", "com.unity.collab-proxy": "2.8.2", "com.unity.feature.2d": "2.0.1", "com.unity.ide.rider": "3.0.36", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 0b83833..3678eb7 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -198,6 +198,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.newtonsoft-json": { + "version": "3.2.1", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.render-pipelines.core": { "version": "17.0.4", "depth": 1, From 0cde984eae7a2e0e54267a351fb3635286cb7102 Mon Sep 17 00:00:00 2001 From: WooSH Date: Mon, 28 Jul 2025 23:06:40 +0900 Subject: [PATCH 5/8] =?UTF-8?q?test:=20api=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Configs/ApiConfig.cs | 96 ++++-- .../Network/Configs/WebSocketConfig.cs | 38 ++- .../Network/DTOs/Chat/ChatRequest.cs | 24 +- .../Network/Http/HttpApiClient.cs | 288 +++++++++++------ .../Network/NetworkTestManager.cs.meta | 2 - .../Network/NetworkTestUI.cs.meta | 2 - .../Network/Services/ChatApiService.cs | 92 +++++- .../Network/TestScene/NetworkTestScene.unity | 257 --------------- .../WebSocket/Platforms/RealWebSocket.cs | 219 +++++++++++++ .../WebSocket/Platforms/RealWebSocket.cs.meta | 2 + .../Network/WebSocket/WebSocketManager.cs | 49 ++- .../Models/Mao/Mao.fadeMotionList.asset | 2 +- .../Samples/Models/Mao/motions/mtn_01.anim | 2 +- .../Models/Mao/motions/mtn_01.fade.asset | 2 +- .../Samples/Models/Mao/motions/mtn_02.anim | 2 +- .../Models/Mao/motions/mtn_02.fade.asset | 2 +- .../Samples/Models/Mao/motions/mtn_03.anim | 2 +- .../Models/Mao/motions/mtn_03.fade.asset | 2 +- .../Samples/Models/Mao/motions/mtn_04.anim | 2 +- .../Models/Mao/motions/mtn_04.fade.asset | 2 +- .../Samples/Models/Mao/motions/sample_01.anim | 2 +- .../Models/Mao/motions/sample_01.fade.asset | 2 +- .../Models/Mao/motions/special_01.anim | 2 +- .../Models/Mao/motions/special_01.fade.asset | 2 +- .../Models/Mao/motions/special_02.anim | 2 +- .../Models/Mao/motions/special_02.fade.asset | 2 +- .../Models/Mao/motions/special_03.anim | 2 +- .../Models/Mao/motions/special_03.fade.asset | 2 +- .../Models/Natori/Natori.fadeMotionList.asset | 2 +- .../Samples/Models/Natori/motions/mtn_00.anim | 2 +- .../Models/Natori/motions/mtn_00.fade.asset | 2 +- .../Samples/Models/Natori/motions/mtn_01.anim | 2 +- .../Models/Natori/motions/mtn_01.fade.asset | 2 +- .../Samples/Models/Natori/motions/mtn_02.anim | 2 +- .../Models/Natori/motions/mtn_02.fade.asset | 2 +- .../Samples/Models/Natori/motions/mtn_03.anim | 2 +- .../Models/Natori/motions/mtn_03.fade.asset | 2 +- .../Samples/Models/Natori/motions/mtn_04.anim | 2 +- .../Models/Natori/motions/mtn_04.fade.asset | 2 +- .../Samples/Models/Natori/motions/mtn_05.anim | 2 +- .../Models/Natori/motions/mtn_05.fade.asset | 2 +- .../Samples/Models/Natori/motions/mtn_06.anim | 2 +- .../Models/Natori/motions/mtn_06.fade.asset | 2 +- .../Samples/Models/Natori/motions/mtn_07.anim | 2 +- .../Models/Natori/motions/mtn_07.fade.asset | 2 +- .../Samples/Models/Natori/motions/mtn_08.anim | 2 +- .../Models/Natori/motions/mtn_08.fade.asset | 2 +- .../Models/Rice/motions/mtn_00.fade.asset | 2 +- .../Models/Rice/motions/mtn_01.fade.asset | 2 +- .../Models/Rice/motions/mtn_02.fade.asset | 2 +- .../Models/Rice/motions/mtn_03.fade.asset | 2 +- .../Runtime.meta} | 3 +- .../Runtime}/NetworkTestManager.cs | 295 +++++++++++++++--- .../Tests/Runtime/NetworkTestManager.cs.meta | 2 + .../Runtime}/NetworkTestUI.cs | 2 +- Assets/Tests/Runtime/NetworkTestUI.cs.meta | 2 + Assets/Tests/Sences.meta | 8 + .../Sences}/NetworkTestScene.unity | 208 ++++++++---- .../Sences}/NetworkTestScene.unity.meta | 0 Assets/Tests/UI.meta | 8 + .../UI/NetworkTestCanvas.prefab | 127 +++++--- .../UI/NetworkTestCanvas.prefab.meta | 2 +- Packages/manifest.json | 4 +- Packages/packages-lock.json | 28 -- ProjectSettings/ProjectSettings.asset | 2 +- ProjectSettings/SceneTemplateSettings.json | 121 +++++++ 66 files changed, 1319 insertions(+), 642 deletions(-) delete mode 100644 Assets/Infrastructure/Network/NetworkTestManager.cs.meta delete mode 100644 Assets/Infrastructure/Network/NetworkTestUI.cs.meta delete mode 100644 Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs.meta rename Assets/{Infrastructure/Network/TestScene/NetworkTestScene.unity.meta => Tests/Runtime.meta} (67%) rename Assets/{Infrastructure/Network => Tests/Runtime}/NetworkTestManager.cs (58%) create mode 100644 Assets/Tests/Runtime/NetworkTestManager.cs.meta rename Assets/{Infrastructure/Network => Tests/Runtime}/NetworkTestUI.cs (99%) create mode 100644 Assets/Tests/Runtime/NetworkTestUI.cs.meta create mode 100644 Assets/Tests/Sences.meta rename Assets/{App/Scenes => Tests/Sences}/NetworkTestScene.unity (59%) rename Assets/{App/Scenes => Tests/Sences}/NetworkTestScene.unity.meta (100%) create mode 100644 Assets/Tests/UI.meta rename Assets/{Infrastructure/Network => Tests}/UI/NetworkTestCanvas.prefab (58%) rename Assets/{Infrastructure/Network => Tests}/UI/NetworkTestCanvas.prefab.meta (74%) create mode 100644 ProjectSettings/SceneTemplateSettings.json diff --git a/Assets/Infrastructure/Network/Configs/ApiConfig.cs b/Assets/Infrastructure/Network/Configs/ApiConfig.cs index 6432e37..9c6c581 100644 --- a/Assets/Infrastructure/Network/Configs/ApiConfig.cs +++ b/Assets/Infrastructure/Network/Configs/ApiConfig.cs @@ -29,6 +29,12 @@ public class ApiConfig : ScriptableObject [SerializeField] private string contentType = "application/json"; [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; + // Constants + private const string API_PATH = "api"; + private const string DEFAULT_CLIENT_ID = "unity-client-dev"; + private const string DEFAULT_CLIENT_SECRET = "dev-secret-key"; + private const string DEFAULT_USER_AGENT = "ProjectVG-Unity-Client/1.0"; + // Properties public string BaseUrl => baseUrl; public string ApiVersion => apiVersion; @@ -46,12 +52,14 @@ public class ApiConfig : ScriptableObject public string ConversationEndpoint => conversationEndpoint; public string AuthEndpoint => authEndpoint; + #region URL Generation Methods + /// /// 전체 API URL 생성 /// public string GetFullUrl(string endpoint) { - return $"{baseUrl.TrimEnd('/')}/api/{apiVersion.TrimStart('/').TrimEnd('/')}/{endpoint.TrimStart('/')}"; + return $"{baseUrl.TrimEnd('/')}/{API_PATH}/{apiVersion.TrimStart('/').TrimEnd('/')}/{endpoint.TrimStart('/')}"; } /// @@ -86,57 +94,93 @@ public string GetAuthUrl(string path = "") return GetFullUrl($"{authEndpoint}/{path.TrimStart('/')}"); } + #endregion + + #region Factory Methods + /// - /// 환경별 설정을 위한 팩토리 메서드 + /// 개발 환경 설정 생성 /// public static ApiConfig CreateDevelopmentConfig() { var config = CreateInstance(); - config.baseUrl = "http://localhost:7900"; - config.apiVersion = "v1"; - config.timeout = 10f; - config.maxRetryCount = 1; - config.retryDelay = 0.5f; - config.clientId = "unity-client-dev"; - config.clientSecret = "dev-secret-key"; - config.contentType = "application/json"; - config.userAgent = "ProjectVG-Unity-Client/1.0"; - - // 엔드포인트 설정 - config.userEndpoint = "users"; - config.characterEndpoint = "characters"; - config.conversationEndpoint = "conversations"; - config.authEndpoint = "auth"; - + ConfigureDevelopmentSettings(config); return config; } /// - /// 환경별 설정을 위한 팩토리 메서드 + /// 프로덕션 환경 설정 생성 /// public static ApiConfig CreateProductionConfig() { var config = CreateInstance(); - config.baseUrl = "http://122.153.130.223:7900"; - config.apiVersion = "v1"; - config.timeout = 30f; - config.maxRetryCount = 3; - config.retryDelay = 1f; + ConfigureProductionSettings(config); return config; } /// - /// 환경별 설정을 위한 팩토리 메서드 + /// 테스트 환경 설정 생성 /// public static ApiConfig CreateTestConfig() { var config = CreateInstance(); + ConfigureTestSettings(config); + return config; + } + + #endregion + + #region Private Configuration Methods + + private static void ConfigureDevelopmentSettings(ApiConfig config) + { + config.baseUrl = "http://localhost:7901"; + config.apiVersion = "v1"; + config.timeout = 10f; + config.maxRetryCount = 1; + config.retryDelay = 0.5f; + config.clientId = DEFAULT_CLIENT_ID; + config.clientSecret = DEFAULT_CLIENT_SECRET; + config.contentType = "application/json"; + config.userAgent = DEFAULT_USER_AGENT; + + ConfigureDefaultEndpoints(config); + } + + private static void ConfigureProductionSettings(ApiConfig config) + { config.baseUrl = "http://122.153.130.223:7900"; config.apiVersion = "v1"; + config.timeout = 30f; + config.maxRetryCount = 3; + config.retryDelay = 1f; + config.contentType = "application/json"; + config.userAgent = "ProjectVG-Client/1.0"; + + ConfigureDefaultEndpoints(config); + } + + private static void ConfigureTestSettings(ApiConfig config) + { + config.baseUrl = "http://122.153.130.223:7901"; + config.apiVersion = "v1"; config.timeout = 15f; config.maxRetryCount = 2; config.retryDelay = 0.5f; - return config; + config.contentType = "application/json"; + config.userAgent = "ProjectVG-Client/1.0"; + + ConfigureDefaultEndpoints(config); } + + private static void ConfigureDefaultEndpoints(ApiConfig config) + { + config.userEndpoint = "users"; + config.characterEndpoint = "characters"; + config.conversationEndpoint = "conversations"; + config.authEndpoint = "auth"; + } + + #endregion } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs b/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs index 047047c..0165322 100644 --- a/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs +++ b/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs @@ -1,6 +1,5 @@ using UnityEngine; using System; -using System.Text; namespace ProjectVG.Infrastructure.Network.Configs { @@ -11,7 +10,7 @@ namespace ProjectVG.Infrastructure.Network.Configs public class WebSocketConfig : ScriptableObject { [Header("WebSocket Server Configuration")] - [SerializeField] private string baseUrl = "ws://MTIyLjE1My4xMzAuMjIzOjc5MDA="; // base64 encoded: 122.153.130.223:7900 + [SerializeField] private string baseUrl = "http://122.153.130.223:7900"; [SerializeField] private string wsPath = "ws"; [SerializeField] private string apiVersion = "v1"; @@ -33,7 +32,7 @@ public class WebSocketConfig : ScriptableObject [SerializeField] private string contentType = "application/json"; // Properties - public string BaseUrl => DecodeBase64Url(baseUrl); + public string BaseUrl => ConvertToWebSocketUrl(baseUrl); public string WsPath => wsPath; public string ApiVersion => apiVersion; public float Timeout => timeout; @@ -53,8 +52,8 @@ public class WebSocketConfig : ScriptableObject /// public string GetWebSocketUrl() { - var decodedBaseUrl = DecodeBase64Url(baseUrl); - return $"{decodedBaseUrl.TrimEnd('/')}/{wsPath.TrimStart('/').TrimEnd('/')}"; + var wsBaseUrl = ConvertToWebSocketUrl(baseUrl); + return $"{wsBaseUrl.TrimEnd('/')}/{wsPath.TrimStart('/').TrimEnd('/')}"; } /// @@ -62,8 +61,8 @@ public string GetWebSocketUrl() /// public string GetWebSocketUrlWithVersion() { - var decodedBaseUrl = DecodeBase64Url(baseUrl); - return $"{decodedBaseUrl.TrimEnd('/')}/api/{apiVersion.TrimStart('/').TrimEnd('/')}/{wsPath.TrimStart('/').TrimEnd('/')}"; + var wsBaseUrl = ConvertToWebSocketUrl(baseUrl); + return $"{wsBaseUrl.TrimEnd('/')}/api/{apiVersion.TrimStart('/').TrimEnd('/')}/{wsPath.TrimStart('/').TrimEnd('/')}"; } /// @@ -76,25 +75,24 @@ public string GetWebSocketUrlWithSession(string sessionId) } /// - /// Base64 인코딩된 URL 디코딩 + /// URL을 WebSocket URL로 변환 /// - private string DecodeBase64Url(string encodedUrl) + private string ConvertToWebSocketUrl(string httpUrl) { try { - if (encodedUrl.StartsWith("ws://") || encodedUrl.StartsWith("wss://")) + if (httpUrl.StartsWith("http://") || httpUrl.StartsWith("https://")) { - var protocol = encodedUrl.Substring(0, encodedUrl.IndexOf("://") + 3); - var encodedPart = encodedUrl.Substring(encodedUrl.IndexOf("://") + 3); - var decodedPart = Encoding.UTF8.GetString(Convert.FromBase64String(encodedPart)); - return protocol + decodedPart; + // HTTP URL을 WebSocket URL로 변환 + var wsUrl = httpUrl.Replace("http://", "ws://").Replace("https://", "wss://"); + return wsUrl; } - return encodedUrl; + return httpUrl; } catch (Exception ex) { - Debug.LogError($"WebSocket URL 디코딩 실패: {ex.Message}"); - return "ws://localhost:7900"; // Fallback + Debug.LogError($"WebSocket URL 변환 실패: {ex.Message}"); + return "ws://localhost:7901"; // Fallback } } @@ -106,7 +104,7 @@ private string DecodeBase64Url(string encodedUrl) public static WebSocketConfig CreateDevelopmentConfig() { var config = CreateInstance(); - config.baseUrl = "ws://bG9jYWxob3N0Ojc5MDA="; // base64 encoded: localhost:7900 + config.baseUrl = "http://localhost:7901"; // HTTP 사용 config.wsPath = "ws"; config.apiVersion = "v1"; config.timeout = 10f; @@ -127,7 +125,7 @@ public static WebSocketConfig CreateDevelopmentConfig() public static WebSocketConfig CreateProductionConfig() { var config = CreateInstance(); - config.baseUrl = "ws://MTIyLjE1My4xMzAuMjIzOjc5MDA="; // base64 encoded: 122.153.130.223:7900 + config.baseUrl = "http://122.153.130.223:7900"; // HTTP 사용 config.wsPath = "ws"; config.apiVersion = "v1"; config.timeout = 30f; @@ -148,7 +146,7 @@ public static WebSocketConfig CreateProductionConfig() public static WebSocketConfig CreateTestConfig() { var config = CreateInstance(); - config.baseUrl = "ws://MTIyLjE1My4xMzAuMjIzOjc5MDA="; // base64 encoded: 122.153.130.223:7900 + config.baseUrl = "http://122.153.130.223:7900"; // HTTP 사용 config.wsPath = "ws"; config.apiVersion = "v1"; config.timeout = 15f; diff --git a/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs index e27d034..247f357 100644 --- a/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs +++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs @@ -1,19 +1,37 @@ using System; using UnityEngine; +using Newtonsoft.Json; namespace ProjectVG.Infrastructure.Network.DTOs.Chat { /// - /// 채팅 요청 DTO + /// 채팅 요청 DTO (Newtonsoft.Json을 사용하여 snake_case 지원) /// [Serializable] public class ChatRequest { + [JsonProperty("session_id")] [SerializeField] public string sessionId; - [SerializeField] public string actor; + + [JsonProperty("message")] [SerializeField] public string message; - [SerializeField] public string action = "chat"; + + [JsonProperty("character_id")] [SerializeField] public string characterId; + + [JsonProperty("user_id")] [SerializeField] public string userId; + + [JsonProperty("action")] + [SerializeField] public string action = "chat"; + + [JsonProperty("actor")] + [SerializeField] public string actor; + + [JsonProperty("instruction")] + [SerializeField] public string instruction; + + [JsonProperty("requested_at")] + [SerializeField] public string requestedAt; } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Http/HttpApiClient.cs b/Assets/Infrastructure/Network/Http/HttpApiClient.cs index 1aefbbb..31c7d98 100644 --- a/Assets/Infrastructure/Network/Http/HttpApiClient.cs +++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs @@ -6,6 +6,7 @@ using Cysharp.Threading.Tasks; using System.Threading; using ProjectVG.Infrastructure.Network.Configs; +using Newtonsoft.Json; namespace ProjectVG.Infrastructure.Network.Http { @@ -17,7 +18,7 @@ public class HttpApiClient : MonoBehaviour { [Header("API Configuration")] [SerializeField] private ApiConfig apiConfig; - [SerializeField] private float timeout = 30f; + [SerializeField] private float timeout = 60f; [SerializeField] private int maxRetryCount = 3; [SerializeField] private float retryDelay = 1f; @@ -25,62 +26,33 @@ public class HttpApiClient : MonoBehaviour [SerializeField] private string contentType = "application/json"; [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; + private const string DEFAULT_BASE_URL = "http://122.153.130.223:7900"; + private const string DEFAULT_API_VERSION = "v1"; + private const string ACCEPT_HEADER = "application/json"; + private const string AUTHORIZATION_HEADER = "Authorization"; + private const string BEARER_PREFIX = "Bearer "; + private readonly Dictionary defaultHeaders = new Dictionary(); private CancellationTokenSource cancellationTokenSource; public static HttpApiClient Instance { get; private set; } + #region Unity Lifecycle + private void Awake() { - if (Instance == null) - { - Instance = this; - DontDestroyOnLoad(gameObject); - InitializeClient(); - } - else - { - Destroy(gameObject); - } + InitializeSingleton(); } private void OnDestroy() { - cancellationTokenSource?.Cancel(); - cancellationTokenSource?.Dispose(); + Cleanup(); } - private void InitializeClient() - { - cancellationTokenSource = new CancellationTokenSource(); - - if (apiConfig == null) - { - Debug.LogWarning("ApiConfig가 설정되지 않았습니다. 기본값을 사용합니다."); - SetupDefaultHeaders(); - return; - } - - timeout = apiConfig.Timeout; - maxRetryCount = apiConfig.MaxRetryCount; - retryDelay = apiConfig.RetryDelay; - contentType = apiConfig.ContentType; - userAgent = apiConfig.UserAgent; - - SetupDefaultHeaders(); - } + #endregion - private void SetupDefaultHeaders() - { - defaultHeaders.Clear(); - defaultHeaders["Content-Type"] = contentType; - defaultHeaders["User-Agent"] = userAgent; - defaultHeaders["Accept"] = "application/json"; - } + #region Public API - /// - /// ApiConfig 설정 (런타임에서 동적으로 변경 가능) - /// public void SetApiConfig(ApiConfig config) { apiConfig = config; @@ -94,17 +66,7 @@ public void AddDefaultHeader(string key, string value) public void SetAuthToken(string token) { - AddDefaultHeader("Authorization", $"Bearer {token}"); - } - - private string GetFullUrl(string endpoint) - { - if (apiConfig != null) - { - return apiConfig.GetFullUrl(endpoint); - } - - return $"http://122.153.130.223:7900/api/v1/{endpoint.TrimStart('/')}"; + AddDefaultHeader(AUTHORIZATION_HEADER, $"{BEARER_PREFIX}{token}"); } public async UniTask GetAsync(string endpoint, Dictionary headers = null, CancellationToken cancellationToken = default) @@ -116,14 +78,15 @@ public async UniTask GetAsync(string endpoint, Dictionary public async UniTask PostAsync(string endpoint, object data = null, Dictionary headers = null, CancellationToken cancellationToken = default) { var url = GetFullUrl(endpoint); - var jsonData = data != null ? JsonUtility.ToJson(data) : null; + var jsonData = SerializeData(data); + LogRequestDetails("POST", url, jsonData); return await SendRequestAsync(url, UnityWebRequest.kHttpVerbPOST, jsonData, headers, cancellationToken); } public async UniTask PutAsync(string endpoint, object data = null, Dictionary headers = null, CancellationToken cancellationToken = default) { var url = GetFullUrl(endpoint); - var jsonData = data != null ? JsonUtility.ToJson(data) : null; + var jsonData = SerializeData(data); return await SendRequestAsync(url, UnityWebRequest.kHttpVerbPUT, jsonData, headers, cancellationToken); } @@ -139,9 +102,94 @@ public async UniTask UploadFileAsync(string endpoint, byte[] fileData, str return await SendFileRequestAsync(url, fileData, fileName, fieldName, headers, cancellationToken); } + public void Shutdown() + { + cancellationTokenSource?.Cancel(); + } + + #endregion + + #region Private Methods + + private void InitializeSingleton() + { + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + InitializeClient(); + } + else + { + Destroy(gameObject); + } + } + + private void InitializeClient() + { + cancellationTokenSource = new CancellationTokenSource(); + + if (apiConfig == null) + { + Debug.LogWarning("ApiConfig가 설정되지 않았습니다. 기본값을 사용합니다."); + SetupDefaultHeaders(); + return; + } + + ApplyApiConfig(); + SetupDefaultHeaders(); + } + + private void ApplyApiConfig() + { + timeout = apiConfig.Timeout; + maxRetryCount = apiConfig.MaxRetryCount; + retryDelay = apiConfig.RetryDelay; + contentType = apiConfig.ContentType; + userAgent = apiConfig.UserAgent; + } + + private void SetupDefaultHeaders() + { + defaultHeaders.Clear(); + defaultHeaders["Content-Type"] = contentType; + defaultHeaders["User-Agent"] = userAgent; + defaultHeaders["Accept"] = ACCEPT_HEADER; + } + + private string GetFullUrl(string endpoint) + { + if (apiConfig != null) + { + return apiConfig.GetFullUrl(endpoint); + } + + return $"{DEFAULT_BASE_URL}/api/{DEFAULT_API_VERSION}/{endpoint.TrimStart('/')}"; + } + + private string SerializeData(object data) + { + return data != null ? JsonConvert.SerializeObject(data) : null; + } + + private void LogRequestDetails(string method, string url, string jsonData) + { + Debug.Log($"HTTP {method} 요청 URL: {url}"); + if (!string.IsNullOrEmpty(jsonData)) + { + Debug.Log($"HTTP 요청 데이터: {jsonData}"); + } + } + private async UniTask SendRequestAsync(string url, string method, string jsonData, Dictionary headers, CancellationToken cancellationToken) { - var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token).Token; + var combinedCancellationToken = CreateCombinedCancellationToken(cancellationToken); + + Debug.Log($"HTTP 요청 시작: {method} {url}"); + if (!string.IsNullOrEmpty(jsonData)) + { + Debug.Log($"HTTP 요청 데이터: {jsonData}"); + } for (int attempt = 0; attempt <= maxRetryCount; attempt++) { @@ -149,25 +197,18 @@ private async UniTask SendRequestAsync(string url, string method, string j { using var request = CreateRequest(url, method, jsonData, headers); + Debug.Log($"HTTP 요청 전송 중... (시도 {attempt + 1}/{maxRetryCount + 1})"); var operation = request.SendWebRequest(); await operation.WithCancellation(combinedCancellationToken); if (request.result == UnityWebRequest.Result.Success) { + Debug.Log($"HTTP 요청 성공: {request.responseCode}"); return ParseResponse(request); } else { - var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); - - if (ShouldRetry(request.responseCode) && attempt < maxRetryCount) - { - Debug.LogWarning($"API 요청 실패 (시도 {attempt + 1}/{maxRetryCount + 1}): {error.Message}"); - await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: combinedCancellationToken); - continue; - } - - throw error; + await HandleRequestFailure(request, attempt, combinedCancellationToken); } } catch (OperationCanceledException) @@ -176,13 +217,7 @@ private async UniTask SendRequestAsync(string url, string method, string j } catch (Exception ex) when (ex is not ApiException) { - if (attempt < maxRetryCount) - { - Debug.LogWarning($"API 요청 예외 발생 (시도 {attempt + 1}/{maxRetryCount + 1}): {ex.Message}"); - await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: combinedCancellationToken); - continue; - } - throw new ApiException($"{maxRetryCount + 1}번 시도 후 요청 실패", 0, ex.Message); + await HandleRequestException(ex, attempt, combinedCancellationToken); } } @@ -191,7 +226,7 @@ private async UniTask SendRequestAsync(string url, string method, string j private async UniTask SendFileRequestAsync(string url, byte[] fileData, string fileName, string fieldName, Dictionary headers, CancellationToken cancellationToken) { - var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token).Token; + var combinedCancellationToken = CreateCombinedCancellationToken(cancellationToken); for (int attempt = 0; attempt <= maxRetryCount; attempt++) { @@ -213,16 +248,7 @@ private async UniTask SendFileRequestAsync(string url, byte[] fileData, st } else { - var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); - - if (ShouldRetry(request.responseCode) && attempt < maxRetryCount) - { - Debug.LogWarning($"파일 업로드 실패 (시도 {attempt + 1}/{maxRetryCount + 1}): {error.Message}"); - await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: combinedCancellationToken); - continue; - } - - throw error; + await HandleFileUploadFailure(request, attempt, combinedCancellationToken); } } catch (OperationCanceledException) @@ -231,19 +257,69 @@ private async UniTask SendFileRequestAsync(string url, byte[] fileData, st } catch (Exception ex) when (ex is not ApiException) { - if (attempt < maxRetryCount) - { - Debug.LogWarning($"파일 업로드 예외 발생 (시도 {attempt + 1}/{maxRetryCount + 1}): {ex.Message}"); - await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: combinedCancellationToken); - continue; - } - throw new ApiException($"{maxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, ex.Message); + await HandleFileUploadException(ex, attempt, combinedCancellationToken); } } throw new ApiException($"{maxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, "최대 재시도 횟수 초과"); } + private CancellationToken CreateCombinedCancellationToken(CancellationToken cancellationToken) + { + return CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token).Token; + } + + private async UniTask HandleRequestFailure(UnityWebRequest request, int attempt, CancellationToken cancellationToken) + { + var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); + Debug.LogError($"HTTP 요청 실패: {request.result}, 상태코드: {request.responseCode}, 오류: {request.error}"); + + if (ShouldRetry(request.responseCode) && attempt < maxRetryCount) + { + Debug.LogWarning($"API 요청 실패 (시도 {attempt + 1}/{maxRetryCount + 1}): {error.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: cancellationToken); + return; + } + + throw error; + } + + private async UniTask HandleRequestException(Exception ex, int attempt, CancellationToken cancellationToken) + { + if (attempt < maxRetryCount) + { + Debug.LogWarning($"API 요청 예외 발생 (시도 {attempt + 1}/{maxRetryCount + 1}): {ex.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: cancellationToken); + return; + } + throw new ApiException($"{maxRetryCount + 1}번 시도 후 요청 실패", 0, ex.Message); + } + + private async UniTask HandleFileUploadFailure(UnityWebRequest request, int attempt, CancellationToken cancellationToken) + { + var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); + + if (ShouldRetry(request.responseCode) && attempt < maxRetryCount) + { + Debug.LogWarning($"파일 업로드 실패 (시도 {attempt + 1}/{maxRetryCount + 1}): {error.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: cancellationToken); + return; + } + + throw error; + } + + private async UniTask HandleFileUploadException(Exception ex, int attempt, CancellationToken cancellationToken) + { + if (attempt < maxRetryCount) + { + Debug.LogWarning($"파일 업로드 예외 발생 (시도 {attempt + 1}/{maxRetryCount + 1}): {ex.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: cancellationToken); + return; + } + throw new ApiException($"{maxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, ex.Message); + } + private UnityWebRequest CreateRequest(string url, string method, string jsonData, Dictionary headers) { var request = new UnityWebRequest(url, method); @@ -288,20 +364,23 @@ private T ParseResponse(UnityWebRequest request) try { - // Newtonsoft.Json 사용 (snake_case 지원) - return Newtonsoft.Json.JsonConvert.DeserializeObject(responseText); + return JsonConvert.DeserializeObject(responseText); } catch (Exception ex) { - // Newtonsoft.Json 실패 시 Unity JsonUtility로 폴백 - try - { - return JsonUtility.FromJson(responseText); - } - catch (Exception fallbackEx) - { - throw new ApiException($"응답 파싱 실패: {ex.Message} (폴백도 실패: {fallbackEx.Message})", request.responseCode, responseText); - } + return TryFallbackParse(responseText, request.responseCode, ex); + } + } + + private T TryFallbackParse(string responseText, long responseCode, Exception originalException) + { + try + { + return JsonUtility.FromJson(responseText); + } + catch (Exception fallbackEx) + { + throw new ApiException($"응답 파싱 실패: {originalException.Message} (폴백도 실패: {fallbackEx.Message})", responseCode, responseText); } } @@ -310,10 +389,13 @@ private bool ShouldRetry(long responseCode) return responseCode >= 500 || responseCode == 429; } - public void Shutdown() + private void Cleanup() { cancellationTokenSource?.Cancel(); + cancellationTokenSource?.Dispose(); } + + #endregion } /// diff --git a/Assets/Infrastructure/Network/NetworkTestManager.cs.meta b/Assets/Infrastructure/Network/NetworkTestManager.cs.meta deleted file mode 100644 index b47c067..0000000 --- a/Assets/Infrastructure/Network/NetworkTestManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: fca7c331eb199ca4988f589a0cfefffe \ No newline at end of file diff --git a/Assets/Infrastructure/Network/NetworkTestUI.cs.meta b/Assets/Infrastructure/Network/NetworkTestUI.cs.meta deleted file mode 100644 index 4ed77f3..0000000 --- a/Assets/Infrastructure/Network/NetworkTestUI.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f6ca4fabfdceafa479759e863d482a18 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Services/ChatApiService.cs b/Assets/Infrastructure/Network/Services/ChatApiService.cs index 579db8a..f6ec055 100644 --- a/Assets/Infrastructure/Network/Services/ChatApiService.cs +++ b/Assets/Infrastructure/Network/Services/ChatApiService.cs @@ -1,8 +1,10 @@ +using System; using System.Threading; using UnityEngine; using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.Http; using ProjectVG.Infrastructure.Network.DTOs.Chat; +using Newtonsoft.Json; namespace ProjectVG.Infrastructure.Network.Services { @@ -12,31 +14,30 @@ namespace ProjectVG.Infrastructure.Network.Services public class ChatApiService { private readonly HttpApiClient _httpClient; + private const string CHAT_ENDPOINT = "chat"; + private const string DEFAULT_ACTION = "chat"; public ChatApiService() { _httpClient = HttpApiClient.Instance; - if (_httpClient == null) - { - Debug.LogError("HttpApiClient.Instance가 null입니다. HttpApiClient가 생성되지 않았습니다."); - } + ValidateHttpClient(); } /// - /// 채팅 요청을 큐에 등록 + /// 채팅 요청 전송 /// /// 채팅 요청 데이터 /// 취소 토큰 /// 채팅 응답 public async UniTask SendChatAsync(ChatRequest request, CancellationToken cancellationToken = default) { - if (_httpClient == null) - { - Debug.LogError("HttpApiClient가 null입니다. 초기화를 확인해주세요."); - return null; - } + ValidateRequest(request); + ValidateHttpClient(); - return await _httpClient.PostAsync("chat", request, cancellationToken: cancellationToken); + var serverRequest = CreateServerRequest(request); + LogRequestDetails(serverRequest); + + return await _httpClient.PostAsync(CHAT_ENDPOINT, serverRequest, cancellationToken: cancellationToken); } /// @@ -57,15 +58,78 @@ public async UniTask SendChatAsync( string actor = null, CancellationToken cancellationToken = default) { - var request = new ChatRequest + var request = CreateSimpleRequest(message, characterId, userId, sessionId, actor); + return await SendChatAsync(request, cancellationToken); + } + + #region Private Methods + + private void ValidateHttpClient() + { + if (_httpClient == null) + { + throw new InvalidOperationException("HttpApiClient.Instance가 null입니다. HttpApiClient가 생성되지 않았습니다."); + } + } + + private void ValidateRequest(ChatRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), "채팅 요청이 null입니다."); + } + + if (string.IsNullOrEmpty(request.message)) + { + throw new ArgumentException("메시지가 비어있습니다.", nameof(request.message)); + } + + if (string.IsNullOrEmpty(request.characterId)) + { + throw new ArgumentException("캐릭터 ID가 비어있습니다.", nameof(request.characterId)); + } + + if (string.IsNullOrEmpty(request.userId)) + { + throw new ArgumentException("사용자 ID가 비어있습니다.", nameof(request.userId)); + } + } + + private ChatRequest CreateServerRequest(ChatRequest originalRequest) + { + return new ChatRequest + { + sessionId = originalRequest.sessionId, + message = originalRequest.message, + characterId = originalRequest.characterId, + userId = originalRequest.userId, + action = originalRequest.action, + actor = originalRequest.actor, + instruction = originalRequest.instruction, + requestedAt = originalRequest.requestedAt + }; + } + + private ChatRequest CreateSimpleRequest(string message, string characterId, string userId, string sessionId, string actor) + { + return new ChatRequest { sessionId = sessionId, message = message, characterId = characterId, - userId = userId + userId = userId, + actor = actor, + action = DEFAULT_ACTION, + requestedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }; + } - return await SendChatAsync(request, cancellationToken); + private void LogRequestDetails(ChatRequest request) + { + Debug.Log($"채팅 요청 엔드포인트: {CHAT_ENDPOINT}"); + Debug.Log($"서버로 전송할 JSON: {JsonConvert.SerializeObject(request)}"); } + + #endregion } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity b/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity deleted file mode 100644 index 3af233a..0000000 --- a/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity +++ /dev/null @@ -1,257 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!29 &1 -OcclusionCullingSettings: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_OcclusionBakeSettings: - smallestOccluder: 5 - smallestHole: 0.25 - backfaceThreshold: 100 - m_SceneGUID: 00000000000000000000000000000000 - m_OcclusionCullingData: {fileID: 0} ---- !u!104 &2 -RenderSettings: - m_ObjectHideFlags: 0 - serializedVersion: 9 - m_Fog: 0 - m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} - m_FogMode: 3 - m_FogDensity: 0.01 - m_LinearFogStart: 0 - m_LinearFogEnd: 300 - m_AmbientMode: 0 - m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} - m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} - m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} - m_AmbientIntensity: 1 - m_AmbientMode: 3 - m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} - m_SkyboxMaterial: {fileID: 0} - m_HaloStrength: 0.5 - m_FlareStrength: 1 - m_FlareFadeSpeed: 3 - m_HaloTexture: {fileID: 0} - m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} - m_DefaultReflectionMode: 0 - m_DefaultReflectionResolution: 128 - m_ReflectionBounces: 1 - m_ReflectionIntensity: 1 - m_CustomReflection: {fileID: 0} - m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} - m_UseRadianceAmbientProbe: 0 ---- !u!157 &3 -LightmapSettings: - m_ObjectHideFlags: 0 - serializedVersion: 12 - m_GIWorkflowMode: 1 - m_GISettings: - serializedVersion: 2 - m_BounceScale: 1 - m_IndirectOutputScale: 1 - m_AlbedoBoost: 1 - m_EnvironmentLightingMode: 0 - m_EnableBakedLightmaps: 0 - m_EnableRealtimeLightmaps: 0 - m_LightmapEditorSettings: - serializedVersion: 12 - m_Resolution: 2 - m_BakeResolution: 40 - m_AtlasSize: 1024 - m_AO: 0 - m_AOMaxDistance: 1 - m_CompAOExponent: 1 - m_CompAOExponentDirect: 0 - m_ExtractAmbientOcclusion: 0 - m_Padding: 2 - m_LightmapParameters: {fileID: 0} - m_LightmapsBakeMode: 1 - m_TextureCompression: 1 - m_FinalGather: 0 - m_FinalGatherFiltering: 1 - m_FinalGatherRayCount: 256 - m_ReflectionCompression: 2 - m_MixedBakeMode: 2 - m_BakeBackend: 1 - m_PVRSampling: 1 - m_PVRDirectSampleCount: 32 - m_PVRSampleCount: 512 - m_PVRBounces: 2 - m_PVREnvironmentSampleCount: 256 - m_PVREnvironmentReferencePointCount: 2048 - m_PVRFilteringMode: 1 - m_PVRDenoiserTypeDirect: 1 - m_PVRDenoiserTypeIndirect: 1 - m_PVRDenoiserTypeAO: 1 - m_PVRFilterTypeDirect: 0 - m_PVRFilterTypeIndirect: 0 - m_PVRFilterTypeAO: 0 - m_PVREnvironmentMIS: 1 - m_PVRCulling: 1 - m_PVRFilteringGaussRadiusDirect: 1 - m_PVRFilteringGaussRadiusIndirect: 5 - m_PVRFilteringGaussRadiusAO: 2 - m_PVRFilteringAtrousPositionSigmaDirect: 0.5 - m_PVRFilteringAtrousPositionSigmaIndirect: 2 - m_PVRFilteringAtrousPositionSigmaAO: 1 - m_ExportTrainingData: 0 - m_TrainingDataDestination: TrainingData - m_LightProbeSampleCountMultiplier: 4 - m_LightingDataAsset: {fileID: 0} - m_LightingSettings: {fileID: 0} ---- !u!196 &4 -NavMeshSettings: - serializedVersion: 2 - m_ObjectHideFlags: 0 - m_BuildSettings: - serializedVersion: 2 - agentTypeID: 0 - agentRadius: 0.5 - agentHeight: 2 - agentSlope: 45 - agentClimb: 0.4 - ledgeDropHeight: 0 - maxJumpAcrossDistance: 0 - minRegionArea: 2 - manualCellSize: 0 - cellSize: 0.16666667 - manualTileSize: 0 - tileSize: 256 - accuratePlacement: 0 - maxJobWorkers: 0 - preserveTilesOutsideBounds: 0 - debug: - m_Flags: 0 - m_NavMeshData: {fileID: 0} ---- !u!1 &519420028 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 519420032} - - component: {fileID: 519420031} - - component: {fileID: 519420029} - m_Layer: 0 - m_Name: Main Camera - m_TagString: MainCamera - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!81 &519420029 -AudioListener: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 519420028} - m_Enabled: 1 ---- !u!20 &519420031 -Camera: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 519420028} - m_Enabled: 1 - serializedVersion: 2 - m_ClearFlags: 1 - m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} - m_projectionMatrixMode: 1 - m_GateFitMode: 2 - m_FOVAxisMode: 0 - m_SensorSize: {x: 36, y: 24} - m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 - m_NormalizedViewPortRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 1 - height: 1 - m_near: 0.3 - m_far: 1000 - m_fieldOfView: 60 - m_orthographic: 0 - m_orthographicSize: 5 - m_Depth: -1 - m_CullingMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_RenderingPath: -1 - m_TargetTexture: {fileID: 0} - m_TargetDisplay: 0 - m_TargetEye: 3 - m_HDR: 1 - m_AllowMSAA: 1 - m_AllowDynamicResolution: 0 - m_ForceIntoRT: 0 - m_OcclusionCulling: 1 - m_StereoConvergence: 10 - m_StereoSeparation: 0.022 ---- !u!4 &519420032 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 519420028} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 1, z: -10} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &1234567890 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1234567891} - - component: {fileID: 1234567892} - m_Layer: 0 - m_Name: NetworkTestManager - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &1234567891 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1234567890} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 1 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &1234567892 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1234567890} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 00000000000000000000000000000000, type: 3} - m_Name: - m_EditorClassIdentifier: - testSessionId: test-session-123 - testCharacterId: test-character-456 - testUserId: test-user-789 - autoTest: 0 - testInterval: 5 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs new file mode 100644 index 0000000..b533581 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs @@ -0,0 +1,219 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms +{ + /// + /// 실제 WebSocket 연결을 수행하는 구현체 + /// + public class RealWebSocket : INativeWebSocket + { + public bool IsConnected { get; private set; } + public bool IsConnecting { get; private set; } + + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; + public event Action OnMessageReceived; + public event Action OnBinaryDataReceived; + + private ClientWebSocket _webSocket; + private CancellationTokenSource _cancellationTokenSource; + private bool _isDisposed = false; + + public RealWebSocket() + { + _webSocket = new ClientWebSocket(); + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) + { + if (IsConnected || IsConnecting) + { + return IsConnected; + } + + IsConnecting = true; + + try + { + Debug.Log($"실제 WebSocket 연결 시도: {url}"); + + // HTTP URL을 WebSocket URL로 변환 (HTTPS 우선 사용) + var wsUrl = url.Replace("http://", "wss://").Replace("https://", "wss://"); + Debug.Log($"변환된 WebSocket URL: {wsUrl}"); + + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + await _webSocket.ConnectAsync(new Uri(wsUrl), combinedCancellationToken); + + IsConnected = true; + IsConnecting = false; + + Debug.Log("실제 WebSocket 연결 성공"); + OnConnected?.Invoke(); + + // 메시지 수신 루프 시작 + _ = ReceiveLoopAsync(); + + return true; + } + catch (Exception ex) + { + IsConnecting = false; + var error = $"WebSocket 연결 실패: {ex.Message}"; + Debug.LogError(error); + OnError?.Invoke(error); + return false; + } + } + + public async UniTask DisconnectAsync() + { + if (!IsConnected) + { + Debug.Log("WebSocket이 이미 연결 해제됨"); + return; + } + + try + { + Debug.Log($"WebSocket 연결 해제 시작 - 상태: {_webSocket.State}"); + IsConnected = false; + IsConnecting = false; + + if (_webSocket.State == WebSocketState.Open) + { + Debug.Log("WebSocket 정상 종료 시도"); + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnect", CancellationToken.None); + } + else + { + Debug.Log($"WebSocket 상태가 Open이 아님: {_webSocket.State}"); + } + + Debug.Log("실제 WebSocket 연결 해제 완료"); + OnDisconnected?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 연결 해제 중 오류: {ex.Message}"); + } + } + + public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + if (!IsConnected || _webSocket.State != WebSocketState.Open) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + var buffer = Encoding.UTF8.GetBytes(message); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); + Debug.Log($"WebSocket 메시지 전송: {message}"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 메시지 전송 실패: {ex.Message}"); + return false; + } + } + + public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) + { + if (!IsConnected || _webSocket.State != WebSocketState.Open) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + await _webSocket.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, cancellationToken); + Debug.Log($"WebSocket 바이너리 전송: {data.Length} bytes"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"WebSocket 바이너리 전송 실패: {ex.Message}"); + return false; + } + } + + private async Task ReceiveLoopAsync() + { + var buffer = new byte[4096]; + + try + { + Debug.Log("WebSocket 수신 루프 시작"); + while (IsConnected && _webSocket.State == WebSocketState.Open) + { + Debug.Log($"WebSocket 상태: {_webSocket.State}, 메시지 대기 중..."); + var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), _cancellationTokenSource.Token); + + Debug.Log($"WebSocket 메시지 수신: 타입={result.MessageType}, 크기={result.Count}, 종료={result.EndOfMessage}"); + + if (result.MessageType == WebSocketMessageType.Close) + { + Debug.Log("서버에서 연결 종료 요청"); + break; + } + else if (result.MessageType == WebSocketMessageType.Text) + { + var message = Encoding.UTF8.GetString(buffer, 0, result.Count); + Debug.Log($"WebSocket 텍스트 메시지 수신: {message}"); + Debug.Log($"메시지 길이: {message.Length}, 내용: '{message}'"); + OnMessageReceived?.Invoke(message); + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + var data = new byte[result.Count]; + Array.Copy(buffer, data, result.Count); + Debug.Log($"WebSocket 바이너리 메시지 수신: {result.Count} bytes"); + OnBinaryDataReceived?.Invoke(data); + } + } + } + catch (Exception ex) + { + if (!_isDisposed) + { + Debug.LogError($"WebSocket 수신 루프 오류: {ex.Message}"); + Debug.LogError($"스택 트레이스: {ex.StackTrace}"); + OnError?.Invoke(ex.Message); + } + } + finally + { + Debug.Log("WebSocket 수신 루프 종료"); + IsConnected = false; + if (!_isDisposed) + { + OnDisconnected?.Invoke(); + } + } + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _webSocket?.Dispose(); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs.meta new file mode 100644 index 0000000..e0f3754 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 543c199ba5388224c8deace64fdcacc6 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs index 52ef80f..f822b36 100644 --- a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs @@ -78,9 +78,9 @@ private void InitializeManager() private void InitializeNativeWebSocket() { - // 시뮬레이션 WebSocket 구현체 사용 - _nativeWebSocket = new UnityWebSocket(); - Debug.Log("WebSocket 시뮬레이션 구현체를 사용합니다."); + // 실제 WebSocket 구현체 사용 + _nativeWebSocket = new RealWebSocket(); + Debug.Log("실제 WebSocket 구현체를 사용합니다."); // 이벤트 연결 _nativeWebSocket.OnConnected += OnNativeConnected; @@ -280,8 +280,10 @@ private void ProcessReceivedMessage(WebSocketMessage baseMessage) switch (baseMessage.type?.ToLower()) { case "session_id": + Debug.Log("세션 ID 메시지 처리 중..."); var sessionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); _sessionId = sessionMessage.session_id; // 세션 ID 저장 + Debug.Log($"세션 ID 저장됨: {_sessionId}"); foreach (var handler in _handlers) { handler.OnSessionIdMessageReceived(sessionMessage); @@ -407,10 +409,13 @@ private void OnNativeConnected() _isConnecting = false; _reconnectAttempts = 0; + Debug.Log("WebSocket 연결 성공 - 핸들러 수: " + _handlers.Count); + // 이벤트 발생 OnConnected?.Invoke(); foreach (var handler in _handlers) { + Debug.Log($"핸들러에게 연결 이벤트 전달: {handler.GetType().Name}"); handler.OnConnected(); } } @@ -448,13 +453,49 @@ private void OnNativeMessageReceived(string message) { try { - // JSON 메시지 파싱 + Debug.Log($"원시 메시지 수신: {message}"); + Debug.Log($"핸들러 수: {_handlers.Count}"); + + // 클라이언트와 동일한 방식으로 세션 ID 메시지 처리 + if (message.Contains("\"type\":\"session_id\"")) + { + Debug.Log("세션 ID 메시지 감지됨"); + + // JSON에서 session_id 추출 + int sessionIdStart = message.IndexOf("\"session_id\":\"") + 14; + int sessionIdEnd = message.IndexOf("\"", sessionIdStart); + if (sessionIdStart > 13 && sessionIdEnd > sessionIdStart) + { + _sessionId = message.Substring(sessionIdStart, sessionIdEnd - sessionIdStart); + Debug.Log($"세션 ID 저장됨: {_sessionId}"); + + // 핸들러들에게 세션 ID 메시지 전달 + var sessionMessage = new SessionIdMessage { session_id = _sessionId }; + foreach (var handler in _handlers) + { + Debug.Log($"핸들러에게 세션 ID 전달: {handler.GetType().Name}"); + handler.OnSessionIdMessageReceived(sessionMessage); + } + return; + } + else + { + Debug.LogError("세션 ID 추출 실패 - JSON 형식 확인 필요"); + } + } + else + { + Debug.Log("세션 ID 메시지가 아님 - 다른 메시지 타입"); + } + + // 기존 방식으로 처리 var baseMessage = JsonUtility.FromJson(message); ProcessReceivedMessage(baseMessage); } catch (Exception ex) { Debug.LogError($"메시지 파싱 실패: {ex.Message}"); + Debug.LogError($"원시 메시지: {message}"); } } diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/Mao.fadeMotionList.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/Mao.fadeMotionList.asset index 09f5f5a..4a530de 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/Mao.fadeMotionList.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/Mao.fadeMotionList.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 403ae2dd693bb1d4b924f6b8d206b053, type: 3} m_Name: Mao.fadeMotionList m_EditorClassIdentifier: - MotionInstanceIds: e0d10000e8d1000042d2000052d20000bed100001cd2000024d20000e4d10000 + MotionInstanceIds: 14d2000020d20000d8d20000e8d20000dad100007ad2000086d2000018d20000 CubismFadeMotionObjects: - {fileID: 11400000, guid: c6a5276089e6b2c4bb2f78cca94c4ded, type: 2} - {fileID: 11400000, guid: a0145e27134640343b894f03870256bc, type: 2} diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.anim index 8083ee5..5e56187 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.anim @@ -11378,5 +11378,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53788 + intParameter: 53882 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.fade.asset index eb31e47..e5459ca 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_01.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/mtn_01.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.anim index 5323a7b..38b146e 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.anim @@ -12098,5 +12098,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53694 + intParameter: 53722 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.fade.asset index 0b70576..a911e59 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_02.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_02.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/mtn_02.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Mao/motions/mtn_02.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.5 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.anim index 8b6dff7..0445509 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.anim @@ -11774,5 +11774,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53736 + intParameter: 53792 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.fade.asset index 65403f3..7f02379 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_03.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_03.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/mtn_03.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Mao/motions/mtn_03.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.5 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.anim index 5f8bca3..1202af8 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.anim @@ -12944,5 +12944,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53796 + intParameter: 53894 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.fade.asset index 76fb1c1..9e8d2a1 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_04.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/mtn_04.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.5 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.anim index 50626d2..c5d4590 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.anim @@ -11168,5 +11168,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53826 + intParameter: 53976 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.fade.asset index b647ce3..8508f79 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: sample_01.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/sample_01.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/sample_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.anim index a8ba80f..7f8f15a 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.anim @@ -15266,5 +15266,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53732 + intParameter: 53784 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.fade.asset index 80566a8..9bc7561 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: special_01.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Mao/motions/special_01.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.25 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.anim index 9a145ff..676e10f 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.anim @@ -17336,5 +17336,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53728 + intParameter: 53780 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.fade.asset index 29d1ffe..121ded3 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_02.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: special_02.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Mao/motions/special_02.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Mao/motions/special_02.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.25 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.anim index 3383509..1d7c7e4 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.anim @@ -18134,5 +18134,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53842 + intParameter: 53992 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.fade.asset index 3788240..84be377 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Mao/motions/special_03.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: special_03.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Mao/motions/special_03.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Mao/motions/special_03.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 0.25 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/Natori.fadeMotionList.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/Natori.fadeMotionList.asset index 4d4da11..20c858a 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/Natori.fadeMotionList.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/Natori.fadeMotionList.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 403ae2dd693bb1d4b924f6b8d206b053, type: 3} m_Name: Natori.fadeMotionList m_EditorClassIdentifier: - MotionInstanceIds: 10d20000d8d10000f8d1000020d200003ad20000d4d1000056d20000bad100002cd20000 + MotionInstanceIds: 5cd200000cd2000032d200007ed20000b4d2000002d20000ecd20000d6d10000a2d20000 CubismFadeMotionObjects: - {fileID: 11400000, guid: b211dc549c58a9e448169cc818f5edfa, type: 2} - {fileID: 11400000, guid: 145e4856478ab1540affc40745dbea64, type: 2} diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.anim index 14196ef..e16506c 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.anim @@ -731,5 +731,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53804 + intParameter: 53922 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.fade.asset index 759eed3..605d5f4 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_00.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_00.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.anim index e828e8c..2cf088d 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.anim @@ -6842,5 +6842,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53792 + intParameter: 53886 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.fade.asset index a96ec73..f069fae 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_01.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_01.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.anim index cfa71b0..44ec12a 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.anim @@ -9107,5 +9107,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53716 + intParameter: 53762 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.fade.asset index 37cd1ec..f698b66 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_02.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_02.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_02.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_02.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.anim index 5731b3f..efd8cdc 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.anim @@ -9668,5 +9668,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53846 + intParameter: 53996 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.fade.asset index 1faf747..02b649f 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_03.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_03.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_03.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_03.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.anim index 09e400b..2485b36 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.anim @@ -10931,5 +10931,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53752 + intParameter: 53810 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.fade.asset index 933b3a7..19c0b38 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_04.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_04.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.anim index 1dd9384..5ca0846 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.anim @@ -10211,5 +10211,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53818 + intParameter: 53940 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.fade.asset index d4faf9d..df66149 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_05.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_05.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_05.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_05.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.anim index a6d7d28..798fcc1 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.anim @@ -10199,5 +10199,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53720 + intParameter: 53772 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.fade.asset index fb5f7d7..98e3237 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_06.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_06.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_06.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_06.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.anim index da79cf3..9d23e6b 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.anim @@ -9533,5 +9533,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53776 + intParameter: 53852 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.fade.asset index e317caf..501f020 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_07.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_07.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_07.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Natori/motions/mtn_07.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.anim b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.anim index d68f5ff..e242e1a 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.anim +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.anim @@ -9083,5 +9083,5 @@ AnimationClip: data: objectReferenceParameter: {fileID: 0} floatParameter: 0 - intParameter: 53690 + intParameter: 53718 messageOptions: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.fade.asset index e2173d1..6b3a873 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_08.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Natori/motions/mtn_08.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Natori/motions/mtn_08.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.fade.asset index 6c34070..655c1d6 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_00.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Rice/motions/mtn_00.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Rice/motions/mtn_00.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_01.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_01.fade.asset index dbac025..cc7b39e 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_01.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_01.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_01.fade m_EditorClassIdentifier: - MotionName: Assets\Live2D\Cubism\Samples\Models\Rice/motions/mtn_01.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Rice/motions/mtn_01.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.fade.asset index 66fc115..c293b7c 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_02.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.motion3.json + MotionName: Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_02.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.fade.asset b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.fade.asset index 52bcdb0..533bdfa 100644 --- a/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.fade.asset +++ b/Assets/Plugins/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.fade.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f1ee1adc36a8a04a8d7c42ac5d6bc27, type: 3} m_Name: mtn_03.fade m_EditorClassIdentifier: - MotionName: Assets/Live2D/Cubism/Samples/Models/Rice/motions/mtn_03.motion3.json + MotionName: Assets\Plugins\Live2D\Cubism\Samples\Models\Rice/motions/mtn_03.motion3.json ModelFadeInTime: -1 ModelFadeOutTime: -1 FadeInTime: 1 diff --git a/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity.meta b/Assets/Tests/Runtime.meta similarity index 67% rename from Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity.meta rename to Assets/Tests/Runtime.meta index 9c723cb..76a563c 100644 --- a/Assets/Infrastructure/Network/TestScene/NetworkTestScene.unity.meta +++ b/Assets/Tests/Runtime.meta @@ -1,5 +1,6 @@ fileFormatVersion: 2 -guid: 4fc900551cce9f9489af0a318a1a71bc +guid: 0412d803e6fb9cd408e6cf0d4a917590 +folderAsset: yes DefaultImporter: externalObjects: {} userData: diff --git a/Assets/Infrastructure/Network/NetworkTestManager.cs b/Assets/Tests/Runtime/NetworkTestManager.cs similarity index 58% rename from Assets/Infrastructure/Network/NetworkTestManager.cs rename to Assets/Tests/Runtime/NetworkTestManager.cs index 9202008..80ab66e 100644 --- a/Assets/Infrastructure/Network/NetworkTestManager.cs +++ b/Assets/Tests/Runtime/NetworkTestManager.cs @@ -5,23 +5,24 @@ using ProjectVG.Infrastructure.Network.WebSocket; using ProjectVG.Infrastructure.Network.Services; using ProjectVG.Infrastructure.Network.Http; +using ProjectVG.Infrastructure.Network.Configs; -namespace ProjectVG.Infrastructure.Network +namespace ProjectVG.Tests.Runtime { /// /// WebSocket + HTTP 통합 테스트 매니저 - /// WebSocket 연결 → HTTP 요청 → WebSocket으로 결과 수신 + /// 더미 클라이언트와 동일한 동작: WebSocket 연결 → 세션 ID 수신 → HTTP 요청 → WebSocket으로 결과 수신 /// public class NetworkTestManager : MonoBehaviour { [Header("테스트 설정")] [SerializeField] private string testSessionId = "test-session-123"; - [SerializeField] private string testCharacterId = "test-character-456"; - [SerializeField] private string testUserId = "test-user-789"; + [SerializeField] private string testCharacterId = "44444444-4444-4444-4444-444444444444"; // 제로 + [SerializeField] private string testUserId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; [Header("자동 테스트")] [SerializeField] private bool autoTest = false; - [SerializeField] private float testInterval = 5f; + [SerializeField] private float testInterval = 10f; // 테스트 완료 시간을 고려하여 10초로 증가 // UI에서 접근할 수 있도록 public 프로퍼티 추가 public bool AutoTest @@ -41,11 +42,19 @@ public float TestInterval private DefaultWebSocketHandler _webSocketHandler; private CancellationTokenSource _cancellationTokenSource; private bool _isTestRunning = false; + private string _receivedSessionId = null; // WebSocket에서 받은 세션 ID + private bool _chatResponseReceived = false; // 채팅 응답 수신 여부 + private string _lastChatResponse = null; // 마지막 채팅 응답 private void Awake() { _cancellationTokenSource = new CancellationTokenSource(); + // HTTP 연결 허용 설정 + #if UNITY_EDITOR || UNITY_STANDALONE + UnityEngine.Networking.UnityWebRequest.ClearCookieCache(); + #endif + // 매니저들이 없으면 생성 EnsureManagersExist(); @@ -122,6 +131,19 @@ private void InitializeManagers() return; } + // WebSocket 설정 적용 (localhost:7900 사용) + var webSocketConfig = ProjectVG.Infrastructure.Network.Configs.WebSocketConfig.CreateDevelopmentConfig(); + _webSocketManager.SetWebSocketConfig(webSocketConfig); + Debug.Log($"WebSocket 설정 적용: {webSocketConfig.GetWebSocketUrl()}"); + + // API 설정 적용 (localhost:7900 사용) + var apiConfig = ProjectVG.Infrastructure.Network.Configs.ApiConfig.CreateDevelopmentConfig(); + if (HttpApiClient.Instance != null) + { + HttpApiClient.Instance.SetApiConfig(apiConfig); + Debug.Log($"API 설정 적용: {apiConfig.GetFullUrl("chat")}"); + } + // API 서비스 매니저 초기화 _apiServiceManager = ApiServiceManager.Instance; if (_apiServiceManager == null) @@ -158,7 +180,7 @@ private void InitializeManagers() #region 수동 테스트 메서드들 - [ContextMenu("1. WebSocket 연결")] + [ContextMenu("1. WebSocket 연결 (더미 클라이언트 방식)")] public async void ConnectWebSocket() { if (_isTestRunning) @@ -175,12 +197,15 @@ public async void ConnectWebSocket() try { - Debug.Log("=== WebSocket 연결 시작 ==="); - bool connected = await _webSocketManager.ConnectAsync(testSessionId); + Debug.Log("=== WebSocket 연결 시작 (더미 클라이언트 방식) ==="); + _receivedSessionId = null; // 세션 ID 초기화 + + // 더미 클라이언트처럼 세션 ID 없이 연결 + bool connected = await _webSocketManager.ConnectAsync(); if (connected) { - Debug.Log("✅ WebSocket 연결 성공!"); + Debug.Log("✅ WebSocket 연결 성공! 세션 ID 대기 중..."); } else { @@ -193,7 +218,7 @@ public async void ConnectWebSocket() } } - [ContextMenu("2. HTTP 채팅 요청")] + [ContextMenu("2. HTTP 채팅 요청 (더미 클라이언트 방식)")] public async void SendChatRequest() { if (_webSocketManager == null || !_webSocketManager.IsConnected) @@ -208,17 +233,26 @@ public async void SendChatRequest() return; } + // 세션 ID가 아직 수신되지 않았으면 대기 + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("세션 ID가 없습니다. WebSocket에서 세션 ID를 먼저 받아야 합니다."); + return; + } + try { - Debug.Log("=== HTTP 채팅 요청 시작 ==="); + Debug.Log("=== HTTP 채팅 요청 시작 (더미 클라이언트 방식) ==="); - var chatRequest = new DTOs.Chat.ChatRequest + var chatRequest = new ProjectVG.Infrastructure.Network.DTOs.Chat.ChatRequest { message = "안녕하세요! 테스트 메시지입니다.", characterId = testCharacterId, userId = testUserId, - sessionId = testSessionId, - actor = "web_user" + sessionId = _receivedSessionId, // WebSocket에서 받은 세션 ID 사용 + actor = "web_user", + action = "chat", // 클라이언트와 동일하게 명시적으로 설정 + requestedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }; var response = await _apiServiceManager.Chat.SendChatAsync(chatRequest); @@ -226,6 +260,9 @@ public async void SendChatRequest() if (response != null && response.success) { Debug.Log($"✅ HTTP 채팅 요청 성공! 응답: {response.message}"); + Debug.Log($" - 세션 ID: {_receivedSessionId}"); + Debug.Log($" - 캐릭터 ID: {testCharacterId}"); + Debug.Log($" - 사용자 ID: {testUserId}"); } else { @@ -308,6 +345,7 @@ public async void DisconnectWebSocket() { Debug.Log("=== WebSocket 연결 해제 시작 ==="); await _webSocketManager.DisconnectAsync(); + _receivedSessionId = null; // 세션 ID 초기화 Debug.Log("✅ WebSocket 연결 해제 완료!"); } catch (Exception ex) @@ -316,6 +354,95 @@ public async void DisconnectWebSocket() } } + [ContextMenu("더미 클라이언트 방식 전체 테스트")] + public async void RunDummyClientTest() + { + if (_isTestRunning) + { + Debug.LogWarning("테스트가 이미 실행 중입니다."); + return; + } + + _isTestRunning = true; + + try + { + Debug.Log("🚀 === 더미 클라이언트 방식 전체 테스트 시작 ==="); + + // 0. 기존 연결이 있으면 해제 + if (_webSocketManager.IsConnected) + { + Debug.Log("0️⃣ 기존 연결 해제 중..."); + await _webSocketManager.DisconnectAsync(); + await UniTask.Delay(1000); // 연결 해제 완료 대기 + } + + // 1. WebSocket 연결 (세션 ID 없이) + Debug.Log("1️⃣ WebSocket 연결 중..."); + bool connected = await _webSocketManager.ConnectAsync(); + if (!connected) + { + Debug.LogError("WebSocket 연결 실패로 테스트 중단"); + return; + } + + // 2. 세션 ID 수신 대기 (최대 10초) + Debug.Log("2️⃣ 세션 ID 수신 대기 중..."); + int waitCount = 0; + while (string.IsNullOrEmpty(_receivedSessionId) && waitCount < 100) + { + await UniTask.Delay(100); + waitCount++; + if (waitCount % 10 == 0) // 1초마다 로그 + { + Debug.Log($"2️⃣ 세션 ID 대기 중... ({waitCount/10}초 경과)"); + } + } + + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("세션 ID를 받지 못했습니다. (10초 타임아웃)"); + Debug.LogWarning("서버에서 세션 ID 메시지를 보내지 않았거나, 메시지 형식이 다를 수 있습니다."); + return; + } + + Debug.Log($"✅ 세션 ID 수신: {_receivedSessionId}"); + + await UniTask.Delay(1000); // 안정화 대기 + + // 3. HTTP 채팅 요청 (세션 ID 포함) + Debug.Log("3️⃣ HTTP 채팅 요청 중..."); + await SendChatRequestInternal(); + + // 채팅 응답을 기다림 + await WaitForChatResponse(15); // 15초 타임아웃 + + // 4. HTTP 캐릭터 정보 요청 + Debug.Log("4️⃣ HTTP 캐릭터 정보 요청 중..."); + await GetCharacterInfoInternal(); + + await UniTask.Delay(1000); + + // 5. WebSocket 연결 해제 + Debug.Log("5️⃣ WebSocket 연결 해제 중..."); + await _webSocketManager.DisconnectAsync(); + _receivedSessionId = null; + + // 연결 해제 후 충분한 대기 시간 + await UniTask.Delay(2000); + + Debug.Log("✅ === 더미 클라이언트 방식 전체 테스트 완료 ==="); + } + catch (Exception ex) + { + Debug.LogError($"더미 클라이언트 방식 전체 테스트 중 오류: {ex.Message}"); + } + finally + { + _isTestRunning = false; + } + } + [ContextMenu("전체 테스트 실행")] public async void RunFullTest() { @@ -402,7 +529,7 @@ private async UniTaskVoid StartAutoTest() { try { - await RunFullTestInternal(); + await RunDummyClientTestInternal(); await UniTask.Delay(TimeSpan.FromSeconds(testInterval), cancellationToken: _cancellationTokenSource.Token); } catch (OperationCanceledException) @@ -421,7 +548,7 @@ private async UniTaskVoid StartAutoTest() Debug.Log("🔄 자동 테스트 종료"); } - private async UniTask RunFullTestInternal() + private async UniTask RunDummyClientTestInternal() { // 매니저 null 체크 if (_webSocketManager == null) @@ -438,30 +565,69 @@ private async UniTask RunFullTestInternal() try { - // 1. WebSocket 연결 + // 0. 기존 연결이 있으면 해제 + if (_webSocketManager.IsConnected) + { + Debug.Log("자동 테스트 - 기존 연결 해제 중..."); + await _webSocketManager.DisconnectAsync(); + await UniTask.Delay(1000); + } + + // 1. WebSocket 연결 (세션 ID 없이) Debug.Log("자동 테스트 - WebSocket 연결 중..."); - await _webSocketManager.ConnectAsync(testSessionId); + bool connected = await _webSocketManager.ConnectAsync(); + if (!connected) + { + Debug.LogError("자동 테스트 - WebSocket 연결 실패"); + return; + } + + // 2. 세션 ID 수신 대기 + Debug.Log("자동 테스트 - 세션 ID 수신 대기 중..."); + int waitCount = 0; + while (string.IsNullOrEmpty(_receivedSessionId) && waitCount < 100) // 10초로 증가 + { + await UniTask.Delay(100); + waitCount++; + if (waitCount % 10 == 0) // 1초마다 로그 + { + Debug.Log($"자동 테스트 - 세션 ID 대기 중... ({waitCount/10}초 경과)"); + } + } + + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("자동 테스트 - 세션 ID를 받지 못했습니다. (10초 타임아웃)"); + Debug.LogWarning("서버에서 세션 ID 메시지를 보내지 않았거나, 메시지 형식이 다를 수 있습니다."); + return; + } + + Debug.Log($"자동 테스트 - 세션 ID 수신: {_receivedSessionId}"); await UniTask.Delay(500); - // 2. HTTP 요청들 + // 3. HTTP 채팅 요청 (세션 ID 포함) Debug.Log("자동 테스트 - HTTP 채팅 요청 중..."); await SendChatRequestInternal(); - await UniTask.Delay(500); + Debug.Log("자동 테스트 - HTTP 요청 완료, 채팅 응답 대기 중..."); + + // 채팅 응답을 기다림 + await WaitForChatResponse(15); // 15초 타임아웃 + // 4. HTTP 캐릭터 정보 요청 Debug.Log("자동 테스트 - HTTP 캐릭터 정보 요청 중..."); await GetCharacterInfoInternal(); await UniTask.Delay(500); - // 3. WebSocket 메시지 전송 - Debug.Log("자동 테스트 - WebSocket 메시지 전송 중..."); - await SendWebSocketMessageInternal(); - await UniTask.Delay(500); - - // 4. 연결 해제 - Debug.Log("자동 테스트 - WebSocket 연결 해제 중..."); + // 5. 연결 해제 (더 오래 기다린 후) + Debug.Log("자동 테스트 - WebSocket 응답 대기 완료, 연결 해제 중..."); + await UniTask.Delay(2000); // 추가 대기 시간 await _webSocketManager.DisconnectAsync(); + _receivedSessionId = null; + + // 연결 해제 후 충분한 대기 시간 + await UniTask.Delay(2000); - Debug.Log("자동 테스트 - 전체 테스트 완료"); + Debug.Log("자동 테스트 - 더미 클라이언트 방식 테스트 완료"); } catch (Exception ex) { @@ -478,16 +644,69 @@ private async UniTask SendChatRequestInternal() return; } - var chatRequest = new DTOs.Chat.ChatRequest + // 세션 ID가 없으면 HTTP 요청을 보내지 않음 + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("세션 ID가 없습니다. WebSocket에서 세션 ID를 먼저 받아야 합니다."); + return; + } + + var chatRequest = new ProjectVG.Infrastructure.Network.DTOs.Chat.ChatRequest { message = $"자동 테스트 메시지 - {DateTime.Now:HH:mm:ss}", characterId = testCharacterId, userId = testUserId, - sessionId = testSessionId, - actor = "web_user" + sessionId = _receivedSessionId, // 서버에서 받은 세션 ID 사용 + actor = "web_user", + action = "chat", // 클라이언트와 동일하게 명시적으로 설정 + requestedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }; - await _apiServiceManager.Chat.SendChatAsync(chatRequest); + Debug.Log($"HTTP 채팅 요청 전송: {JsonUtility.ToJson(chatRequest)}"); + + // 채팅 응답 수신 상태 초기화 + _chatResponseReceived = false; + _lastChatResponse = null; + + try + { + var response = await _apiServiceManager.Chat.SendChatAsync(chatRequest); + Debug.Log($"HTTP 채팅 요청 성공: {JsonUtility.ToJson(response)}"); + } + catch (Exception ex) + { + Debug.LogError($"HTTP 채팅 요청 실패: {ex.Message}"); + } + } + + /// + /// 채팅 응답을 기다리는 메서드 + /// + private async UniTask WaitForChatResponse(int timeoutSeconds = 15) + { + Debug.Log($"채팅 응답 대기 시작 (타임아웃: {timeoutSeconds}초)"); + + var startTime = DateTime.Now; + while (!_chatResponseReceived && (DateTime.Now - startTime).TotalSeconds < timeoutSeconds) + { + await UniTask.Delay(100); + + // WebSocket 연결 상태 확인 + if (_webSocketManager != null && !_webSocketManager.IsConnected) + { + Debug.LogWarning("WebSocket 연결이 끊어졌습니다. 응답 대기 중단"); + break; + } + } + + if (_chatResponseReceived) + { + Debug.Log($"✅ 채팅 응답 수신 완료: {_lastChatResponse}"); + } + else + { + Debug.LogWarning($"⚠️ 채팅 응답 타임아웃 ({timeoutSeconds}초)"); + } } private async UniTask GetCharacterInfoInternal() @@ -539,19 +758,23 @@ private void OnWebSocketError(string error) Debug.LogError($"❌ WebSocket 오류: {error}"); } - private void OnChatMessageReceived(DTOs.WebSocket.ChatMessage message) + private void OnChatMessageReceived(ProjectVG.Infrastructure.Network.DTOs.WebSocket.ChatMessage message) { Debug.Log($"💬 WebSocket 채팅 메시지 수신: {message.message}"); + _chatResponseReceived = true; + _lastChatResponse = message.message; } - private void OnSystemMessageReceived(DTOs.WebSocket.SystemMessage message) + private void OnSystemMessageReceived(ProjectVG.Infrastructure.Network.DTOs.WebSocket.SystemMessage message) { Debug.Log($"🔧 WebSocket 시스템 메시지 수신: {message.description}"); } - private void OnSessionIdMessageReceived(DTOs.WebSocket.SessionIdMessage message) + private void OnSessionIdMessageReceived(ProjectVG.Infrastructure.Network.DTOs.WebSocket.SessionIdMessage message) { - Debug.Log($"🆔 WebSocket 세션 ID 수신: {message.session_id}"); + _receivedSessionId = message.session_id; + Debug.Log($"🆔 WebSocket 세션 ID 수신: {_receivedSessionId}"); + Debug.Log($"✅ 세션 ID가 성공적으로 저장되었습니다!"); } #endregion diff --git a/Assets/Tests/Runtime/NetworkTestManager.cs.meta b/Assets/Tests/Runtime/NetworkTestManager.cs.meta new file mode 100644 index 0000000..6e6f889 --- /dev/null +++ b/Assets/Tests/Runtime/NetworkTestManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d0a3e75ea471e5f47b758f5a032a094e \ No newline at end of file diff --git a/Assets/Infrastructure/Network/NetworkTestUI.cs b/Assets/Tests/Runtime/NetworkTestUI.cs similarity index 99% rename from Assets/Infrastructure/Network/NetworkTestUI.cs rename to Assets/Tests/Runtime/NetworkTestUI.cs index ced1a02..22201e7 100644 --- a/Assets/Infrastructure/Network/NetworkTestUI.cs +++ b/Assets/Tests/Runtime/NetworkTestUI.cs @@ -2,7 +2,7 @@ using UnityEngine.UI; using TMPro; -namespace ProjectVG.Infrastructure.Network +namespace ProjectVG.Tests.Runtime { /// /// 네트워크 테스트를 위한 UI 매니저 diff --git a/Assets/Tests/Runtime/NetworkTestUI.cs.meta b/Assets/Tests/Runtime/NetworkTestUI.cs.meta new file mode 100644 index 0000000..492659f --- /dev/null +++ b/Assets/Tests/Runtime/NetworkTestUI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 729ea7f71af70c8429f9b8bc96104350 \ No newline at end of file diff --git a/Assets/Tests/Sences.meta b/Assets/Tests/Sences.meta new file mode 100644 index 0000000..53f0200 --- /dev/null +++ b/Assets/Tests/Sences.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 77f308e5225109541b47748e59a0fd92 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/App/Scenes/NetworkTestScene.unity b/Assets/Tests/Sences/NetworkTestScene.unity similarity index 59% rename from Assets/App/Scenes/NetworkTestScene.unity rename to Assets/Tests/Sences/NetworkTestScene.unity index e066d23..7f5b9d1 100644 --- a/Assets/App/Scenes/NetworkTestScene.unity +++ b/Assets/Tests/Sences/NetworkTestScene.unity @@ -13,14 +13,13 @@ OcclusionCullingSettings: --- !u!104 &2 RenderSettings: m_ObjectHideFlags: 0 - serializedVersion: 9 + serializedVersion: 10 m_Fog: 0 m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_FogMode: 3 m_FogDensity: 0.01 m_LinearFogStart: 0 m_LinearFogEnd: 300 - m_AmbientMode: 0 m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} @@ -39,13 +38,12 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 12 - m_GIWorkflowMode: 1 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 m_GISettings: serializedVersion: 2 m_BounceScale: 1 @@ -68,9 +66,6 @@ LightmapSettings: m_LightmapParameters: {fileID: 0} m_LightmapsBakeMode: 1 m_TextureCompression: 1 - m_FinalGather: 0 - m_FinalGatherFiltering: 1 - m_FinalGatherRayCount: 256 m_ReflectionCompression: 2 m_MixedBakeMode: 2 m_BakeBackend: 1 @@ -105,7 +100,7 @@ NavMeshSettings: serializedVersion: 2 m_ObjectHideFlags: 0 m_BuildSettings: - serializedVersion: 2 + serializedVersion: 3 agentTypeID: 0 agentRadius: 0.5 agentHeight: 2 @@ -118,7 +113,7 @@ NavMeshSettings: cellSize: 0.16666667 manualTileSize: 0 tileSize: 256 - accuratePlacement: 0 + buildHeightMesh: 0 maxJobWorkers: 0 preserveTilesOutsideBounds: 0 debug: @@ -164,20 +159,28 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 y: 0 width: 1 height: 1 - m_near: 0.3 - m_far: 1000 - m_fieldOfView: 60 - m_orthographic: 0 - m_orthographicSize: 5 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 m_Depth: -1 m_CullingMask: serializedVersion: 2 @@ -185,7 +188,7 @@ Camera: m_RenderingPath: -1 m_TargetTexture: {fileID: 0} m_TargetDisplay: 0 - m_TargetEye: 0 + m_TargetEye: 3 m_HDR: 1 m_AllowMSAA: 1 m_AllowDynamicResolution: 0 @@ -200,13 +203,123 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 519420028} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1001 &757794372 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 1234567890, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_Name + value: NetworkTestCanvas + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1234567891, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 1234567890, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + insertIndex: -1 + addedObject: {fileID: 1075127431} + m_SourcePrefab: {fileID: 100100000, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} +--- !u!1 &1075127430 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 1234567890, guid: 39117f8af9ef0d742ac7d549538819b1, type: 3} + m_PrefabInstance: {fileID: 757794372} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1075127431 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1075127430} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 --- !u!1 &1234567890 GameObject: m_ObjectHideFlags: 0 @@ -231,12 +344,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1234567890} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1234567892 MonoBehaviour: @@ -247,54 +361,18 @@ MonoBehaviour: m_GameObject: {fileID: 1234567890} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 00000000000000000000000000000000, type: 3} + m_Script: {fileID: 11500000, guid: d0a3e75ea471e5f47b758f5a032a094e, type: 3} m_Name: m_EditorClassIdentifier: testSessionId: test-session-123 - testCharacterId: test-character-456 - testUserId: test-user-789 + testCharacterId: 44444444-4444-4444-4444-444444444444 + testUserId: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb autoTest: 0 - testInterval: 5 ---- !u!1 &9876543210 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 9876543211} - - component: {fileID: 9876543212} - m_Layer: 0 - m_Name: NetworkTestUI - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &9876543211 -Transform: + testInterval: 10 +--- !u!1660057539 &9223372036854775807 +SceneRoots: m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 9876543210} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 2 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &9876543212 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 9876543210} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 00000000000000000000000000000000, type: 3} - m_Name: - m_EditorClassIdentifier: \ No newline at end of file + m_Roots: + - {fileID: 519420032} + - {fileID: 1234567891} + - {fileID: 757794372} diff --git a/Assets/App/Scenes/NetworkTestScene.unity.meta b/Assets/Tests/Sences/NetworkTestScene.unity.meta similarity index 100% rename from Assets/App/Scenes/NetworkTestScene.unity.meta rename to Assets/Tests/Sences/NetworkTestScene.unity.meta diff --git a/Assets/Tests/UI.meta b/Assets/Tests/UI.meta new file mode 100644 index 0000000..05b6495 --- /dev/null +++ b/Assets/Tests/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e536c7952bfc22f49b22d94b7f4dcb8c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab b/Assets/Tests/UI/NetworkTestCanvas.prefab similarity index 58% rename from Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab rename to Assets/Tests/UI/NetworkTestCanvas.prefab index 8bec50c..6454229 100644 --- a/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab +++ b/Assets/Tests/UI/NetworkTestCanvas.prefab @@ -8,9 +8,10 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 1234567891} + - component: {fileID: 2572838951417541798} - component: {fileID: 1234567892} - component: {fileID: 1234567893} + - component: {fileID: 3180861085867238070} m_Layer: 5 m_Name: NetworkTestCanvas m_TagString: Untagged @@ -18,8 +19,8 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!4 &1234567891 -Transform: +--- !u!224 &2572838951417541798 +RectTransform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -28,11 +29,16 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 m_Children: - - {fileID: 1234567894} + - {fileID: 1234567896} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} --- !u!20 &1234567892 Camera: m_ObjectHideFlags: 0 @@ -47,20 +53,28 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 y: 0 width: 1 height: 1 - m_near: 0.3 - m_far: 1000 - m_fieldOfView: 60 - m_orthographic: 1 - m_orthographicSize: 5 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 m_Depth: -1 m_CullingMask: serializedVersion: 2 @@ -93,12 +107,56 @@ Canvas: m_OverrideSorting: 0 m_OverridePixelPerfect: 0 m_SortingBucketNormalizedSize: 0 - m_AdditionalShaderChannels: - serializedVersion: 2 - m_SerializedAdditionalShaderChannels: 25 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 m_SortingLayerID: 0 m_SortingOrder: 0 m_TargetDisplay: 0 +--- !u!114 &3180861085867238070 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1234567890} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 --- !u!1 &1234567894 GameObject: m_ObjectHideFlags: 0 @@ -107,9 +165,8 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 1234567895} - component: {fileID: 1234567896} - - component: {fileID: 1234567897} + - component: {fileID: 9090619396326344028} m_Layer: 5 m_Name: TestPanel m_TagString: Untagged @@ -117,20 +174,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!4 &1234567895 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1234567894} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 1234567891} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!224 &1234567896 RectTransform: m_ObjectHideFlags: 0 @@ -141,16 +184,16 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 1234567891} - m_RootOrder: 0 + m_Father: {fileID: 2572838951417541798} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1234567897 +--- !u!114 &9090619396326344028 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -159,6 +202,20 @@ MonoBehaviour: m_GameObject: {fileID: 1234567894} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 00000000000000000000000000000000, type: 3} + m_Script: {fileID: 11500000, guid: 729ea7f71af70c8429f9b8bc96104350, type: 3} m_Name: - m_EditorClassIdentifier: \ No newline at end of file + m_EditorClassIdentifier: + connectButton: {fileID: 0} + disconnectButton: {fileID: 0} + chatRequestButton: {fileID: 0} + characterInfoButton: {fileID: 0} + webSocketMessageButton: {fileID: 0} + fullTestButton: {fileID: 0} + autoTestButton: {fileID: 0} + statusText: {fileID: 0} + logText: {fileID: 0} + logScrollRect: {fileID: 0} + sessionIdInput: {fileID: 0} + characterIdInput: {fileID: 0} + userIdInput: {fileID: 0} + messageInput: {fileID: 0} diff --git a/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab.meta b/Assets/Tests/UI/NetworkTestCanvas.prefab.meta similarity index 74% rename from Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab.meta rename to Assets/Tests/UI/NetworkTestCanvas.prefab.meta index 4f4e015..48cb39b 100644 --- a/Assets/Infrastructure/Network/UI/NetworkTestCanvas.prefab.meta +++ b/Assets/Tests/UI/NetworkTestCanvas.prefab.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 1e15157bc7b44a8499f09747a910ec91 +guid: 39117f8af9ef0d742ac7d549538819b1 PrefabImporter: externalObjects: {} userData: diff --git a/Packages/manifest.json b/Packages/manifest.json index 0f3c020..22f7d83 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -42,8 +42,6 @@ "com.unity.modules.unitywebrequestwww": "1.0.0", "com.unity.modules.vehicles": "1.0.0", "com.unity.modules.video": "1.0.0", - "com.unity.modules.vr": "1.0.0", - "com.unity.modules.wind": "1.0.0", - "com.unity.modules.xr": "1.0.0" + "com.unity.modules.wind": "1.0.0" } } diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 3678eb7..c643a11 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -418,14 +418,6 @@ "com.unity.modules.imageconversion": "1.0.0" } }, - "com.unity.modules.subsystems": { - "version": "1.0.0", - "depth": 1, - "source": "builtin", - "dependencies": { - "com.unity.modules.jsonserialize": "1.0.0" - } - }, "com.unity.modules.terrain": { "version": "1.0.0", "depth": 0, @@ -545,31 +537,11 @@ "com.unity.modules.unitywebrequest": "1.0.0" } }, - "com.unity.modules.vr": { - "version": "1.0.0", - "depth": 0, - "source": "builtin", - "dependencies": { - "com.unity.modules.jsonserialize": "1.0.0", - "com.unity.modules.physics": "1.0.0", - "com.unity.modules.xr": "1.0.0" - } - }, "com.unity.modules.wind": { "version": "1.0.0", "depth": 0, "source": "builtin", "dependencies": {} - }, - "com.unity.modules.xr": { - "version": "1.0.0", - "depth": 0, - "source": "builtin", - "dependencies": { - "com.unity.modules.physics": "1.0.0", - "com.unity.modules.jsonserialize": "1.0.0", - "com.unity.modules.subsystems": "1.0.0" - } } } } diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 8c4ad5b..6286a4b 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -776,6 +776,6 @@ PlayerSettings: hmiLoadingImage: {fileID: 0} platformRequiresReadableAssets: 0 virtualTexturingSupportEnabled: 0 - insecureHttpOption: 0 + insecureHttpOption: 2 androidVulkanDenyFilterList: [] androidVulkanAllowFilterList: [] diff --git a/ProjectSettings/SceneTemplateSettings.json b/ProjectSettings/SceneTemplateSettings.json new file mode 100644 index 0000000..ede5887 --- /dev/null +++ b/ProjectSettings/SceneTemplateSettings.json @@ -0,0 +1,121 @@ +{ + "templatePinStates": [], + "dependencyTypeInfos": [ + { + "userAdded": false, + "type": "UnityEngine.AnimationClip", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Animations.AnimatorController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.AnimatorOverrideController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Audio.AudioMixerController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.ComputeShader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Cubemap", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.GameObject", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.LightingDataAsset", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.LightingSettings", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Material", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.MonoScript", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessResources", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.VolumeProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.SceneAsset", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Shader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.ShaderVariantCollection", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Timeline.TimelineAsset", + "defaultInstantiationMode": 0 + } + ], + "defaultDependencyTypeInfo": { + "userAdded": false, + "type": "", + "defaultInstantiationMode": 1 + }, + "newSceneOverride": 0 +} \ No newline at end of file From 0946b2c005031268f57e4010a56421cf062401bd Mon Sep 17 00:00:00 2001 From: WooSH Date: Tue, 29 Jul 2025 11:05:53 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactory:=20network=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Configs/ApiConfig.cs | 186 --------- .../Network/Configs/ApiConfig.cs.meta | 2 - .../Network/Configs/NetworkConfig.cs | 388 ++++++++++++++++++ .../Network/Configs/NetworkConfig.cs.meta | 2 + .../Network/Configs/WebSocketConfig.cs | 166 -------- .../Network/Configs/WebSocketConfig.cs.meta | 2 - .../Network/Http/HttpApiClient.cs | 88 ++-- Assets/Infrastructure/Network/README.md | 194 +++++---- .../Network/Services/ApiServiceManager.cs | 2 +- .../{RealWebSocket.cs => DesktopWebSocket.cs} | 49 +-- .../Platforms/DesktopWebSocket.cs.meta | 2 + .../WebSocket/Platforms/MobileWebSocket.cs | 120 +++++- .../WebSocket/Platforms/RealWebSocket.cs.meta | 2 - .../WebSocket/Platforms/UnityWebSocket.cs | 86 ---- .../Platforms/UnityWebSocket.cs.meta | 2 - .../WebSocket/Platforms/WebGLWebSocket.cs | 191 +++++++++ .../Platforms/WebGLWebSocket.cs.meta | 2 + .../Platforms/WebSocketSharpFallback.cs | 86 ---- .../Platforms/WebSocketSharpFallback.cs.meta | 2 - .../Network/WebSocket/WebSocketFactory.cs | 25 ++ .../WebSocket/WebSocketFactory.cs.meta | 2 + .../Network/WebSocket/WebSocketManager.cs | 71 +--- Assets/Tests/Runtime/NetworkTestManager.cs | 51 ++- Assets/Tests/Runtime/NetworkTestUI.cs | 2 +- Assets/Tests/Sences/NetworkTestScene.unity | 49 ++- 25 files changed, 994 insertions(+), 778 deletions(-) delete mode 100644 Assets/Infrastructure/Network/Configs/ApiConfig.cs delete mode 100644 Assets/Infrastructure/Network/Configs/ApiConfig.cs.meta create mode 100644 Assets/Infrastructure/Network/Configs/NetworkConfig.cs create mode 100644 Assets/Infrastructure/Network/Configs/NetworkConfig.cs.meta delete mode 100644 Assets/Infrastructure/Network/Configs/WebSocketConfig.cs delete mode 100644 Assets/Infrastructure/Network/Configs/WebSocketConfig.cs.meta rename Assets/Infrastructure/Network/WebSocket/Platforms/{RealWebSocket.cs => DesktopWebSocket.cs} (70%) create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs delete mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs delete mode 100644 Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs.meta diff --git a/Assets/Infrastructure/Network/Configs/ApiConfig.cs b/Assets/Infrastructure/Network/Configs/ApiConfig.cs deleted file mode 100644 index 9c6c581..0000000 --- a/Assets/Infrastructure/Network/Configs/ApiConfig.cs +++ /dev/null @@ -1,186 +0,0 @@ -using UnityEngine; - -namespace ProjectVG.Infrastructure.Network.Configs -{ - /// - /// API 설정을 관리하는 ScriptableObject - /// - [CreateAssetMenu(fileName = "ApiConfig", menuName = "ProjectVG/Network/ApiConfig")] - public class ApiConfig : ScriptableObject - { - [Header("Server Configuration")] - [SerializeField] private string baseUrl = "http://122.153.130.223:7900"; - [SerializeField] private string apiVersion = "v1"; - [SerializeField] private float timeout = 30f; - [SerializeField] private int maxRetryCount = 3; - [SerializeField] private float retryDelay = 1f; - - [Header("Authentication")] - [SerializeField] private string clientId = ""; - [SerializeField] private string clientSecret = ""; - - [Header("Endpoints")] - [SerializeField] private string userEndpoint = "users"; - [SerializeField] private string characterEndpoint = "characters"; - [SerializeField] private string conversationEndpoint = "conversations"; - [SerializeField] private string authEndpoint = "auth"; - - [Header("Headers")] - [SerializeField] private string contentType = "application/json"; - [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; - - // Constants - private const string API_PATH = "api"; - private const string DEFAULT_CLIENT_ID = "unity-client-dev"; - private const string DEFAULT_CLIENT_SECRET = "dev-secret-key"; - private const string DEFAULT_USER_AGENT = "ProjectVG-Unity-Client/1.0"; - - // Properties - public string BaseUrl => baseUrl; - public string ApiVersion => apiVersion; - public float Timeout => timeout; - public int MaxRetryCount => maxRetryCount; - public float RetryDelay => retryDelay; - public string ClientId => clientId; - public string ClientSecret => clientSecret; - public string ContentType => contentType; - public string UserAgent => userAgent; - - // Endpoint Properties - public string UserEndpoint => userEndpoint; - public string CharacterEndpoint => characterEndpoint; - public string ConversationEndpoint => conversationEndpoint; - public string AuthEndpoint => authEndpoint; - - #region URL Generation Methods - - /// - /// 전체 API URL 생성 - /// - public string GetFullUrl(string endpoint) - { - return $"{baseUrl.TrimEnd('/')}/{API_PATH}/{apiVersion.TrimStart('/').TrimEnd('/')}/{endpoint.TrimStart('/')}"; - } - - /// - /// 사용자 API URL - /// - public string GetUserUrl(string path = "") - { - return GetFullUrl($"{userEndpoint}/{path.TrimStart('/')}"); - } - - /// - /// 캐릭터 API URL - /// - public string GetCharacterUrl(string path = "") - { - return GetFullUrl($"{characterEndpoint}/{path.TrimStart('/')}"); - } - - /// - /// 대화 API URL - /// - public string GetConversationUrl(string path = "") - { - return GetFullUrl($"{conversationEndpoint}/{path.TrimStart('/')}"); - } - - /// - /// 인증 API URL - /// - public string GetAuthUrl(string path = "") - { - return GetFullUrl($"{authEndpoint}/{path.TrimStart('/')}"); - } - - #endregion - - #region Factory Methods - - /// - /// 개발 환경 설정 생성 - /// - public static ApiConfig CreateDevelopmentConfig() - { - var config = CreateInstance(); - ConfigureDevelopmentSettings(config); - return config; - } - - /// - /// 프로덕션 환경 설정 생성 - /// - public static ApiConfig CreateProductionConfig() - { - var config = CreateInstance(); - ConfigureProductionSettings(config); - return config; - } - - /// - /// 테스트 환경 설정 생성 - /// - public static ApiConfig CreateTestConfig() - { - var config = CreateInstance(); - ConfigureTestSettings(config); - return config; - } - - #endregion - - #region Private Configuration Methods - - private static void ConfigureDevelopmentSettings(ApiConfig config) - { - config.baseUrl = "http://localhost:7901"; - config.apiVersion = "v1"; - config.timeout = 10f; - config.maxRetryCount = 1; - config.retryDelay = 0.5f; - config.clientId = DEFAULT_CLIENT_ID; - config.clientSecret = DEFAULT_CLIENT_SECRET; - config.contentType = "application/json"; - config.userAgent = DEFAULT_USER_AGENT; - - ConfigureDefaultEndpoints(config); - } - - private static void ConfigureProductionSettings(ApiConfig config) - { - config.baseUrl = "http://122.153.130.223:7900"; - config.apiVersion = "v1"; - config.timeout = 30f; - config.maxRetryCount = 3; - config.retryDelay = 1f; - config.contentType = "application/json"; - config.userAgent = "ProjectVG-Client/1.0"; - - ConfigureDefaultEndpoints(config); - } - - private static void ConfigureTestSettings(ApiConfig config) - { - config.baseUrl = "http://122.153.130.223:7901"; - config.apiVersion = "v1"; - config.timeout = 15f; - config.maxRetryCount = 2; - config.retryDelay = 0.5f; - config.contentType = "application/json"; - config.userAgent = "ProjectVG-Client/1.0"; - - ConfigureDefaultEndpoints(config); - } - - private static void ConfigureDefaultEndpoints(ApiConfig config) - { - config.userEndpoint = "users"; - config.characterEndpoint = "characters"; - config.conversationEndpoint = "conversations"; - config.authEndpoint = "auth"; - } - - #endregion - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ApiConfig.cs.meta b/Assets/Infrastructure/Network/Configs/ApiConfig.cs.meta deleted file mode 100644 index 63497c5..0000000 --- a/Assets/Infrastructure/Network/Configs/ApiConfig.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3060309933b81664ca0bb0f38ee668e0 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/NetworkConfig.cs b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs new file mode 100644 index 0000000..f4518df --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs @@ -0,0 +1,388 @@ +using UnityEngine; + +namespace ProjectVG.Infrastructure.Network.Configs +{ + /// + /// Unity 표준 방식의 네트워크 설정 ScriptableObject + /// Editor에서 설정 가능하고, 런타임에서는 정적 접근자로 사용 + /// + [CreateAssetMenu(fileName = "NetworkConfig", menuName = "ProjectVG/Network/NetworkConfig")] + public class NetworkConfig : ScriptableObject + { + [Header("Environment Settings")] + [SerializeField] private EnvironmentType environment = EnvironmentType.Development; + + [Header("Server Addresses")] + [SerializeField] private string developmentServer = "localhost:7900"; + [SerializeField] private string testServer = "localhost:7900"; + [SerializeField] private string productionServer = "122.153.130.223:7900"; + + [Header("HTTP API Settings")] + [SerializeField] private string apiVersion = "v1"; + [SerializeField] private string apiPath = "api"; + [SerializeField] private float httpTimeout = 30f; + [SerializeField] private int maxRetryCount = 3; + [SerializeField] private float retryDelay = 1f; + + [Header("WebSocket Settings")] + [SerializeField] private string wsPath = "ws"; + [SerializeField] private float wsTimeout = 30f; + [SerializeField] private float reconnectDelay = 5f; + [SerializeField] private int maxReconnectAttempts = 3; + [SerializeField] private bool autoReconnect = true; + [SerializeField] private float heartbeatInterval = 30f; + [SerializeField] private bool enableHeartbeat = true; + [SerializeField] private int maxMessageSize = 65536; // 64KB + [SerializeField] private float messageTimeout = 10f; + [SerializeField] private bool enableMessageLogging = true; + + [Header("Common Settings")] + [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; + [SerializeField] private string contentType = "application/json"; + + // Environment enum + public enum EnvironmentType + { + Development, + Test, + Production + } + + // Singleton instance + private static NetworkConfig _instance; + public static NetworkConfig Instance + { + get + { + if (_instance == null) + { + _instance = Resources.Load("NetworkConfig"); + if (_instance == null) + { + Debug.LogError("NetworkConfig를 찾을 수 없습니다. Resources 폴더에 NetworkConfig.asset 파일을 생성하세요."); + _instance = CreateDefaultInstance(); + } + } + return _instance; + } + } + + // Properties + public EnvironmentType Environment => environment; + public string ApiPath => apiPath; + public string WsPath => wsPath; + + + + #region Environment Configuration + + + + #endregion + + #region Static Accessors (편의 메서드들) + + /// + /// 현재 환경 + /// + public static EnvironmentType CurrentEnvironment => Instance.Environment; + + /// + /// HTTP 서버 주소 + /// + public static string HttpServerAddress + { + get + { + string server; + switch (Instance.environment) + { + case EnvironmentType.Development: + server = Instance.developmentServer; + break; + case EnvironmentType.Test: + server = Instance.testServer; + break; + case EnvironmentType.Production: + server = Instance.productionServer; + break; + default: + server = Instance.developmentServer; + break; + } + return $"http://{server}"; + } + } + + /// + /// WebSocket 서버 주소 + /// + public static string WebSocketServerAddress + { + get + { + string server; + switch (Instance.environment) + { + case EnvironmentType.Development: + server = Instance.developmentServer; + break; + case EnvironmentType.Test: + server = Instance.testServer; + break; + case EnvironmentType.Production: + server = Instance.productionServer; + break; + default: + server = Instance.developmentServer; + break; + } + return $"ws://{server}"; + } + } + + /// + /// API 버전 + /// + public static string ApiVersion => Instance.apiVersion; + + /// + /// HTTP 타임아웃 + /// + public static float HttpTimeout => Instance.httpTimeout; + + /// + /// 최대 재시도 횟수 + /// + public static int MaxRetryCount => Instance.maxRetryCount; + + /// + /// 재시도 지연 시간 + /// + public static float RetryDelay => Instance.retryDelay; + + /// + /// WebSocket 타임아웃 + /// + public static float WebSocketTimeout => Instance.wsTimeout; + + /// + /// 재연결 지연 시간 + /// + public static float ReconnectDelay => Instance.reconnectDelay; + + /// + /// 최대 재연결 시도 횟수 + /// + public static int MaxReconnectAttempts => Instance.maxReconnectAttempts; + + /// + /// 자동 재연결 + /// + public static bool AutoReconnect => Instance.autoReconnect; + + /// + /// 하트비트 간격 + /// + public static float HeartbeatInterval => Instance.heartbeatInterval; + + /// + /// 하트비트 활성화 + /// + public static bool EnableHeartbeat => Instance.enableHeartbeat; + + /// + /// 최대 메시지 크기 + /// + public static int MaxMessageSize => Instance.maxMessageSize; + + /// + /// 메시지 타임아웃 + /// + public static float MessageTimeout => Instance.messageTimeout; + + /// + /// 메시지 로깅 활성화 + /// + public static bool EnableMessageLogging => Instance.enableMessageLogging; + + /// + /// 사용자 에이전트 + /// + public static string UserAgent => Instance.userAgent; + + /// + /// 콘텐츠 타입 + /// + public static string ContentType => Instance.contentType; + + /// + /// 전체 API URL 생성 + /// + public static string GetFullApiUrl(string endpoint) + { + var baseUrl = HttpServerAddress; + return $"{baseUrl.TrimEnd('/')}/{Instance.apiPath.TrimStart('/').TrimEnd('/')}/{Instance.apiVersion.TrimStart('/').TrimEnd('/')}/{endpoint.TrimStart('/')}"; + } + + /// + /// 사용자 API URL + /// + public static string GetUserApiUrl(string path = "") + { + return GetFullApiUrl($"users/{path.TrimStart('/')}"); + } + + /// + /// 캐릭터 API URL + /// + public static string GetCharacterApiUrl(string path = "") + { + return GetFullApiUrl($"characters/{path.TrimStart('/')}"); + } + + /// + /// 대화 API URL + /// + public static string GetConversationApiUrl(string path = "") + { + return GetFullApiUrl($"conversations/{path.TrimStart('/')}"); + } + + /// + /// 인증 API URL + /// + public static string GetAuthApiUrl(string path = "") + { + return GetFullApiUrl($"auth/{path.TrimStart('/')}"); + } + + /// + /// WebSocket URL + /// + public static string GetWebSocketUrl() + { + var baseUrl = WebSocketServerAddress; + return $"{baseUrl.TrimEnd('/')}/{Instance.wsPath.TrimStart('/').TrimEnd('/')}"; + } + + /// + /// 버전이 포함된 WebSocket URL + /// + public static string GetWebSocketUrlWithVersion() + { + var baseUrl = WebSocketServerAddress; + return $"{baseUrl.TrimEnd('/')}/api/{Instance.apiVersion.TrimStart('/').TrimEnd('/')}/{Instance.wsPath.TrimStart('/').TrimEnd('/')}"; + } + + /// + /// 세션이 포함된 WebSocket URL + /// + public static string GetWebSocketUrlWithSession(string sessionId) + { + var baseWsUrl = GetWebSocketUrlWithVersion(); + return $"{baseWsUrl}?sessionId={sessionId}"; + } + + + /// + /// 개발 환경 설정 + /// + public static void SetDevelopmentEnvironment() + { + if (Application.isPlaying) + { + Debug.LogWarning("런타임 중에는 환경 설정을 변경할 수 없습니다."); + return; + } + + Instance.environment = EnvironmentType.Development; + } + + /// + /// 테스트 환경 설정 + /// + public static void SetTestEnvironment() + { + if (Application.isPlaying) + { + Debug.LogWarning("런타임 중에는 환경 설정을 변경할 수 없습니다."); + return; + } + + Instance.environment = EnvironmentType.Test; + } + + /// + /// 프로덕션 환경 설정 + /// + public static void SetProductionEnvironment() + { + if (Application.isPlaying) + { + Debug.LogWarning("런타임 중에는 환경 설정을 변경할 수 없습니다."); + return; + } + + Instance.environment = EnvironmentType.Production; + } + + /// + /// 현재 설정 로그 출력 + /// + public static void LogCurrentSettings() + { + Debug.Log($"=== NetworkConfig 현재 설정 ==="); + Debug.Log($"환경: {CurrentEnvironment}"); + Debug.Log($"HTTP 서버: {HttpServerAddress}"); + Debug.Log($"WebSocket 서버: {WebSocketServerAddress}"); + Debug.Log($"API 버전: {ApiVersion}"); + Debug.Log($"HTTP 타임아웃: {HttpTimeout}s"); + Debug.Log($"WebSocket 타임아웃: {WebSocketTimeout}s"); + Debug.Log($"자동 재연결: {AutoReconnect}"); + Debug.Log($"하트비트: {EnableHeartbeat} ({HeartbeatInterval}s)"); + Debug.Log($"================================"); + } + + #endregion + + #region Private Methods + + /// + /// 기본 인스턴스 생성 (Resources 폴더에 파일이 없을 때) + /// + private static NetworkConfig CreateDefaultInstance() + { + var instance = CreateInstance(); + + // 기본 설정 + instance.environment = EnvironmentType.Development; + instance.developmentServer = "localhost:7900"; + instance.testServer = "localhost:7900"; + instance.productionServer = "122.153.130.223:7900"; + instance.apiVersion = "v1"; + instance.apiPath = "api"; + instance.httpTimeout = 30f; + instance.maxRetryCount = 3; + instance.retryDelay = 1f; + instance.wsPath = "ws"; + instance.wsTimeout = 30f; + instance.reconnectDelay = 5f; + instance.maxReconnectAttempts = 3; + instance.autoReconnect = true; + instance.heartbeatInterval = 30f; + instance.enableHeartbeat = true; + instance.maxMessageSize = 65536; + instance.messageTimeout = 10f; + instance.enableMessageLogging = true; + instance.userAgent = "ProjectVG-Client/1.0"; + instance.contentType = "application/json"; + + Debug.LogWarning("기본 NetworkConfig를 생성했습니다. Resources 폴더에 NetworkConfig.asset 파일을 생성하는 것을 권장합니다."); + + return instance; + } + + #endregion + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/NetworkConfig.cs.meta b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs.meta new file mode 100644 index 0000000..233db0c --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 86f34d98107a627428323909bc15f04d \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs b/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs deleted file mode 100644 index 0165322..0000000 --- a/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs +++ /dev/null @@ -1,166 +0,0 @@ -using UnityEngine; -using System; - -namespace ProjectVG.Infrastructure.Network.Configs -{ - /// - /// WebSocket 설정을 관리하는 ScriptableObject - /// - [CreateAssetMenu(fileName = "WebSocketConfig", menuName = "ProjectVG/Network/WebSocketConfig")] - public class WebSocketConfig : ScriptableObject - { - [Header("WebSocket Server Configuration")] - [SerializeField] private string baseUrl = "http://122.153.130.223:7900"; - [SerializeField] private string wsPath = "ws"; - [SerializeField] private string apiVersion = "v1"; - - [Header("Connection Settings")] - [SerializeField] private float timeout = 30f; - [SerializeField] private float reconnectDelay = 5f; - [SerializeField] private int maxReconnectAttempts = 3; - [SerializeField] private bool autoReconnect = true; - [SerializeField] private float heartbeatInterval = 30f; - [SerializeField] private bool enableHeartbeat = true; - - [Header("Message Settings")] - [SerializeField] private int maxMessageSize = 65536; // 64KB - [SerializeField] private float messageTimeout = 10f; - [SerializeField] private bool enableMessageLogging = true; - - [Header("Headers")] - [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; - [SerializeField] private string contentType = "application/json"; - - // Properties - public string BaseUrl => ConvertToWebSocketUrl(baseUrl); - public string WsPath => wsPath; - public string ApiVersion => apiVersion; - public float Timeout => timeout; - public float ReconnectDelay => reconnectDelay; - public int MaxReconnectAttempts => maxReconnectAttempts; - public bool AutoReconnect => autoReconnect; - public float HeartbeatInterval => heartbeatInterval; - public bool EnableHeartbeat => enableHeartbeat; - public int MaxMessageSize => maxMessageSize; - public float MessageTimeout => messageTimeout; - public bool EnableMessageLogging => enableMessageLogging; - public string UserAgent => userAgent; - public string ContentType => contentType; - - /// - /// WebSocket URL 생성 - /// - public string GetWebSocketUrl() - { - var wsBaseUrl = ConvertToWebSocketUrl(baseUrl); - return $"{wsBaseUrl.TrimEnd('/')}/{wsPath.TrimStart('/').TrimEnd('/')}"; - } - - /// - /// API 버전이 포함된 WebSocket URL 생성 - /// - public string GetWebSocketUrlWithVersion() - { - var wsBaseUrl = ConvertToWebSocketUrl(baseUrl); - return $"{wsBaseUrl.TrimEnd('/')}/api/{apiVersion.TrimStart('/').TrimEnd('/')}/{wsPath.TrimStart('/').TrimEnd('/')}"; - } - - /// - /// 세션별 WebSocket URL 생성 - /// - public string GetWebSocketUrlWithSession(string sessionId) - { - var baseWsUrl = GetWebSocketUrlWithVersion(); - return $"{baseWsUrl}?sessionId={sessionId}"; - } - - /// - /// URL을 WebSocket URL로 변환 - /// - private string ConvertToWebSocketUrl(string httpUrl) - { - try - { - if (httpUrl.StartsWith("http://") || httpUrl.StartsWith("https://")) - { - // HTTP URL을 WebSocket URL로 변환 - var wsUrl = httpUrl.Replace("http://", "ws://").Replace("https://", "wss://"); - return wsUrl; - } - return httpUrl; - } - catch (Exception ex) - { - Debug.LogError($"WebSocket URL 변환 실패: {ex.Message}"); - return "ws://localhost:7901"; // Fallback - } - } - - #region Factory Methods - - /// - /// 개발 환경 설정 생성 - /// - public static WebSocketConfig CreateDevelopmentConfig() - { - var config = CreateInstance(); - config.baseUrl = "http://localhost:7901"; // HTTP 사용 - config.wsPath = "ws"; - config.apiVersion = "v1"; - config.timeout = 10f; - config.reconnectDelay = 2f; - config.maxReconnectAttempts = 2; - config.autoReconnect = true; - config.heartbeatInterval = 15f; - config.enableHeartbeat = true; - config.maxMessageSize = 32768; // 32KB - config.messageTimeout = 5f; - config.enableMessageLogging = true; - return config; - } - - /// - /// 프로덕션 환경 설정 생성 - /// - public static WebSocketConfig CreateProductionConfig() - { - var config = CreateInstance(); - config.baseUrl = "http://122.153.130.223:7900"; // HTTP 사용 - config.wsPath = "ws"; - config.apiVersion = "v1"; - config.timeout = 30f; - config.reconnectDelay = 5f; - config.maxReconnectAttempts = 3; - config.autoReconnect = true; - config.heartbeatInterval = 30f; - config.enableHeartbeat = true; - config.maxMessageSize = 65536; // 64KB - config.messageTimeout = 10f; - config.enableMessageLogging = false; - return config; - } - - /// - /// 테스트 환경 설정 생성 - /// - public static WebSocketConfig CreateTestConfig() - { - var config = CreateInstance(); - config.baseUrl = "http://122.153.130.223:7900"; // HTTP 사용 - config.wsPath = "ws"; - config.apiVersion = "v1"; - config.timeout = 15f; - config.reconnectDelay = 3f; - config.maxReconnectAttempts = 2; - config.autoReconnect = true; - config.heartbeatInterval = 20f; - config.enableHeartbeat = true; - config.maxMessageSize = 32768; // 32KB - config.messageTimeout = 8f; - config.enableMessageLogging = true; - return config; - } - - #endregion - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs.meta b/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs.meta deleted file mode 100644 index 56bccbf..0000000 --- a/Assets/Infrastructure/Network/Configs/WebSocketConfig.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: bf7d6fa8efeb2fc4cab6197e3b03420e \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Http/HttpApiClient.cs b/Assets/Infrastructure/Network/Http/HttpApiClient.cs index 31c7d98..9983bfa 100644 --- a/Assets/Infrastructure/Network/Http/HttpApiClient.cs +++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs @@ -17,17 +17,8 @@ namespace ProjectVG.Infrastructure.Network.Http public class HttpApiClient : MonoBehaviour { [Header("API Configuration")] - [SerializeField] private ApiConfig apiConfig; - [SerializeField] private float timeout = 60f; - [SerializeField] private int maxRetryCount = 3; - [SerializeField] private float retryDelay = 1f; + // NetworkConfig를 사용하여 설정을 관리합니다. - [Header("Headers")] - [SerializeField] private string contentType = "application/json"; - [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; - - private const string DEFAULT_BASE_URL = "http://122.153.130.223:7900"; - private const string DEFAULT_API_VERSION = "v1"; private const string ACCEPT_HEADER = "application/json"; private const string AUTHORIZATION_HEADER = "Authorization"; private const string BEARER_PREFIX = "Bearer "; @@ -53,11 +44,7 @@ private void OnDestroy() #region Public API - public void SetApiConfig(ApiConfig config) - { - apiConfig = config; - InitializeClient(); - } + public void AddDefaultHeader(string key, string value) { @@ -128,43 +115,26 @@ private void InitializeSingleton() private void InitializeClient() { cancellationTokenSource = new CancellationTokenSource(); - - if (apiConfig == null) - { - Debug.LogWarning("ApiConfig가 설정되지 않았습니다. 기본값을 사용합니다."); - SetupDefaultHeaders(); - return; - } - - ApplyApiConfig(); + ApplyNetworkConfig(); SetupDefaultHeaders(); } - private void ApplyApiConfig() + private void ApplyNetworkConfig() { - timeout = apiConfig.Timeout; - maxRetryCount = apiConfig.MaxRetryCount; - retryDelay = apiConfig.RetryDelay; - contentType = apiConfig.ContentType; - userAgent = apiConfig.UserAgent; + Debug.Log($"NetworkConfig 적용: {NetworkConfig.CurrentEnvironment} 환경"); } private void SetupDefaultHeaders() { defaultHeaders.Clear(); - defaultHeaders["Content-Type"] = contentType; - defaultHeaders["User-Agent"] = userAgent; + defaultHeaders["Content-Type"] = NetworkConfig.ContentType; + defaultHeaders["User-Agent"] = NetworkConfig.UserAgent; defaultHeaders["Accept"] = ACCEPT_HEADER; } private string GetFullUrl(string endpoint) { - if (apiConfig != null) - { - return apiConfig.GetFullUrl(endpoint); - } - - return $"{DEFAULT_BASE_URL}/api/{DEFAULT_API_VERSION}/{endpoint.TrimStart('/')}"; + return NetworkConfig.GetFullApiUrl(endpoint); } private string SerializeData(object data) @@ -191,13 +161,13 @@ private async UniTask SendRequestAsync(string url, string method, string j Debug.Log($"HTTP 요청 데이터: {jsonData}"); } - for (int attempt = 0; attempt <= maxRetryCount; attempt++) + for (int attempt = 0; attempt <= NetworkConfig.MaxRetryCount; attempt++) { try { using var request = CreateRequest(url, method, jsonData, headers); - Debug.Log($"HTTP 요청 전송 중... (시도 {attempt + 1}/{maxRetryCount + 1})"); + Debug.Log($"HTTP 요청 전송 중... (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1})"); var operation = request.SendWebRequest(); await operation.WithCancellation(combinedCancellationToken); @@ -221,14 +191,14 @@ private async UniTask SendRequestAsync(string url, string method, string j } } - throw new ApiException($"{maxRetryCount + 1}번 시도 후 요청 실패", 0, "최대 재시도 횟수 초과"); + throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 요청 실패", 0, "최대 재시도 횟수 초과"); } private async UniTask SendFileRequestAsync(string url, byte[] fileData, string fileName, string fieldName, Dictionary headers, CancellationToken cancellationToken) { var combinedCancellationToken = CreateCombinedCancellationToken(cancellationToken); - for (int attempt = 0; attempt <= maxRetryCount; attempt++) + for (int attempt = 0; attempt <= NetworkConfig.MaxRetryCount; attempt++) { try { @@ -237,7 +207,7 @@ private async UniTask SendFileRequestAsync(string url, byte[] fileData, st using var request = UnityWebRequest.Post(url, form); SetupRequest(request, headers); - request.timeout = (int)timeout; + request.timeout = (int)NetworkConfig.HttpTimeout; var operation = request.SendWebRequest(); await operation.WithCancellation(combinedCancellationToken); @@ -261,7 +231,7 @@ private async UniTask SendFileRequestAsync(string url, byte[] fileData, st } } - throw new ApiException($"{maxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, "최대 재시도 횟수 초과"); + throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, "최대 재시도 횟수 초과"); } private CancellationToken CreateCombinedCancellationToken(CancellationToken cancellationToken) @@ -274,10 +244,10 @@ private async UniTask HandleRequestFailure(UnityWebRequest request, int attempt, var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); Debug.LogError($"HTTP 요청 실패: {request.result}, 상태코드: {request.responseCode}, 오류: {request.error}"); - if (ShouldRetry(request.responseCode) && attempt < maxRetryCount) + if (ShouldRetry(request.responseCode) && attempt < NetworkConfig.MaxRetryCount) { - Debug.LogWarning($"API 요청 실패 (시도 {attempt + 1}/{maxRetryCount + 1}): {error.Message}"); - await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: cancellationToken); + Debug.LogWarning($"API 요청 실패 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {error.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken); return; } @@ -286,23 +256,23 @@ private async UniTask HandleRequestFailure(UnityWebRequest request, int attempt, private async UniTask HandleRequestException(Exception ex, int attempt, CancellationToken cancellationToken) { - if (attempt < maxRetryCount) + if (attempt < NetworkConfig.MaxRetryCount) { - Debug.LogWarning($"API 요청 예외 발생 (시도 {attempt + 1}/{maxRetryCount + 1}): {ex.Message}"); - await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: cancellationToken); + Debug.LogWarning($"API 요청 예외 발생 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {ex.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken); return; } - throw new ApiException($"{maxRetryCount + 1}번 시도 후 요청 실패", 0, ex.Message); + throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 요청 실패", 0, ex.Message); } private async UniTask HandleFileUploadFailure(UnityWebRequest request, int attempt, CancellationToken cancellationToken) { var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); - if (ShouldRetry(request.responseCode) && attempt < maxRetryCount) + if (ShouldRetry(request.responseCode) && attempt < NetworkConfig.MaxRetryCount) { - Debug.LogWarning($"파일 업로드 실패 (시도 {attempt + 1}/{maxRetryCount + 1}): {error.Message}"); - await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: cancellationToken); + Debug.LogWarning($"파일 업로드 실패 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {error.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken); return; } @@ -311,13 +281,13 @@ private async UniTask HandleFileUploadFailure(UnityWebRequest request, int attem private async UniTask HandleFileUploadException(Exception ex, int attempt, CancellationToken cancellationToken) { - if (attempt < maxRetryCount) + if (attempt < NetworkConfig.MaxRetryCount) { - Debug.LogWarning($"파일 업로드 예외 발생 (시도 {attempt + 1}/{maxRetryCount + 1}): {ex.Message}"); - await UniTask.Delay(TimeSpan.FromSeconds(retryDelay * (attempt + 1)), cancellationToken: cancellationToken); + Debug.LogWarning($"파일 업로드 예외 발생 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {ex.Message}"); + await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken); return; } - throw new ApiException($"{maxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, ex.Message); + throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, ex.Message); } private UnityWebRequest CreateRequest(string url, string method, string jsonData, Dictionary headers) @@ -332,7 +302,7 @@ private UnityWebRequest CreateRequest(string url, string method, string jsonData request.downloadHandler = new DownloadHandlerBuffer(); SetupRequest(request, headers); - request.timeout = (int)timeout; + request.timeout = (int)NetworkConfig.HttpTimeout; return request; } diff --git a/Assets/Infrastructure/Network/README.md b/Assets/Infrastructure/Network/README.md index 2b46816..9789ff1 100644 --- a/Assets/Infrastructure/Network/README.md +++ b/Assets/Infrastructure/Network/README.md @@ -10,46 +10,82 @@ Unity 클라이언트와 서버 간의 통신을 위한 네트워크 모듈입 "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask" ``` -### 2. WebSocket 시뮬레이션 -현재는 시뮬레이션 구현체를 사용합니다. 개발/테스트에 최적화되어 있습니다. +### 2. 플랫폼별 WebSocket 구현 +플랫폼에 따라 최적화된 WebSocket 구현을 사용합니다. -**시뮬레이션 구현체 장점:** -- 🟩 패키지 의존성 없음 -- 🟩 즉시 사용 가능 -- 🟩 개발/테스트에 적합 -- 🟩 크로스 플랫폼 지원 +**플랫폼별 구현:** +- 🟩 Desktop: System.Net.WebSockets.ClientWebSocket +- 🟩 WebGL: UnityWebRequest.WebSocket +- 🟩 Mobile: 네이티브 WebSocket 라이브러리 ## 🏗️ 구조 ``` Assets/Infrastructure/Network/ -├── Configs/ # 설정 파일들 -│ ├── ApiConfig.cs # HTTP API 설정 -│ └── WebSocketConfig.cs # WebSocket 설정 -├── DTOs/ # 데이터 전송 객체들 -│ ├── BaseApiResponse.cs # 기본 API 응답 -│ ├── Chat/ # 채팅 관련 DTO -│ ├── Character/ # 캐릭터 관련 DTO -│ └── WebSocket/ # WebSocket 메시지 DTO -├── Http/ # HTTP 클라이언트 -│ └── HttpApiClient.cs # HTTP API 클라이언트 -├── Services/ # API 서비스들 +├── Configs/ # 설정 파일들 +│ └── NetworkConfig.cs # Unity 표준 ScriptableObject 기반 설정 +├── DTOs/ # 데이터 전송 객체들 +│ ├── BaseApiResponse.cs # 기본 API 응답 +│ ├── Chat/ # 채팅 관련 DTO +│ ├── Character/ # 캐릭터 관련 DTO +│ └── WebSocket/ # WebSocket 메시지 DTO +├── Http/ # HTTP 클라이언트 +│ └── HttpApiClient.cs # HTTP API 클라이언트 +├── Services/ # API 서비스들 │ ├── ApiServiceManager.cs # API 서비스 매니저 │ ├── ChatApiService.cs # 채팅 API 서비스 │ └── CharacterApiService.cs # 캐릭터 API 서비스 -└── WebSocket/ # WebSocket 관련 - ├── WebSocketManager.cs # WebSocket 매니저 - ├── IWebSocketHandler.cs # WebSocket 핸들러 인터페이스 +└── WebSocket/ # WebSocket 관련 + ├── WebSocketManager.cs # WebSocket 매니저 + ├── WebSocketFactory.cs # 플랫폼별 WebSocket 팩토리 + ├── IWebSocketHandler.cs # WebSocket 핸들러 인터페이스 ├── DefaultWebSocketHandler.cs # 기본 핸들러 - └── Platforms/ # 플랫폼별 WebSocket 구현 - ├── UnityWebSocket.cs # WebSocket 시뮬레이션 - ├── MobileWebSocket.cs # 모바일 시뮬레이션 - └── WebSocketSharpFallback.cs # 폴백 시뮬레이션 + └── Platforms/ # 플랫폼별 WebSocket 구현 + ├── DesktopWebSocket.cs # 데스크톱용 (.NET ClientWebSocket) + ├── WebGLWebSocket.cs # WebGL용 (UnityWebRequest) + └── MobileWebSocket.cs # 모바일용 (네이티브 라이브러리) ``` ## 🚀 사용법 -### 1. 전체 흐름 테스트 (권장) +### 1. Unity 표준 ScriptableObject 기반 설정 관리 + +#### 설정 파일 생성 +```csharp +// Unity Editor에서 ScriptableObject 생성 +// Assets/Infrastructure/Network/Configs/ 폴더에서 우클릭 +// Create > ProjectVG > Network > NetworkConfig +// Resources 폴더에 NetworkConfig.asset 파일 생성 +``` + +#### 앱 시작 시 환경 설정 +```csharp +// 앱 시작 시 (Editor에서만 가능) +NetworkConfig.SetDevelopmentEnvironment(); // localhost:7900 +NetworkConfig.SetTestEnvironment(); // localhost:7900 +NetworkConfig.SetProductionEnvironment(); // 122.153.130.223:7900 +``` + +#### 런타임 중 설정 사용 +```csharp +// 어디서든 동일한 설정 접근 (강제로 NetworkConfig 사용) +var currentEnv = NetworkConfig.CurrentEnvironment; +var apiUrl = NetworkConfig.GetFullApiUrl("chat"); +var wsUrl = NetworkConfig.GetWebSocketUrl(); + +// API URL 생성 +var userUrl = NetworkConfig.GetUserApiUrl(); +var characterUrl = NetworkConfig.GetCharacterApiUrl(); +var conversationUrl = NetworkConfig.GetConversationApiUrl(); +var authUrl = NetworkConfig.GetAuthApiUrl(); + +// WebSocket URL 생성 +var wsUrl = NetworkConfig.GetWebSocketUrl(); +var wsUrlWithVersion = NetworkConfig.GetWebSocketUrlWithVersion(); +var wsUrlWithSession = NetworkConfig.GetWebSocketUrlWithSession("session-123"); +``` + +### 2. 전체 흐름 테스트 (권장) ```csharp // NetworkTestManager 사용 var testManager = FindObjectOfType(); @@ -64,7 +100,7 @@ await testManager.SendChatRequest(); // 서버가 비동기 작업 완료 후 WebSocket으로 결과 전송 ``` -### 2. 개별 모듈 사용 +### 3. 개별 모듈 사용 #### HTTP API 사용 ```csharp @@ -108,59 +144,69 @@ await wsManager.SendChatMessageAsync( ## ⚙️ 설정 -### 테스트 환경 설정 -```csharp -// localhost:7900으로 설정 -var apiConfig = ApiConfig.CreateDevelopmentConfig(); -var wsConfig = WebSocketConfig.CreateDevelopmentConfig(); -``` - -### 프로덕션 환경 설정 -```csharp -// 실제 서버로 설정 -var apiConfig = ApiConfig.CreateProductionConfig(); -var wsConfig = WebSocketConfig.CreateProductionConfig(); -``` - -### WebSocketConfig 설정 -```csharp -// ScriptableObject로 생성 -var wsConfig = WebSocketConfig.CreateProductionConfig(); -wsConfig.BaseUrl = "ws://122.153.130.223:7900/ws"; -``` - -## 🔧 WebSocket 시뮬레이션 특별 기능 - -### 1. 패키지 의존성 없음 -- 외부 패키지 설치 없이 즉시 사용 가능 -- 개발/테스트에 최적화 - -### 2. 크로스 플랫폼 지원 -- ✅ Unity Desktop -- ✅ Unity WebGL -- ✅ Unity Android -- ✅ Unity iOS - -### 3. 시뮬레이션 모드 -```csharp -// 실제 WebSocket 연결 대신 시뮬레이션 -// 개발/테스트 단계에서 안전하게 사용 -Debug.Log("WebSocket 시뮬레이션 연결"); -``` +### Unity 표준 ScriptableObject 설정 관리 + +#### 1. 설정 파일 생성 +1. **NetworkConfig.asset** 생성: + - `Assets/Resources/` 폴더에 `NetworkConfig.asset` 파일 생성 + - Unity Editor에서 우클릭 > Create > ProjectVG > Network > NetworkConfig + +#### 2. 환경별 서버 주소 +- **개발 환경**: `localhost:7900` +- **테스트 환경**: `localhost:7900` +- **프로덕션 환경**: `122.153.130.223:7900` + +#### 3. 설정값들 (Editor에서 설정 가능) +- **HTTP 타임아웃**: 30초 +- **최대 재시도**: 3회 +- **WebSocket 타임아웃**: 30초 +- **자동 재연결**: 활성화 +- **하트비트**: 활성화 (30초 간격) + +#### 4. 런타임 보안 +- ✅ 앱 시작 시 한 번 설정 +- ✅ 런타임 중 설정 변경 불가 +- ✅ 어디서든 동일한 설정 접근 +- ✅ ScriptableObject로 일관성 보장 +- ✅ Editor에서 설정 가능 +- ✅ 팀 협업 용이 +- ✅ NetworkConfig 강제 사용으로 일관성 보장 + +## 🔧 플랫폼별 WebSocket 구현 + +### 1. DesktopWebSocket (데스크톱) +- System.Net.WebSockets.ClientWebSocket 사용 +- Windows/Mac/Linux 지원 +- 최고 성능 + +### 2. WebGLWebSocket (브라우저) +- UnityWebRequest.WebSocket 사용 +- WebGL 플랫폼 지원 +- 브라우저 제약사항 대응 + +### 3. MobileWebSocket (모바일) +- 네이티브 WebSocket 라이브러리 사용 +- iOS/Android 지원 +- 네이티브 성능 + +### 4. WebSocketFactory +- 플랫폼별 WebSocket 구현 생성 +- 컴파일 타임에 적절한 구현체 선택 ## 🐛 문제 해결 -### 시뮬레이션 모드 +### 플랫폼별 WebSocket ``` -WebSocket 시뮬레이션 연결: ws://localhost:7900/ws -WebSocket 시뮬레이션 메시지: ... +데스크톱 플랫폼용 WebSocket 생성 +WebSocket 연결 시도: ws://localhost:7900/ws +WebSocket 연결 성공 ``` -**설명:** 실제 WebSocket 연결 대신 시뮬레이션으로 동작합니다. +**설명:** 플랫폼에 따라 적절한 WebSocket 구현체가 자동으로 선택됩니다. -### 개발/테스트 -- 실제 서버 연결 없이도 개발 가능 -- 로그를 통해 메시지 흐름 확인 -- 안전한 테스트 환경 제공 +### 플랫폼별 특징 +- **Desktop**: .NET ClientWebSocket으로 최고 성능 +- **WebGL**: 브라우저 WebSocket API 사용 +- **Mobile**: 네이티브 라이브러리로 최적화 ### 테스트 실행 방법 1. **NetworkTestManager** 컴포넌트를 씬에 추가 diff --git a/Assets/Infrastructure/Network/Services/ApiServiceManager.cs b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs index d6be76a..08f14da 100644 --- a/Assets/Infrastructure/Network/Services/ApiServiceManager.cs +++ b/Assets/Infrastructure/Network/Services/ApiServiceManager.cs @@ -15,7 +15,7 @@ public static ApiServiceManager Instance { if (_instance == null) { - _instance = FindObjectOfType(); + _instance = FindFirstObjectByType(); if (_instance == null) { GameObject go = new GameObject("ApiServiceManager"); diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs similarity index 70% rename from Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs rename to Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs index b533581..d147294 100644 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs @@ -9,9 +9,10 @@ namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms { /// - /// 실제 WebSocket 연결을 수행하는 구현체 + /// 데스크톱 플랫폼용 WebSocket 구현체 + /// System.Net.WebSockets.ClientWebSocket을 사용합니다. /// - public class RealWebSocket : INativeWebSocket + public class DesktopWebSocket : INativeWebSocket { public bool IsConnected { get; private set; } public bool IsConnecting { get; private set; } @@ -26,7 +27,7 @@ public class RealWebSocket : INativeWebSocket private CancellationTokenSource _cancellationTokenSource; private bool _isDisposed = false; - public RealWebSocket() + public DesktopWebSocket() { _webSocket = new ClientWebSocket(); _cancellationTokenSource = new CancellationTokenSource(); @@ -43,11 +44,8 @@ public async UniTask ConnectAsync(string url, CancellationToken cancellati try { - Debug.Log($"실제 WebSocket 연결 시도: {url}"); - - // HTTP URL을 WebSocket URL로 변환 (HTTPS 우선 사용) var wsUrl = url.Replace("http://", "wss://").Replace("https://", "wss://"); - Debug.Log($"변환된 WebSocket URL: {wsUrl}"); + Debug.Log($"Desktop WebSocket 연결: {wsUrl}"); var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; @@ -55,8 +53,6 @@ public async UniTask ConnectAsync(string url, CancellationToken cancellati IsConnected = true; IsConnecting = false; - - Debug.Log("실제 WebSocket 연결 성공"); OnConnected?.Invoke(); // 메시지 수신 루프 시작 @@ -67,7 +63,7 @@ public async UniTask ConnectAsync(string url, CancellationToken cancellati catch (Exception ex) { IsConnecting = false; - var error = $"WebSocket 연결 실패: {ex.Message}"; + var error = $"Desktop WebSocket 연결 실패: {ex.Message}"; Debug.LogError(error); OnError?.Invoke(error); return false; @@ -78,32 +74,24 @@ public async UniTask DisconnectAsync() { if (!IsConnected) { - Debug.Log("WebSocket이 이미 연결 해제됨"); return; } try { - Debug.Log($"WebSocket 연결 해제 시작 - 상태: {_webSocket.State}"); IsConnected = false; IsConnecting = false; if (_webSocket.State == WebSocketState.Open) { - Debug.Log("WebSocket 정상 종료 시도"); await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnect", CancellationToken.None); } - else - { - Debug.Log($"WebSocket 상태가 Open이 아님: {_webSocket.State}"); - } - Debug.Log("실제 WebSocket 연결 해제 완료"); OnDisconnected?.Invoke(); } catch (Exception ex) { - Debug.LogError($"WebSocket 연결 해제 중 오류: {ex.Message}"); + Debug.LogError($"Desktop WebSocket 연결 해제 중 오류: {ex.Message}"); } } @@ -111,7 +99,7 @@ public async UniTask SendMessageAsync(string message, CancellationToken ca { if (!IsConnected || _webSocket.State != WebSocketState.Open) { - Debug.LogWarning("WebSocket이 연결되지 않았습니다."); + Debug.LogWarning("Desktop WebSocket이 연결되지 않았습니다."); return false; } @@ -119,12 +107,11 @@ public async UniTask SendMessageAsync(string message, CancellationToken ca { var buffer = Encoding.UTF8.GetBytes(message); await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); - Debug.Log($"WebSocket 메시지 전송: {message}"); return true; } catch (Exception ex) { - Debug.LogError($"WebSocket 메시지 전송 실패: {ex.Message}"); + Debug.LogError($"Desktop WebSocket 메시지 전송 실패: {ex.Message}"); return false; } } @@ -133,19 +120,18 @@ public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancel { if (!IsConnected || _webSocket.State != WebSocketState.Open) { - Debug.LogWarning("WebSocket이 연결되지 않았습니다."); + Debug.LogWarning("Desktop WebSocket이 연결되지 않았습니다."); return false; } try { await _webSocket.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, cancellationToken); - Debug.Log($"WebSocket 바이너리 전송: {data.Length} bytes"); return true; } catch (Exception ex) { - Debug.LogError($"WebSocket 바이너리 전송 실패: {ex.Message}"); + Debug.LogError($"Desktop WebSocket 바이너리 전송 실패: {ex.Message}"); return false; } } @@ -156,31 +142,24 @@ private async Task ReceiveLoopAsync() try { - Debug.Log("WebSocket 수신 루프 시작"); while (IsConnected && _webSocket.State == WebSocketState.Open) { - Debug.Log($"WebSocket 상태: {_webSocket.State}, 메시지 대기 중..."); var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), _cancellationTokenSource.Token); - Debug.Log($"WebSocket 메시지 수신: 타입={result.MessageType}, 크기={result.Count}, 종료={result.EndOfMessage}"); - if (result.MessageType == WebSocketMessageType.Close) { - Debug.Log("서버에서 연결 종료 요청"); + Debug.Log("Desktop WebSocket: 서버에서 연결 종료 요청"); break; } else if (result.MessageType == WebSocketMessageType.Text) { var message = Encoding.UTF8.GetString(buffer, 0, result.Count); - Debug.Log($"WebSocket 텍스트 메시지 수신: {message}"); - Debug.Log($"메시지 길이: {message.Length}, 내용: '{message}'"); OnMessageReceived?.Invoke(message); } else if (result.MessageType == WebSocketMessageType.Binary) { var data = new byte[result.Count]; Array.Copy(buffer, data, result.Count); - Debug.Log($"WebSocket 바이너리 메시지 수신: {result.Count} bytes"); OnBinaryDataReceived?.Invoke(data); } } @@ -189,14 +168,12 @@ private async Task ReceiveLoopAsync() { if (!_isDisposed) { - Debug.LogError($"WebSocket 수신 루프 오류: {ex.Message}"); - Debug.LogError($"스택 트레이스: {ex.StackTrace}"); + Debug.LogError($"Desktop WebSocket 수신 루프 오류: {ex.Message}"); OnError?.Invoke(ex.Message); } } finally { - Debug.Log("WebSocket 수신 루프 종료"); IsConnected = false; if (!_isDisposed) { diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs.meta new file mode 100644 index 0000000..8b9b5bd --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ecf4142f21c17ee48a6646dd7e267fec \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs index a6c6fe6..753b562 100644 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs @@ -6,8 +6,8 @@ namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms { /// - /// 모바일 환경에 최적화된 WebSocket 시뮬레이션 구현체 - /// 개발/테스트용으로만 사용 + /// 모바일 플랫폼용 WebSocket 구현체 + /// iOS/Android 네이티브 WebSocket 라이브러리를 사용합니다. /// public class MobileWebSocket : INativeWebSocket { @@ -17,8 +17,18 @@ public class MobileWebSocket : INativeWebSocket public event Action OnConnected; public event Action OnDisconnected; public event Action OnError; +#pragma warning disable CS0067 public event Action OnMessageReceived; public event Action OnBinaryDataReceived; +#pragma warning restore CS0067 + + private CancellationTokenSource _cancellationTokenSource; + private bool _isDisposed = false; + + public MobileWebSocket() + { + _cancellationTokenSource = new CancellationTokenSource(); + } public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) { @@ -31,56 +41,138 @@ public async UniTask ConnectAsync(string url, CancellationToken cancellati try { - Debug.Log($"모바일 WebSocket 시뮬레이션 연결: {url}"); - await UniTask.Delay(100, cancellationToken: cancellationToken); + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + // TODO : 모바일에서는 네이티브 WebSocket 라이브러리 사용 + // TODO : 현재는 기본 구현만 제공 (실제 구현 시 네이티브 플러그인 필요) + + // TODO : 임시로 성공 시뮬레이션 + await UniTask.Delay(100, cancellationToken: combinedCancellationToken); IsConnected = true; IsConnecting = false; OnConnected?.Invoke(); + // 메시지 수신 루프 시작 + _ = ReceiveLoopAsync(); + return true; } catch (Exception ex) { IsConnecting = false; - OnError?.Invoke(ex.Message); + var error = $"모바일 WebSocket 연결 중 예외 발생: {ex.Message}"; + Debug.LogError(error); + OnError?.Invoke(error); return false; } } public async UniTask DisconnectAsync() { - IsConnected = false; - IsConnecting = false; - OnDisconnected?.Invoke(); - await UniTask.CompletedTask; + if (!IsConnected) + { + return; + } + + try + { + IsConnected = false; + IsConnecting = false; + + // TODO : 네이티브 WebSocket 연결 해제 + // TODO : 실제 구현 시 네이티브 플러그인 호출 + await UniTask.CompletedTask; // 비동기 작업 시뮬레이션 + + OnDisconnected?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError($"모바일 WebSocket 연결 해제 중 오류: {ex.Message}"); + } } public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) { if (!IsConnected) { + Debug.LogWarning("모바일 WebSocket이 연결되지 않았습니다."); return false; } - Debug.Log($"모바일 WebSocket 시뮬레이션 메시지: {message}"); - return true; + try + { + // TODO : 네이티브 WebSocket 메시지 전송 + // TODO : 실제 구현 시 네이티브 플러그인 호출 + await UniTask.CompletedTask; // 비동기 작업 시뮬레이션 + return true; + } + catch (Exception ex) + { + Debug.LogError($"모바일 WebSocket 메시지 전송 실패: {ex.Message}"); + return false; + } } public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) { if (!IsConnected) { + Debug.LogWarning("모바일 WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + // TODO : 네이티브 WebSocket 바이너리 전송 + // TODO : 실제 구현 시 네이티브 플러그인 호출 + await UniTask.CompletedTask; // 비동기 작업 시뮬레이션 + return true; + } + catch (Exception ex) + { + Debug.LogError($"모바일 WebSocket 바이너리 전송 실패: {ex.Message}"); return false; } + } - Debug.Log($"모바일 WebSocket 시뮬레이션 바이너리: {data.Length} bytes"); - return true; + private async UniTask ReceiveLoopAsync() + { + try + { + while (IsConnected && !_isDisposed) + { + // TODO : 네이티브 WebSocket 메시지 수신 + // TODO : 실제 구현 시 네이티브 플러그인에서 메시지 수신 + await UniTask.Delay(100); + } + } + catch (Exception ex) + { + if (!_isDisposed) + { + Debug.LogError($"모바일 WebSocket 수신 루프 오류: {ex.Message}"); + OnError?.Invoke(ex.Message); + } + } + finally + { + IsConnected = false; + if (!_isDisposed) + { + OnDisconnected?.Invoke(); + } + } } public void Dispose() { - DisconnectAsync().Forget(); + if (_isDisposed) + return; + + _isDisposed = true; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); } } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs.meta deleted file mode 100644 index e0f3754..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/RealWebSocket.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 543c199ba5388224c8deace64fdcacc6 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs deleted file mode 100644 index b0fe1c1..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Threading; -using UnityEngine; -using Cysharp.Threading.Tasks; - -namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms -{ - /// - /// Unity WebSocket 시뮬레이션 구현체 - /// 개발/테스트용으로만 사용 - /// - public class UnityWebSocket : INativeWebSocket - { - public bool IsConnected { get; private set; } - public bool IsConnecting { get; private set; } - - public event Action OnConnected; - public event Action OnDisconnected; - public event Action OnError; - public event Action OnMessageReceived; - public event Action OnBinaryDataReceived; - - public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) - { - if (IsConnected || IsConnecting) - { - return IsConnected; - } - - IsConnecting = true; - - try - { - Debug.Log($"WebSocket 시뮬레이션 연결: {url}"); - await UniTask.Delay(100, cancellationToken: cancellationToken); - - IsConnected = true; - IsConnecting = false; - OnConnected?.Invoke(); - - return true; - } - catch (Exception ex) - { - IsConnecting = false; - OnError?.Invoke(ex.Message); - return false; - } - } - - public async UniTask DisconnectAsync() - { - IsConnected = false; - IsConnecting = false; - OnDisconnected?.Invoke(); - await UniTask.CompletedTask; - } - - public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) - { - if (!IsConnected) - { - return false; - } - - Debug.Log($"WebSocket 시뮬레이션 메시지: {message}"); - return true; - } - - public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) - { - if (!IsConnected) - { - return false; - } - - Debug.Log($"WebSocket 시뮬레이션 바이너리: {data.Length} bytes"); - return true; - } - - public void Dispose() - { - DisconnectAsync().Forget(); - } - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs.meta deleted file mode 100644 index e81aad8..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/UnityWebSocket.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b2d311f801b0bf64e9f2a0045935857e \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs new file mode 100644 index 0000000..bbf7e0b --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs @@ -0,0 +1,191 @@ +using System; +using System.Threading; +using UnityEngine; +using Cysharp.Threading.Tasks; +using UnityEngine.Networking; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms +{ + /// + /// WebGL 플랫폼용 WebSocket 구현체 + /// UnityWebRequest.WebSocket을 사용합니다. + /// + public class WebGLWebSocket : INativeWebSocket + { + public bool IsConnected { get; private set; } + public bool IsConnecting { get; private set; } + + public event Action OnConnected; + public event Action OnDisconnected; + public event Action OnError; +#pragma warning disable CS0067 + public event Action OnMessageReceived; + public event Action OnBinaryDataReceived; +#pragma warning restore CS0067 + + private UnityWebRequest _webRequest; + private CancellationTokenSource _cancellationTokenSource; + private bool _isDisposed = false; + + public WebGLWebSocket() + { + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) + { + if (IsConnected || IsConnecting) + { + return IsConnected; + } + + IsConnecting = true; + + try + { + var combinedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + // UnityWebRequest.WebSocket 사용 + _webRequest = UnityWebRequest.Get(url); + _webRequest.SetRequestHeader("Upgrade", "websocket"); + _webRequest.SetRequestHeader("Connection", "Upgrade"); + + var operation = _webRequest.SendWebRequest(); + await operation.WithCancellation(combinedCancellationToken); + + if (_webRequest.result == UnityWebRequest.Result.Success) + { + IsConnected = true; + IsConnecting = false; + OnConnected?.Invoke(); + + // 메시지 수신 루프 시작 + _ = ReceiveLoopAsync(); + + return true; + } + else + { + var error = $"WebGL WebSocket 연결 실패: {_webRequest.error}"; + Debug.LogError(error); + OnError?.Invoke(error); + return false; + } + } + catch (Exception ex) + { + IsConnecting = false; + var error = $"WebGL WebSocket 연결 중 예외 발생: {ex.Message}"; + Debug.LogError(error); + OnError?.Invoke(error); + return false; + } + } + + public async UniTask DisconnectAsync() + { + if (!IsConnected) + { + return; + } + + try + { + IsConnected = false; + IsConnecting = false; + + _webRequest?.Abort(); + _webRequest?.Dispose(); + _webRequest = null; + + await UniTask.CompletedTask; // 비동기 작업 시뮬레이션 + + OnDisconnected?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError($"WebGL WebSocket 연결 해제 중 오류: {ex.Message}"); + } + } + + public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + Debug.LogWarning("WebGL WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + // TODO : WebGL에서는 WebSocket 메시지 전송을 위한 별도 구현 필요 + await UniTask.CompletedTask; + return true; + } + catch (Exception ex) + { + Debug.LogError($"WebGL WebSocket 메시지 전송 실패: {ex.Message}"); + return false; + } + } + + public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) + { + if (!IsConnected) + { + Debug.LogWarning("WebGL WebSocket이 연결되지 않았습니다."); + return false; + } + + try + { + await UniTask.CompletedTask; + return true; + } + catch (Exception ex) + { + Debug.LogError($"WebGL WebSocket 바이너리 전송 실패: {ex.Message}"); + return false; + } + } + + private async UniTask ReceiveLoopAsync() + { + try + { + while (IsConnected && !_isDisposed) + { + // TODO : WebGL에서는 WebSocket 메시지 수신을 위한 별도 구현 필요 + await UniTask.Delay(100); + } + } + catch (Exception ex) + { + if (!_isDisposed) + { + Debug.LogError($"WebGL WebSocket 수신 루프 오류: {ex.Message}"); + OnError?.Invoke(ex.Message); + } + } + finally + { + IsConnected = false; + if (!_isDisposed) + { + OnDisconnected?.Invoke(); + } + } + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _webRequest?.Dispose(); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs.meta new file mode 100644 index 0000000..3705f86 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 21c0c4d6a3b08134d975cd3c476a8dd6 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs deleted file mode 100644 index 2001241..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Threading; -using UnityEngine; -using Cysharp.Threading.Tasks; - -namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms -{ - /// - /// WebSocket 시뮬레이션 구현체 - /// 개발/테스트용으로만 사용 - /// - public class WebSocketSharpFallback : INativeWebSocket - { - public bool IsConnected { get; private set; } - public bool IsConnecting { get; private set; } - - public event Action OnConnected; - public event Action OnDisconnected; - public event Action OnError; - public event Action OnMessageReceived; - public event Action OnBinaryDataReceived; - - public async UniTask ConnectAsync(string url, CancellationToken cancellationToken = default) - { - if (IsConnected || IsConnecting) - { - return IsConnected; - } - - IsConnecting = true; - - try - { - Debug.Log($"WebSocket 시뮬레이션 연결: {url}"); - await UniTask.Delay(100, cancellationToken: cancellationToken); - - IsConnected = true; - IsConnecting = false; - OnConnected?.Invoke(); - - return true; - } - catch (Exception ex) - { - IsConnecting = false; - OnError?.Invoke(ex.Message); - return false; - } - } - - public async UniTask DisconnectAsync() - { - IsConnected = false; - IsConnecting = false; - OnDisconnected?.Invoke(); - await UniTask.CompletedTask; - } - - public async UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default) - { - if (!IsConnected) - { - return false; - } - - Debug.Log($"WebSocket 시뮬레이션 메시지: {message}"); - return true; - } - - public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) - { - if (!IsConnected) - { - return false; - } - - Debug.Log($"WebSocket 시뮬레이션 바이너리: {data.Length} bytes"); - return true; - } - - public void Dispose() - { - DisconnectAsync().Forget(); - } - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs.meta b/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs.meta deleted file mode 100644 index 12dbb3a..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/WebSocketSharpFallback.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e59441b18c3f45c4f8b114e21d897f1c \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs new file mode 100644 index 0000000..fa186d5 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs @@ -0,0 +1,25 @@ +using UnityEngine; +using ProjectVG.Infrastructure.Network.WebSocket.Platforms; + +namespace ProjectVG.Infrastructure.Network.WebSocket +{ + /// + /// 플랫폼별 WebSocket 구현을 생성하는 팩토리 + /// + public static class WebSocketFactory + { + /// + /// 현재 플랫폼에 맞는 WebSocket 구현을 생성합니다. + /// + public static INativeWebSocket Create() + { + #if UNITY_WEBGL && !UNITY_EDITOR + return new WebGLWebSocket(); + #elif UNITY_IOS || UNITY_ANDROID + return new MobileWebSocket(); + #else + return new DesktopWebSocket(); + #endif + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs.meta b/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs.meta new file mode 100644 index 0000000..6b9be90 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60c74fa19d97fbb46930ab87d63ab193 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs index f822b36..1f8f215 100644 --- a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs @@ -17,8 +17,8 @@ namespace ProjectVG.Infrastructure.Network.WebSocket /// public class WebSocketManager : MonoBehaviour { - [Header("WebSocket Configuration")] - [SerializeField] private WebSocketConfig webSocketConfig; + [Header("WebSocket Configuration")] + // NetworkConfig를 사용하여 설정을 관리합니다. private INativeWebSocket _nativeWebSocket; private CancellationTokenSource _cancellationTokenSource; @@ -67,39 +67,27 @@ private void InitializeManager() { _cancellationTokenSource = new CancellationTokenSource(); - if (webSocketConfig == null) - { - Debug.LogWarning("WebSocketConfig가 설정되지 않았습니다. 기본 설정을 사용합니다."); - } - // Native WebSocket 초기화 InitializeNativeWebSocket(); } - private void InitializeNativeWebSocket() - { - // 실제 WebSocket 구현체 사용 - _nativeWebSocket = new RealWebSocket(); - Debug.Log("실제 WebSocket 구현체를 사용합니다."); - - // 이벤트 연결 - _nativeWebSocket.OnConnected += OnNativeConnected; - _nativeWebSocket.OnDisconnected += OnNativeDisconnected; - _nativeWebSocket.OnError += OnNativeError; - _nativeWebSocket.OnMessageReceived += OnNativeMessageReceived; - _nativeWebSocket.OnBinaryDataReceived += OnNativeBinaryDataReceived; - } - - /// - /// WebSocketConfig 설정 - /// - public void SetWebSocketConfig(WebSocketConfig config) + private void InitializeNativeWebSocket() { - webSocketConfig = config; + // 플랫폼별 WebSocket 구현체 생성 + _nativeWebSocket = WebSocketFactory.Create(); + + // 이벤트 연결 + _nativeWebSocket.OnConnected += OnNativeConnected; + _nativeWebSocket.OnDisconnected += OnNativeDisconnected; + _nativeWebSocket.OnError += OnNativeError; + _nativeWebSocket.OnMessageReceived += OnNativeMessageReceived; + _nativeWebSocket.OnBinaryDataReceived += OnNativeBinaryDataReceived; } + + /// /// 핸들러 등록 /// @@ -347,19 +335,8 @@ private void ProcessAudioData(byte[] audioData) /// private string GetWebSocketUrl(string sessionId = null) { - string baseUrl; + string baseUrl = NetworkConfig.GetWebSocketUrl(); - if (webSocketConfig != null) - { - baseUrl = webSocketConfig.GetWebSocketUrl(); - } - else - { - // 기본값 (base64 인코딩된 IP:Port 사용) - baseUrl = "ws://MTIyLjE1My4xMzAuMjIzOjc5MDA=/ws"; - } - - // 세션 ID가 있으면 쿼리 파라미터로 추가 (더미 클라이언트와 동일) if (!string.IsNullOrEmpty(sessionId)) { return $"{baseUrl}?sessionId={sessionId}"; @@ -375,17 +352,19 @@ private string GetWebSocketUrl(string sessionId = null) /// private async UniTaskVoid TryReconnectAsync() { - var config = webSocketConfig ?? CreateDefaultWebSocketConfig(); + bool autoReconnect = NetworkConfig.AutoReconnect; + int maxReconnectAttempts = NetworkConfig.MaxReconnectAttempts; + float reconnectDelay = NetworkConfig.ReconnectDelay; - if (!config.AutoReconnect || _reconnectAttempts >= config.MaxReconnectAttempts) + if (!autoReconnect || _reconnectAttempts >= maxReconnectAttempts) { return; } _reconnectAttempts++; - Debug.Log($"WebSocket 재연결 시도 {_reconnectAttempts}/{config.MaxReconnectAttempts}"); + Debug.Log($"WebSocket 재연결 시도 {_reconnectAttempts}/{maxReconnectAttempts}"); - await UniTask.Delay(TimeSpan.FromSeconds(config.ReconnectDelay)); + await UniTask.Delay(TimeSpan.FromSeconds(reconnectDelay)); if (!_isConnected) { @@ -393,13 +372,7 @@ private async UniTaskVoid TryReconnectAsync() } } - /// - /// 기본 WebSocket 설정 생성 - /// - private WebSocketConfig CreateDefaultWebSocketConfig() - { - return WebSocketConfig.CreateDevelopmentConfig(); - } + #region Native WebSocket Event Handlers diff --git a/Assets/Tests/Runtime/NetworkTestManager.cs b/Assets/Tests/Runtime/NetworkTestManager.cs index 80ab66e..9ad7ea7 100644 --- a/Assets/Tests/Runtime/NetworkTestManager.cs +++ b/Assets/Tests/Runtime/NetworkTestManager.cs @@ -123,6 +123,9 @@ private void InitializeManagers() { try { + // NetworkConfig 초기화 (앱 시작 시 환경 설정) + NetworkConfig.SetDevelopmentEnvironment(); // 또는 SetTestEnvironment(), SetProductionEnvironment() + // WebSocket 매니저 초기화 _webSocketManager = WebSocketManager.Instance; if (_webSocketManager == null) @@ -131,17 +134,13 @@ private void InitializeManagers() return; } - // WebSocket 설정 적용 (localhost:7900 사용) - var webSocketConfig = ProjectVG.Infrastructure.Network.Configs.WebSocketConfig.CreateDevelopmentConfig(); - _webSocketManager.SetWebSocketConfig(webSocketConfig); - Debug.Log($"WebSocket 설정 적용: {webSocketConfig.GetWebSocketUrl()}"); + Debug.Log($"WebSocket 설정 적용: {NetworkConfig.GetWebSocketUrl()}"); + Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); - // API 설정 적용 (localhost:7900 사용) - var apiConfig = ProjectVG.Infrastructure.Network.Configs.ApiConfig.CreateDevelopmentConfig(); + // HTTP API 클라이언트 설정 if (HttpApiClient.Instance != null) { - HttpApiClient.Instance.SetApiConfig(apiConfig); - Debug.Log($"API 설정 적용: {apiConfig.GetFullUrl("chat")}"); + Debug.Log($"API 설정 적용: {NetworkConfig.GetFullApiUrl("chat")}"); } // API 서비스 매니저 초기화 @@ -171,6 +170,7 @@ private void InitializeManagers() _webSocketHandler.OnSessionIdMessageReceivedEvent += OnSessionIdMessageReceived; Debug.Log("NetworkTestManager 초기화 완료"); + NetworkConfig.LogCurrentSettings(); } catch (Exception ex) { @@ -198,6 +198,11 @@ public async void ConnectWebSocket() try { Debug.Log("=== WebSocket 연결 시작 (더미 클라이언트 방식) ==="); + + // 현재 설정 정보 출력 + Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + _receivedSessionId = null; // 세션 ID 초기화 // 더미 클라이언트처럼 세션 ID 없이 연결 @@ -244,6 +249,11 @@ public async void SendChatRequest() { Debug.Log("=== HTTP 채팅 요청 시작 (더미 클라이언트 방식) ==="); + // 현재 설정 정보 출력 + Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("chat")}"); + Debug.Log($"세션 ID: {_receivedSessionId}"); + var chatRequest = new ProjectVG.Infrastructure.Network.DTOs.Chat.ChatRequest { message = "안녕하세요! 테스트 메시지입니다.", @@ -369,6 +379,11 @@ public async void RunDummyClientTest() { Debug.Log("🚀 === 더미 클라이언트 방식 전체 테스트 시작 ==="); + // 현재 설정 정보 출력 + Debug.Log($"테스트 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("")}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + // 0. 기존 연결이 있으면 해제 if (_webSocketManager.IsConnected) { @@ -458,6 +473,11 @@ public async void RunFullTest() { Debug.Log("🚀 === 전체 테스트 시작 ==="); + // 현재 설정 정보 출력 + Debug.Log($"테스트 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("")}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + // 1. WebSocket 연결 Debug.Log("1️⃣ WebSocket 연결 중..."); bool connected = await _webSocketManager.ConnectAsync(testSessionId); @@ -503,6 +523,13 @@ public async void RunFullTest() } } + [ContextMenu("현재 네트워크 설정 정보 출력")] + public void LogCurrentNetworkConfig() + { + Debug.Log("=== 현재 네트워크 설정 정보 ==="); + NetworkConfig.LogCurrentSettings(); + } + #endregion #region 자동 테스트 @@ -525,6 +552,11 @@ private async UniTaskVoid StartAutoTest() return; } + // 현재 설정 정보 출력 + Debug.Log($"자동 테스트 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("")}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + while (!_cancellationTokenSource.Token.IsCancellationRequested) { try @@ -565,6 +597,9 @@ private async UniTask RunDummyClientTestInternal() try { + // 현재 설정 정보 출력 + Debug.Log($"자동 테스트 환경: {NetworkConfig.CurrentEnvironment}"); + // 0. 기존 연결이 있으면 해제 if (_webSocketManager.IsConnected) { diff --git a/Assets/Tests/Runtime/NetworkTestUI.cs b/Assets/Tests/Runtime/NetworkTestUI.cs index 22201e7..90bae8d 100644 --- a/Assets/Tests/Runtime/NetworkTestUI.cs +++ b/Assets/Tests/Runtime/NetworkTestUI.cs @@ -34,7 +34,7 @@ public class NetworkTestUI : MonoBehaviour private void Start() { - _testManager = FindObjectOfType(); + _testManager = FindFirstObjectByType(); if (_testManager == null) { Debug.LogError("NetworkTestManager를 찾을 수 없습니다!"); diff --git a/Assets/Tests/Sences/NetworkTestScene.unity b/Assets/Tests/Sences/NetworkTestScene.unity index 7f5b9d1..0d1d109 100644 --- a/Assets/Tests/Sences/NetworkTestScene.unity +++ b/Assets/Tests/Sences/NetworkTestScene.unity @@ -130,6 +130,7 @@ GameObject: - component: {fileID: 519420032} - component: {fileID: 519420031} - component: {fileID: 519420029} + - component: {fileID: 519420033} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera @@ -211,6 +212,50 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &519420033 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 --- !u!1001 &757794372 PrefabInstance: m_ObjectHideFlags: 0 @@ -367,8 +412,8 @@ MonoBehaviour: testSessionId: test-session-123 testCharacterId: 44444444-4444-4444-4444-444444444444 testUserId: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb - autoTest: 0 - testInterval: 10 + autoTest: 1 + testInterval: 1 --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 From 5c07f589cdada02a5d94a030eacb6ba299b14629 Mon Sep 17 00:00:00 2001 From: WooSH Date: Tue, 29 Jul 2025 14:29:12 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=EB=B0=A9=EB=B2=95=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20=EB=B8=8C=EB=A6=BF=EC=A7=80=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 브릿지 패턴을 사용하여 json와 바이너리 방식 두가지 방법 중 하나를 선택하여 처리하도록 수정 --- .../Network/Configs/NetworkConfig.cs | 16 + .../Network/Configs/ServerConfig.cs | 59 ++++ .../Network/Configs/ServerConfig.cs.meta | 2 + .../Network/Configs/ServerConfigLoader.cs | 112 +++++++ .../Configs/ServerConfigLoader.cs.meta | 2 + .../DTOs/WebSocket/IntegratedMessage.cs | 50 +++ .../DTOs/WebSocket/IntegratedMessage.cs.meta | 2 + .../WebSocket/DefaultWebSocketHandler.cs | 11 + .../Network/WebSocket/IWebSocketHandler.cs | 6 + .../Network/WebSocket/Processors.meta | 8 + .../Processors/BinaryMessageProcessor.cs | 201 ++++++++++++ .../Processors/BinaryMessageProcessor.cs.meta | 2 + .../WebSocket/Processors/IMessageProcessor.cs | 37 +++ .../Processors/IMessageProcessor.cs.meta | 2 + .../Processors/JsonMessageProcessor.cs | 153 +++++++++ .../Processors/JsonMessageProcessor.cs.meta | 2 + .../Processors/MessageProcessorFactory.cs | 81 +++++ .../MessageProcessorFactory.cs.meta | 2 + .../Network/WebSocket/WebSocketManager.cs | 300 ++++++++++-------- Assets/Tests/Runtime/NetworkTestManager.cs | 127 +++++++- 20 files changed, 1045 insertions(+), 130 deletions(-) create mode 100644 Assets/Infrastructure/Network/Configs/ServerConfig.cs create mode 100644 Assets/Infrastructure/Network/Configs/ServerConfig.cs.meta create mode 100644 Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs create mode 100644 Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs.meta create mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs create mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs create mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs.meta diff --git a/Assets/Infrastructure/Network/Configs/NetworkConfig.cs b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs index f4518df..e69bdde 100644 --- a/Assets/Infrastructure/Network/Configs/NetworkConfig.cs +++ b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs @@ -35,6 +35,7 @@ public class NetworkConfig : ScriptableObject [SerializeField] private int maxMessageSize = 65536; // 64KB [SerializeField] private float messageTimeout = 10f; [SerializeField] private bool enableMessageLogging = true; + [SerializeField] private string wsMessageType = "json"; // "json" 또는 "binary" [Header("Common Settings")] [SerializeField] private string userAgent = "ProjectVG-Client/1.0"; @@ -206,6 +207,21 @@ public static string WebSocketServerAddress /// public static bool EnableMessageLogging => Instance.enableMessageLogging; + /// + /// WebSocket 메시지 타입 + /// + public static string WebSocketMessageType => Instance.wsMessageType; + + /// + /// JSON 메시지 타입인지 확인 + /// + public static bool IsJsonMessageType => Instance.wsMessageType?.ToLower() == "json"; + + /// + /// 바이너리 메시지 타입인지 확인 + /// + public static bool IsBinaryMessageType => Instance.wsMessageType?.ToLower() == "binary"; + /// /// 사용자 에이전트 /// diff --git a/Assets/Infrastructure/Network/Configs/ServerConfig.cs b/Assets/Infrastructure/Network/Configs/ServerConfig.cs new file mode 100644 index 0000000..8ea77ca --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ServerConfig.cs @@ -0,0 +1,59 @@ +namespace ProjectVG.Infrastructure.Network.Configs +{ + /// + /// 서버 설정 정보 (현재 미사용) + /// 서버의 메시지 형식과 지원 기능을 정의합니다. + /// WebSocket뿐만 아니라 다른 서버 설정도 포함할 수 있습니다. + /// + [System.Serializable] + public class ServerConfig + { + /// + /// 메시지 타입 ("json" 또는 "binary") + /// + public string messageType; + + /// + /// 서버 버전 + /// + public string version; + + /// + /// 오디오 지원 여부 + /// + public bool supportsAudio; + + /// + /// 바이너리 메시지 지원 여부 + /// + public bool supportsBinary; + + /// + /// 오디오 형식 (예: "wav", "mp3") + /// + public string audioFormat; + + /// + /// 최대 메시지 크기 (바이트) + /// + public int maxMessageSize; + + /// + /// JSON 형식인지 확인 + /// + public bool IsJsonFormat => messageType?.ToLower() == "json"; + + /// + /// 바이너리 형식인지 확인 + /// + public bool IsBinaryFormat => messageType?.ToLower() == "binary"; + + /// + /// 설정 정보를 문자열로 반환 + /// + public override string ToString() + { + return $"ServerConfig[Type: {messageType}, Version: {version}, Audio: {supportsAudio}, Binary: {supportsBinary}, Format: {audioFormat}, MaxSize: {maxMessageSize} bytes]"; + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ServerConfig.cs.meta b/Assets/Infrastructure/Network/Configs/ServerConfig.cs.meta new file mode 100644 index 0000000..d2afddc --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ServerConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: aed6fafe9b2afc74c800a2a8ba99570a \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs new file mode 100644 index 0000000..5685315 --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs @@ -0,0 +1,112 @@ +using System; +using UnityEngine; +using UnityEngine.Networking; +using Cysharp.Threading.Tasks; +using ProjectVG.Infrastructure.Network.Configs; +using ProjectVG.Infrastructure.Network.DTOs.WebSocket; + +namespace ProjectVG.Infrastructure.Network.Configs +{ + /// + /// 서버 설정 로더 (현재 미사용) + /// 서버에서 메시지 타입 등의 설정을 동적으로 로드합니다. + /// WebSocket뿐만 아니라 다른 서버 설정도 로드할 수 있습니다. + /// + public static class ServerConfigLoader + { + /// + /// 서버 설정 로드 + /// + /// 설정 엔드포인트 (기본값: "config") + /// 서버 설정 또는 null + public static async UniTask LoadServerConfigAsync(string configEndpoint = "config") + { + try + { + Debug.Log($"서버 설정 로드 중... (엔드포인트: {configEndpoint})"); + + var configUrl = NetworkConfig.GetFullApiUrl(configEndpoint); + using (var request = UnityWebRequest.Get(configUrl)) + { + request.timeout = 10; + await request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.Success) + { + var jsonResponse = request.downloadHandler.text; + var serverConfig = JsonUtility.FromJson(jsonResponse); + + Debug.Log($"서버 설정 로드 완료: {serverConfig.messageType}"); + return serverConfig; + } + else + { + Debug.LogWarning($"서버 설정 로드 실패: {request.error}"); + return null; + } + } + } + catch (Exception ex) + { + Debug.LogError($"서버 설정 로드 중 오류: {ex.Message}"); + return null; + } + } + + /// + /// 서버 설정 유효성 검사 + /// + /// 서버 설정 + /// 유효한지 여부 + public static bool ValidateServerConfig(ServerConfig config) + { + if (config == null) + { + Debug.LogWarning("서버 설정이 null입니다."); + return false; + } + + if (string.IsNullOrEmpty(config.messageType)) + { + Debug.LogWarning("서버 설정에 메시지 타입이 없습니다."); + return false; + } + + var messageType = config.messageType.ToLower(); + if (messageType != "json" && messageType != "binary") + { + Debug.LogWarning($"지원하지 않는 메시지 타입: {config.messageType}"); + return false; + } + + Debug.Log($"서버 설정 유효성 검사 통과: {config.messageType}"); + return true; + } + + /// + /// 서버 설정과 NetworkConfig 비교 + /// + /// 서버 설정 + /// 일치하는지 여부 + public static bool CompareWithNetworkConfig(ServerConfig serverConfig) + { + if (serverConfig == null) return false; + + var networkConfigType = NetworkConfig.WebSocketMessageType.ToLower(); + var serverConfigType = serverConfig.messageType.ToLower(); + + var isMatch = networkConfigType == serverConfigType; + + if (!isMatch) + { + Debug.LogWarning($"NetworkConfig({networkConfigType})와 서버 설정({serverConfigType})이 일치하지 않습니다."); + } + else + { + Debug.Log($"NetworkConfig와 서버 설정이 일치합니다: {networkConfigType}"); + } + + return isMatch; + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs.meta b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs.meta new file mode 100644 index 0000000..c972348 --- /dev/null +++ b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 709b436d429928c4c8da5256aad82347 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs b/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs new file mode 100644 index 0000000..f9cf0eb --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs @@ -0,0 +1,50 @@ +using System; + +namespace ProjectVG.Infrastructure.Network.DTOs.WebSocket +{ + /// + /// 통합 메시지 (텍스트 + 오디오) + /// 바이너리 형식으로 전송되는 통합 메시지를 위한 DTO + /// + [System.Serializable] + public class IntegratedMessage + { + /// + /// 세션 ID + /// + public string sessionId; + + /// + /// 텍스트 메시지 + /// + public string text; + + /// + /// 오디오 바이너리 데이터 + /// + public byte[] audioData; + + /// + /// 오디오 지속시간 (초) + /// + public float audioDuration; + + /// + /// 텍스트가 있는지 확인 + /// + public bool HasText => !string.IsNullOrEmpty(text); + + /// + /// 오디오가 있는지 확인 + /// + public bool HasAudio => audioData != null && audioData.Length > 0; + + /// + /// 메시지 정보를 문자열로 반환 + /// + public override string ToString() + { + return $"IntegratedMessage[SessionId: {sessionId}, Text: {text?.Length ?? 0} chars, Audio: {audioData?.Length ?? 0} bytes, Duration: {audioDuration:F2}s]"; + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs.meta b/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs.meta new file mode 100644 index 0000000..385c3fb --- /dev/null +++ b/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0b929e806824f0b4fb2f4ac9a21bef23 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs b/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs index 91ae465..db5c125 100644 --- a/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs +++ b/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs @@ -23,6 +23,7 @@ public class DefaultWebSocketHandler : MonoBehaviour, IWebSocketHandler public System.Action OnConnectionMessageReceivedEvent; public System.Action OnSessionIdMessageReceivedEvent; public System.Action OnAudioDataReceivedEvent; + public System.Action OnIntegratedMessageReceivedEvent; private void Start() { @@ -159,6 +160,16 @@ public void OnAudioDataReceived(byte[] audioData) OnAudioDataReceivedEvent?.Invoke(audioData); } + public void OnIntegratedMessageReceived(IntegratedMessage message) + { + if (enableLogging) + { + Debug.Log($"통합 메시지 수신 - 텍스트: {message.text?.Length ?? 0}자, 오디오: {message.audioData?.Length ?? 0}바이트, 지속시간: {message.audioDuration:F2}초"); + } + + OnIntegratedMessageReceivedEvent?.Invoke(message); + } + #endregion } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs b/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs index 00e8e64..4311cf7 100644 --- a/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs +++ b/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs @@ -58,5 +58,11 @@ public interface IWebSocketHandler /// /// 오디오 바이트 데이터 void OnAudioDataReceived(byte[] audioData); + + /// + /// 통합 메시지 수신 시 호출 (텍스트 + 오디오) + /// + /// 통합 메시지 + void OnIntegratedMessageReceived(IntegratedMessage message); } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors.meta b/Assets/Infrastructure/Network/WebSocket/Processors.meta new file mode 100644 index 0000000..dd8cb27 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0069f6496b3bf7468c732dd8bce79a1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs b/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs new file mode 100644 index 0000000..52155cf --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs @@ -0,0 +1,201 @@ +using System; +using System.Text; +using UnityEngine; +using ProjectVG.Infrastructure.Network.DTOs.WebSocket; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Processors +{ + /// + /// 바이너리 메시지 처리기 (Bridge Pattern의 구현체) + /// + public class BinaryMessageProcessor : IMessageProcessor + { + public string MessageType => "binary"; + + // 메시지 타입 상수 + private const byte MESSAGE_TYPE_INTEGRATED = 0x03; + private const byte MESSAGE_TYPE_TEXT_ONLY = 0x01; + private const byte MESSAGE_TYPE_AUDIO_ONLY = 0x02; + + public void ProcessMessage(string message, System.Collections.Generic.List handlers) + { + // 바이너리 프로세서는 문자열 메시지를 처리하지 않음 + Debug.LogWarning("바이너리 프로세서는 문자열 메시지를 처리하지 않습니다."); + } + + public void ProcessBinaryMessage(byte[] data, System.Collections.Generic.List handlers) + { + try + { + Debug.Log($"바이너리 메시지 처리: {data.Length} bytes"); + + // 바이너리 메시지 파싱 시도 + var integratedMessage = ParseBinaryMessage(data); + if (integratedMessage != null) + { + ProcessIntegratedMessage(integratedMessage, handlers); + return; + } + + // 바이너리 파싱 실패 시 순수 오디오 데이터로 처리 + Debug.Log("바이너리 메시지 파싱 실패 - 순수 오디오 데이터로 처리"); + ProcessAudioData(data, handlers); + } + catch (Exception ex) + { + Debug.LogError($"바이너리 메시지 처리 실패: {ex.Message}"); + } + } + + public string ExtractSessionId(string message) + { + // 바이너리 프로세서는 문자열에서 세션 ID를 추출하지 않음 + return null; + } + + /// + /// 바이너리 메시지 파싱 + /// + private IntegratedMessage ParseBinaryMessage(byte[] data) + { + try + { + if (data.Length < 5) // 최소 길이 체크 + { + Debug.LogWarning("바이너리 메시지가 너무 짧습니다."); + return null; + } + + int offset = 0; + + // 메시지 타입 확인 (1바이트) + byte messageType = data[offset]; + offset += 1; + + if (messageType != MESSAGE_TYPE_INTEGRATED) + { + Debug.LogWarning($"지원하지 않는 메시지 타입: {messageType}"); + return null; + } + + // 세션 ID 읽기 + if (offset + 4 > data.Length) return null; + int sessionIdLength = BitConverter.ToInt32(data, offset); + offset += 4; + + if (offset + sessionIdLength > data.Length) return null; + string sessionId = Encoding.UTF8.GetString(data, offset, sessionIdLength); + offset += sessionIdLength; + + // 텍스트 읽기 + if (offset + 4 > data.Length) return null; + int textLength = BitConverter.ToInt32(data, offset); + offset += 4; + + string text = null; + if (textLength > 0) + { + if (offset + textLength > data.Length) return null; + text = Encoding.UTF8.GetString(data, offset, textLength); + offset += textLength; + } + + // 오디오 데이터 읽기 + if (offset + 4 > data.Length) return null; + int audioLength = BitConverter.ToInt32(data, offset); + offset += 4; + + byte[] audioData = null; + if (audioLength > 0) + { + if (offset + audioLength > data.Length) return null; + audioData = new byte[audioLength]; + Array.Copy(data, offset, audioData, 0, audioLength); + offset += audioLength; + } + + // 오디오 지속시간 읽기 (float) + if (offset + 4 > data.Length) return null; + float audioDuration = BitConverter.ToSingle(data, offset); + + return new IntegratedMessage + { + sessionId = sessionId, + text = text, + audioData = audioData, + audioDuration = audioDuration + }; + } + catch (Exception ex) + { + Debug.LogError($"바이너리 메시지 파싱 실패: {ex.Message}"); + return null; + } + } + + /// + /// 통합 메시지 처리 (텍스트 + 오디오) + /// + private void ProcessIntegratedMessage(IntegratedMessage integratedMessage, System.Collections.Generic.List handlers) + { + try + { + Debug.Log($"통합 메시지 수신 - 텍스트: {integratedMessage.text?.Length ?? 0}자, 오디오: {integratedMessage.audioData?.Length ?? 0}바이트"); + + // 이벤트 발생 + foreach (var handler in handlers) + { + handler.OnIntegratedMessageReceived(integratedMessage); + } + + // 텍스트가 있는 경우 텍스트 메시지로도 처리 + if (!string.IsNullOrEmpty(integratedMessage.text)) + { + var chatMessage = new ChatMessage + { + type = "chat", + sessionId = integratedMessage.sessionId, + message = integratedMessage.text, + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }; + + foreach (var handler in handlers) + { + handler.OnChatMessageReceived(chatMessage); + } + } + + // 오디오가 있는 경우 오디오 데이터로도 처리 + if (integratedMessage.audioData != null && integratedMessage.audioData.Length > 0) + { + ProcessAudioData(integratedMessage.audioData, handlers); + } + } + catch (Exception ex) + { + Debug.LogError($"통합 메시지 처리 실패: {ex.Message}"); + } + } + + /// + /// 오디오 데이터 처리 + /// + private void ProcessAudioData(byte[] audioData, System.Collections.Generic.List handlers) + { + try + { + Debug.Log($"오디오 데이터 수신: {audioData.Length} bytes"); + + // 이벤트 발생 + foreach (var handler in handlers) + { + handler.OnAudioDataReceived(audioData); + } + } + catch (Exception ex) + { + Debug.LogError($"오디오 데이터 처리 실패: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs.meta b/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs.meta new file mode 100644 index 0000000..d925f89 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 31ced92dddfa10a4a8b751118d8c9184 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs b/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs new file mode 100644 index 0000000..33c8f88 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs @@ -0,0 +1,37 @@ +using System; +using ProjectVG.Infrastructure.Network.DTOs.WebSocket; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Processors +{ + /// + /// 메시지 처리기 인터페이스 (Bridge Pattern의 추상화) + /// + public interface IMessageProcessor + { + /// + /// 메시지 타입 + /// + string MessageType { get; } + + /// + /// 문자열 메시지 처리 + /// + /// 수신된 메시지 + /// 핸들러 목록 + void ProcessMessage(string message, System.Collections.Generic.List handlers); + + /// + /// 바이너리 메시지 처리 + /// + /// 수신된 바이너리 데이터 + /// 핸들러 목록 + void ProcessBinaryMessage(byte[] data, System.Collections.Generic.List handlers); + + /// + /// 세션 ID 추출 + /// + /// 메시지 + /// 세션 ID + string ExtractSessionId(string message); + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs.meta b/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs.meta new file mode 100644 index 0000000..87f346b --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4a6d71367a30ec447bd3598805727650 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs b/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs new file mode 100644 index 0000000..21d4175 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs @@ -0,0 +1,153 @@ +using System; +using UnityEngine; +using ProjectVG.Infrastructure.Network.DTOs.WebSocket; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Processors +{ + /// + /// JSON 메시지 처리기 (Bridge Pattern의 구현체) + /// + public class JsonMessageProcessor : IMessageProcessor + { + public string MessageType => "json"; + + public void ProcessMessage(string message, System.Collections.Generic.List handlers) + { + try + { + Debug.Log($"JSON 메시지 처리: {message}"); + + // 세션 ID 메시지 특별 처리 + if (message.Contains("\"type\":\"session_id\"")) + { + ProcessSessionIdMessage(message, handlers); + return; + } + + // JSON 메시지 파싱 및 처리 + var baseMessage = ParseJsonMessage(message); + if (baseMessage != null) + { + ProcessReceivedMessage(baseMessage, handlers); + } + } + catch (Exception ex) + { + Debug.LogError($"JSON 메시지 처리 실패: {ex.Message}"); + } + } + + public void ProcessBinaryMessage(byte[] data, System.Collections.Generic.List handlers) + { + // JSON 프로세서는 바이너리 메시지를 처리하지 않음 + Debug.LogWarning("JSON 프로세서는 바이너리 메시지를 처리하지 않습니다."); + } + + public string ExtractSessionId(string message) + { + try + { + if (message.Contains("\"type\":\"session_id\"")) + { + int sessionIdStart = message.IndexOf("\"session_id\":\"") + 14; + int sessionIdEnd = message.IndexOf("\"", sessionIdStart); + if (sessionIdStart > 13 && sessionIdEnd > sessionIdStart) + { + return message.Substring(sessionIdStart, sessionIdEnd - sessionIdStart); + } + } + return null; + } + catch (Exception ex) + { + Debug.LogError($"세션 ID 추출 실패: {ex.Message}"); + return null; + } + } + + private void ProcessSessionIdMessage(string message, System.Collections.Generic.List handlers) + { + Debug.Log("세션 ID 메시지 감지됨"); + + var sessionId = ExtractSessionId(message); + if (!string.IsNullOrEmpty(sessionId)) + { + Debug.Log($"세션 ID 저장됨: {sessionId}"); + + // 핸들러들에게 세션 ID 메시지 전달 + var sessionMessage = new SessionIdMessage { session_id = sessionId }; + foreach (var handler in handlers) + { + Debug.Log($"핸들러에게 세션 ID 전달: {handler.GetType().Name}"); + handler.OnSessionIdMessageReceived(sessionMessage); + } + } + else + { + Debug.LogError("세션 ID 추출 실패 - JSON 형식 확인 필요"); + } + } + + private WebSocketMessage ParseJsonMessage(string message) + { + try + { + return JsonUtility.FromJson(message); + } + catch (Exception ex) + { + Debug.LogError($"JSON 메시지 파싱 실패: {ex.Message}"); + return null; + } + } + + private void ProcessReceivedMessage(WebSocketMessage baseMessage, System.Collections.Generic.List handlers) + { + try + { + Debug.Log($"메시지 수신: {baseMessage.type} - {baseMessage.data}"); + + // 메시지 타입에 따른 처리 + switch (baseMessage.type?.ToLower()) + { + case "session_id": + Debug.Log("세션 ID 메시지 처리 중..."); + var sessionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); + foreach (var handler in handlers) + { + handler.OnSessionIdMessageReceived(sessionMessage); + } + break; + + case "chat": + var chatMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); + foreach (var handler in handlers) + { + handler.OnChatMessageReceived(chatMessage); + } + break; + + case "system": + var systemMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); + foreach (var handler in handlers) + { + handler.OnSystemMessageReceived(systemMessage); + } + break; + + case "connection": + var connectionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); + foreach (var handler in handlers) + { + handler.OnConnectionMessageReceived(connectionMessage); + } + break; + } + } + catch (Exception ex) + { + Debug.LogError($"메시지 처리 실패: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs.meta b/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs.meta new file mode 100644 index 0000000..bb8ca30 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 937f58ba9a4ac1e42a3ebfe4496e8f52 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs b/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs new file mode 100644 index 0000000..9c7f767 --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ProjectVG.Infrastructure.Network.WebSocket.Processors +{ + /// + /// 메시지 처리기 팩토리 (Factory Pattern) + /// + public static class MessageProcessorFactory + { + private static readonly Dictionary _processors = new Dictionary(); + + static MessageProcessorFactory() + { + // 기본 프로세서 등록 + RegisterProcessor(new JsonMessageProcessor()); + RegisterProcessor(new BinaryMessageProcessor()); + } + + /// + /// 프로세서 등록 + /// + public static void RegisterProcessor(IMessageProcessor processor) + { + if (processor != null && !string.IsNullOrEmpty(processor.MessageType)) + { + _processors[processor.MessageType.ToLower()] = processor; + Debug.Log($"메시지 프로세서 등록: {processor.MessageType}"); + } + } + + /// + /// 메시지 타입에 따른 프로세서 생성 + /// + public static IMessageProcessor CreateProcessor(string messageType) + { + if (string.IsNullOrEmpty(messageType)) + { + Debug.LogWarning("메시지 타입이 null입니다. JSON 프로세서를 사용합니다."); + return GetDefaultProcessor(); + } + + var key = messageType.ToLower(); + if (_processors.TryGetValue(key, out var processor)) + { + Debug.Log($"메시지 프로세서 생성: {messageType}"); + return processor; + } + + Debug.LogWarning($"지원하지 않는 메시지 타입: {messageType}. JSON 프로세서를 사용합니다."); + return GetDefaultProcessor(); + } + + /// + /// 기본 프로세서 (JSON) 반환 + /// + public static IMessageProcessor GetDefaultProcessor() + { + return _processors["json"]; + } + + /// + /// 등록된 모든 프로세서 타입 반환 + /// + public static IEnumerable GetAvailableProcessors() + { + return _processors.Keys; + } + + /// + /// 프로세서 존재 여부 확인 + /// + public static bool HasProcessor(string messageType) + { + if (string.IsNullOrEmpty(messageType)) + return false; + + return _processors.ContainsKey(messageType.ToLower()); + } + } +} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs.meta b/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs.meta new file mode 100644 index 0000000..2879dbe --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 050f5d1205efd4f49b766a692fbd5cef \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs index 1f8f215..2356a85 100644 --- a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs @@ -7,13 +7,13 @@ using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.Configs; using ProjectVG.Infrastructure.Network.DTOs.WebSocket; -using ProjectVG.Infrastructure.Network.WebSocket.Platforms; +using ProjectVG.Infrastructure.Network.WebSocket.Processors; namespace ProjectVG.Infrastructure.Network.WebSocket { /// - /// WebSocket 연결 및 메시지 관리자 - /// UnityWebRequest를 사용하여 WebSocket 연결을 관리하고, 비동기 결과를 Handler로 전달합니다. + /// WebSocket 연결 및 메시지 관리자 (Bridge Pattern 적용) + /// 메시지 처리기를 통해 JSON과 바이너리 메시지를 분리하여 처리합니다. /// public class WebSocketManager : MonoBehaviour { @@ -24,11 +24,21 @@ public class WebSocketManager : MonoBehaviour private CancellationTokenSource _cancellationTokenSource; private List _handlers = new List(); + // Bridge Pattern: 메시지 처리기 + private IMessageProcessor _messageProcessor; + + // 메시지 버퍼링을 위한 필드 추가 + private readonly StringBuilder _messageBuffer = new StringBuilder(); + private readonly object _bufferLock = new object(); + private bool _isProcessingMessage = false; + private bool _isConnected = false; private bool _isConnecting = false; private int _reconnectAttempts = 0; private string _sessionId; + + public static WebSocketManager Instance { get; private set; } // 이벤트 @@ -36,12 +46,15 @@ public class WebSocketManager : MonoBehaviour public event Action OnDisconnected; public event Action OnError; public event Action OnMessageReceived; + public event Action OnAudioDataReceived; + public event Action OnIntegratedMessageReceived; + - // 프로퍼티 public bool IsConnected => _isConnected; public bool IsConnecting => _isConnecting; public string SessionId => _sessionId; + private void Awake() { if (Instance == null) @@ -67,9 +80,29 @@ private void InitializeManager() { _cancellationTokenSource = new CancellationTokenSource(); + // NetworkConfig 기반으로 메시지 처리기 설정 + InitializeMessageProcessor(); + // Native WebSocket 초기화 InitializeNativeWebSocket(); } + + /// + /// NetworkConfig 기반으로 메시지 처리기 초기화 + /// + private void InitializeMessageProcessor() + { + var messageType = NetworkConfig.WebSocketMessageType; + _messageProcessor = MessageProcessorFactory.CreateProcessor(messageType); + Debug.Log($"NetworkConfig 기반 메시지 처리기 초기화: {messageType}"); + + // 현재 설정 로그 출력 + Debug.Log($"=== WebSocket 메시지 처리 설정 ==="); + Debug.Log($"NetworkConfig 메시지 타입: {messageType}"); + Debug.Log($"JSON 형식 지원: {NetworkConfig.IsJsonMessageType}"); + Debug.Log($"바이너리 형식 지원: {NetworkConfig.IsBinaryMessageType}"); + Debug.Log($"====================================="); + } private void InitializeNativeWebSocket() { @@ -142,6 +175,10 @@ public async UniTask ConnectAsync(string sessionId = null, CancellationTok _reconnectAttempts = 0; Debug.Log("WebSocket 연결 성공"); + + // 연결 후 서버 설정 로드 시도 (선택적) + // LoadServerConfigAsync().Forget(); + return true; } else @@ -246,107 +283,33 @@ public async UniTask SendChatMessageAsync(string message, string character return await SendMessageAsync(chatMessage, cancellationToken); } - - /// - /// 수신된 메시지 처리 (더미 클라이언트와 동일한 방식) - /// - private void ProcessReceivedMessage(WebSocketMessage baseMessage) - { - try - { - Debug.Log($"메시지 수신: {baseMessage.type} - {baseMessage.data}"); - - // 이벤트 발생 - OnMessageReceived?.Invoke(baseMessage); - foreach (var handler in _handlers) - { - handler.OnMessageReceived(baseMessage); - } - - // 메시지 타입에 따른 처리 - switch (baseMessage.type?.ToLower()) - { - case "session_id": - Debug.Log("세션 ID 메시지 처리 중..."); - var sessionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); - _sessionId = sessionMessage.session_id; // 세션 ID 저장 - Debug.Log($"세션 ID 저장됨: {_sessionId}"); - foreach (var handler in _handlers) - { - handler.OnSessionIdMessageReceived(sessionMessage); - } - break; - - case "chat": - var chatMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); - foreach (var handler in _handlers) - { - handler.OnChatMessageReceived(chatMessage); - } - break; - - case "system": - var systemMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); - foreach (var handler in _handlers) - { - handler.OnSystemMessageReceived(systemMessage); - } - break; - - case "connection": - var connectionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); - foreach (var handler in _handlers) - { - handler.OnConnectionMessageReceived(connectionMessage); - } - break; - } - } - catch (Exception ex) - { - Debug.LogError($"메시지 처리 실패: {ex.Message}"); - } - } - - /// - /// 오디오 데이터 처리 (더미 클라이언트와 동일한 방식) - /// - private void ProcessAudioData(byte[] audioData) - { - try - { - Debug.Log($"오디오 데이터 수신: {audioData.Length} bytes"); - - // 이벤트 발생 - foreach (var handler in _handlers) - { - handler.OnAudioDataReceived(audioData); - } - } - catch (Exception ex) - { - Debug.LogError($"오디오 데이터 처리 실패: {ex.Message}"); - } - } - - /// - /// WebSocket URL 생성 (더미 클라이언트와 동일한 방식) + /// WebSocket URL 생성 /// private string GetWebSocketUrl(string sessionId = null) { string baseUrl = NetworkConfig.GetWebSocketUrl(); + Debug.Log($"=== WebSocket URL 생성 ==="); + Debug.Log($"기본 URL: {baseUrl}"); + Debug.Log($"세션 ID: {sessionId}"); + + string finalUrl; if (!string.IsNullOrEmpty(sessionId)) { - return $"{baseUrl}?sessionId={sessionId}"; + finalUrl = $"{baseUrl}?sessionId={sessionId}"; + } + else + { + finalUrl = baseUrl; } - return baseUrl; + Debug.Log($"최종 URL: {finalUrl}"); + Debug.Log($"================================"); + + return finalUrl; } - - /// /// 자동 재연결 시도 /// @@ -372,8 +335,6 @@ private async UniTaskVoid TryReconnectAsync() } } - - #region Native WebSocket Event Handlers private void OnNativeConnected() @@ -426,57 +387,148 @@ private void OnNativeMessageReceived(string message) { try { - Debug.Log($"원시 메시지 수신: {message}"); + Debug.Log($"=== 메시지 수신 디버깅 ==="); + Debug.Log($"원시 메시지 길이: {message?.Length ?? 0}"); + Debug.Log($"원시 메시지 (처음 100자): {message?.Substring(0, Math.Min(100, message?.Length ?? 0))}"); Debug.Log($"핸들러 수: {_handlers.Count}"); + Debug.Log($"현재 메시지 처리기: {_messageProcessor.MessageType}"); + Debug.Log($"================================"); + + // 메시지 버퍼링 처리 + ProcessBufferedMessage(message); + } + catch (Exception ex) + { + Debug.LogError($"메시지 파싱 실패: {ex.Message}"); + Debug.LogError($"원시 메시지: {message}"); + } + } + + /// + /// 메시지 버퍼링 및 완전한 JSON 메시지 처리 + /// + private void ProcessBufferedMessage(string message) + { + lock (_bufferLock) + { + // 메시지를 버퍼에 추가 + _messageBuffer.Append(message); + string bufferedMessage = _messageBuffer.ToString(); + + Debug.Log($"버퍼링된 메시지 길이: {bufferedMessage.Length}"); + Debug.Log($"버퍼링된 메시지 (처음 100자): {bufferedMessage.Substring(0, Math.Min(100, bufferedMessage.Length))}"); - // 클라이언트와 동일한 방식으로 세션 ID 메시지 처리 - if (message.Contains("\"type\":\"session_id\"")) + // 완전한 JSON 메시지인지 확인 + if (IsCompleteJsonMessage(bufferedMessage)) { - Debug.Log("세션 ID 메시지 감지됨"); + Debug.Log("완전한 JSON 메시지 감지됨. 처리 시작."); - // JSON에서 session_id 추출 - int sessionIdStart = message.IndexOf("\"session_id\":\"") + 14; - int sessionIdEnd = message.IndexOf("\"", sessionIdStart); - if (sessionIdStart > 13 && sessionIdEnd > sessionIdStart) + // JSON 형식인지 확인 + if (IsValidJsonMessage(bufferedMessage)) { - _sessionId = message.Substring(sessionIdStart, sessionIdEnd - sessionIdStart); - Debug.Log($"세션 ID 저장됨: {_sessionId}"); - - // 핸들러들에게 세션 ID 메시지 전달 - var sessionMessage = new SessionIdMessage { session_id = _sessionId }; - foreach (var handler in _handlers) - { - Debug.Log($"핸들러에게 세션 ID 전달: {handler.GetType().Name}"); - handler.OnSessionIdMessageReceived(sessionMessage); - } - return; + Debug.Log("JSON 형식으로 처리합니다."); + // Bridge Pattern: 메시지 처리기에 위임 + _messageProcessor.ProcessMessage(bufferedMessage, _handlers); } else { - Debug.LogError("세션 ID 추출 실패 - JSON 형식 확인 필요"); + Debug.LogWarning("JSON 형식이 아닌 메시지가 문자열로 수신됨. 바이너리 처리기로 전달합니다."); + // 바이너리 처리기로 전달 + var binaryProcessor = MessageProcessorFactory.CreateProcessor("binary"); + binaryProcessor.ProcessMessage(bufferedMessage, _handlers); } + + // 버퍼 초기화 + _messageBuffer.Clear(); + Debug.Log("메시지 처리 완료. 버퍼 초기화됨."); } else { - Debug.Log("세션 ID 메시지가 아님 - 다른 메시지 타입"); + Debug.Log($"불완전한 메시지. 버퍼에 누적 중... (현재 길이: {bufferedMessage.Length})"); } - - // 기존 방식으로 처리 - var baseMessage = JsonUtility.FromJson(message); - ProcessReceivedMessage(baseMessage); } - catch (Exception ex) + } + + /// + /// 완전한 JSON 메시지인지 확인 + /// + private bool IsCompleteJsonMessage(string message) + { + if (string.IsNullOrEmpty(message)) + return false; + + // 중괄호 개수로 완전한 JSON인지 확인 + int openBraces = 0; + int closeBraces = 0; + bool inString = false; + char escapeChar = '\\'; + + for (int i = 0; i < message.Length; i++) { - Debug.LogError($"메시지 파싱 실패: {ex.Message}"); - Debug.LogError($"원시 메시지: {message}"); + char c = message[i]; + + if (c == '"' && (i == 0 || message[i - 1] != escapeChar)) + { + inString = !inString; + } + else if (!inString) + { + if (c == '{') + openBraces++; + else if (c == '}') + closeBraces++; + } } + + bool isComplete = openBraces > 0 && openBraces == closeBraces; + Debug.Log($"JSON 완성도 체크: 열린괄호={openBraces}, 닫힌괄호={closeBraces}, 완성={isComplete}"); + + return isComplete; + } + + /// + /// JSON 형식인지 확인 + /// + private bool IsValidJsonMessage(string message) + { + if (string.IsNullOrEmpty(message)) + return false; + + message = message.Trim(); + + // JSON 객체 시작/끝 확인 + if (message.StartsWith("{") && message.EndsWith("}")) + return true; + + // JSON 배열 시작/끝 확인 + if (message.StartsWith("[") && message.EndsWith("]")) + return true; + + // 세션 ID 메시지 특별 처리 + if (message.Contains("\"type\":\"session_id\"")) + return true; + + return false; } private void OnNativeBinaryDataReceived(byte[] data) { - ProcessAudioData(data); + try + { + Debug.Log($"바이너리 데이터 수신: {data.Length} bytes"); + Debug.Log($"현재 메시지 처리기: {_messageProcessor.MessageType}"); + + // Bridge Pattern: 메시지 처리기에 위임 + _messageProcessor.ProcessBinaryMessage(data, _handlers); + } + catch (Exception ex) + { + Debug.LogError($"바이너리 데이터 처리 실패: {ex.Message}"); + } } #endregion } + + } \ No newline at end of file diff --git a/Assets/Tests/Runtime/NetworkTestManager.cs b/Assets/Tests/Runtime/NetworkTestManager.cs index 9ad7ea7..131d73f 100644 --- a/Assets/Tests/Runtime/NetworkTestManager.cs +++ b/Assets/Tests/Runtime/NetworkTestManager.cs @@ -4,14 +4,16 @@ using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.WebSocket; using ProjectVG.Infrastructure.Network.Services; -using ProjectVG.Infrastructure.Network.Http; using ProjectVG.Infrastructure.Network.Configs; +using ProjectVG.Infrastructure.Network.Http; +using ProjectVG.Infrastructure.Network.DTOs.WebSocket; namespace ProjectVG.Tests.Runtime { /// /// WebSocket + HTTP 통합 테스트 매니저 - /// 더미 클라이언트와 동일한 동작: WebSocket 연결 → 세션 ID 수신 → HTTP 요청 → WebSocket으로 결과 수신 + /// WebSocket 연결 → 세션 ID 수신 → HTTP 요청 → WebSocket으로 결과 수신 + /// JSON과 바이너리 메시지를 모두 처리합니다. /// public class NetworkTestManager : MonoBehaviour { @@ -45,6 +47,9 @@ public float TestInterval private string _receivedSessionId = null; // WebSocket에서 받은 세션 ID private bool _chatResponseReceived = false; // 채팅 응답 수신 여부 private string _lastChatResponse = null; // 마지막 채팅 응답 + private bool _integratedMessageReceived = false; // 통합 메시지 수신 여부 + private bool _audioDataReceived = false; // 오디오 데이터 수신 여부 + private void Awake() { @@ -168,6 +173,10 @@ private void InitializeManagers() _webSocketHandler.OnChatMessageReceivedEvent += OnChatMessageReceived; _webSocketHandler.OnSystemMessageReceivedEvent += OnSystemMessageReceived; _webSocketHandler.OnSessionIdMessageReceivedEvent += OnSessionIdMessageReceived; + _webSocketHandler.OnAudioDataReceivedEvent += OnAudioDataReceived; + _webSocketHandler.OnIntegratedMessageReceivedEvent += OnIntegratedMessageReceived; + + Debug.Log("NetworkTestManager 초기화 완료"); NetworkConfig.LogCurrentSettings(); @@ -180,6 +189,8 @@ private void InitializeManagers() #region 수동 테스트 메서드들 + + [ContextMenu("1. WebSocket 연결 (더미 클라이언트 방식)")] public async void ConnectWebSocket() { @@ -200,8 +211,8 @@ public async void ConnectWebSocket() Debug.Log("=== WebSocket 연결 시작 (더미 클라이언트 방식) ==="); // 현재 설정 정보 출력 - Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); - Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); + Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); _receivedSessionId = null; // 세션 ID 초기화 @@ -211,6 +222,7 @@ public async void ConnectWebSocket() if (connected) { Debug.Log("✅ WebSocket 연결 성공! 세션 ID 대기 중..."); + Debug.Log($"현재 메시지 처리기: {NetworkConfig.WebSocketMessageType}"); } else { @@ -250,10 +262,12 @@ public async void SendChatRequest() Debug.Log("=== HTTP 채팅 요청 시작 (더미 클라이언트 방식) ==="); // 현재 설정 정보 출력 - Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); - Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("chat")}"); + Debug.Log($"현재 환경: {NetworkConfig.CurrentEnvironment}"); + Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("chat")}"); Debug.Log($"세션 ID: {_receivedSessionId}"); + + var chatRequest = new ProjectVG.Infrastructure.Network.DTOs.Chat.ChatRequest { message = "안녕하세요! 테스트 메시지입니다.", @@ -364,6 +378,45 @@ public async void DisconnectWebSocket() } } + [ContextMenu("6. 통합 메시지 처리 테스트")] + public async void TestIntegratedMessageHandling() + { + if (!_webSocketManager.IsConnected) + { + Debug.LogWarning("WebSocket이 연결되지 않았습니다. 먼저 연결해주세요."); + return; + } + + try + { + Debug.Log("=== 통합 메시지 처리 테스트 시작 ==="); + + // 응답 상태 초기화 + _chatResponseReceived = false; + _integratedMessageReceived = false; + _audioDataReceived = false; + _lastChatResponse = null; + + // HTTP 채팅 요청으로 통합 메시지 유도 + if (!string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.Log("HTTP 채팅 요청으로 통합 메시지 유도 중..."); + await SendChatRequestInternal(); + + // 통합 메시지 수신 대기 + await WaitForIntegratedMessage(15); + } + else + { + Debug.LogWarning("세션 ID가 없습니다. 먼저 WebSocket 연결을 통해 세션 ID를 받아주세요."); + } + } + catch (Exception ex) + { + Debug.LogError($"통합 메시지 처리 테스트 중 오류: {ex.Message}"); + } + } + [ContextMenu("더미 클라이언트 방식 전체 테스트")] public async void RunDummyClientTest() { @@ -744,6 +797,36 @@ private async UniTask WaitForChatResponse(int timeoutSeconds = 15) } } + /// + /// 통합 메시지를 기다리는 메서드 + /// + private async UniTask WaitForIntegratedMessage(int timeoutSeconds = 15) + { + Debug.Log($"통합 메시지 대기 시작 (타임아웃: {timeoutSeconds}초)"); + + var startTime = DateTime.Now; + while (!_integratedMessageReceived && (DateTime.Now - startTime).TotalSeconds < timeoutSeconds) + { + await UniTask.Delay(100); + + // WebSocket 연결 상태 확인 + if (_webSocketManager != null && !_webSocketManager.IsConnected) + { + Debug.LogWarning("WebSocket 연결이 끊어졌습니다. 응답 대기 중단"); + break; + } + } + + if (_integratedMessageReceived) + { + Debug.Log($"✅ 통합 메시지 수신 완료"); + } + else + { + Debug.LogWarning($"⚠️ 통합 메시지 타임아웃 ({timeoutSeconds}초)"); + } + } + private async UniTask GetCharacterInfoInternal() { if (_apiServiceManager?.Character == null) @@ -812,6 +895,38 @@ private void OnSessionIdMessageReceived(ProjectVG.Infrastructure.Network.DTOs.We Debug.Log($"✅ 세션 ID가 성공적으로 저장되었습니다!"); } + private void OnAudioDataReceived(byte[] audioData) + { + Debug.Log($"🎵 WebSocket 오디오 데이터 수신: {audioData.Length} bytes"); + _audioDataReceived = true; + } + + private void OnIntegratedMessageReceived(IntegratedMessage message) + { + Debug.Log($"🔄 WebSocket 통합 메시지 수신:"); + Debug.Log($" - 세션 ID: {message.sessionId}"); + Debug.Log($" - 텍스트: {message.text?.Length ?? 0}자"); + Debug.Log($" - 오디오: {message.audioData?.Length ?? 0}바이트"); + Debug.Log($" - 지속시간: {message.audioDuration:F2}초"); + + _integratedMessageReceived = true; + + // 텍스트가 있으면 채팅 메시지로도 처리 + if (!string.IsNullOrEmpty(message.text)) + { + _chatResponseReceived = true; + _lastChatResponse = message.text; + } + + // 오디오가 있으면 오디오 데이터로도 처리 + if (message.audioData != null && message.audioData.Length > 0) + { + _audioDataReceived = true; + } + } + + + #endregion } } \ No newline at end of file From a0819c972761ec9ffbe3be1598bb96b33fad44b6 Mon Sep 17 00:00:00 2001 From: WooSH Date: Tue, 29 Jul 2025 18:07:08 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=ED=8C=8C=EC=8B=B1=20=EB=B0=A9=EB=B2=95=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 웹 소캣 데이터 전송 및 수신 방법을 type : data 방식으로 변경 --- .../Network/Configs/ServerConfigLoader.cs | 1 - .../DTOs/WebSocket/IntegratedMessage.cs | 50 --- .../DTOs/WebSocket/IntegratedMessage.cs.meta | 2 - .../DTOs/WebSocket/WebSocketMessage.cs | 57 ---- .../DTOs/WebSocket/WebSocketMessage.cs.meta | 2 - Assets/Infrastructure/Network/README.md | 109 +++++-- .../WebSocket/DefaultWebSocketHandler.cs | 175 ----------- .../WebSocket/DefaultWebSocketHandler.cs.meta | 2 - .../Network/WebSocket/Handlers.meta | 8 + .../Network/WebSocket/INativeWebSocket.cs | 2 - .../Network/WebSocket/IWebSocketHandler.cs | 68 ----- .../WebSocket/IWebSocketHandler.cs.meta | 2 - .../WebSocket/Platforms/DesktopWebSocket.cs | 27 +- .../WebSocket/Platforms/MobileWebSocket.cs | 27 +- .../WebSocket/Platforms/WebGLWebSocket.cs | 21 -- .../Processors/BinaryMessageProcessor.cs | 201 ------------ .../Processors/BinaryMessageProcessor.cs.meta | 2 - .../WebSocket/Processors/IMessageProcessor.cs | 37 --- .../Processors/IMessageProcessor.cs.meta | 2 - .../Processors/JsonMessageProcessor.cs | 153 ---------- .../Processors/JsonMessageProcessor.cs.meta | 2 - .../Processors/MessageProcessorFactory.cs | 81 ----- .../MessageProcessorFactory.cs.meta | 2 - .../Network/WebSocket/WebSocketManager.cs | 288 +++++++----------- Assets/Tests/Runtime/NetworkTestManager.cs | 261 ++++++---------- Assets/Tests/Runtime/NetworkTestUI.cs | 97 +++++- 26 files changed, 378 insertions(+), 1301 deletions(-) delete mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs delete mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs.meta delete mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs delete mode 100644 Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs delete mode 100644 Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs.meta create mode 100644 Assets/Infrastructure/Network/WebSocket/Handlers.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs delete mode 100644 Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs delete mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs delete mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs delete mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs.meta delete mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs delete mode 100644 Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs.meta diff --git a/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs index 5685315..d5a07db 100644 --- a/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs +++ b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs @@ -3,7 +3,6 @@ using UnityEngine.Networking; using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.Configs; -using ProjectVG.Infrastructure.Network.DTOs.WebSocket; namespace ProjectVG.Infrastructure.Network.Configs { diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs b/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs deleted file mode 100644 index f9cf0eb..0000000 --- a/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; - -namespace ProjectVG.Infrastructure.Network.DTOs.WebSocket -{ - /// - /// 통합 메시지 (텍스트 + 오디오) - /// 바이너리 형식으로 전송되는 통합 메시지를 위한 DTO - /// - [System.Serializable] - public class IntegratedMessage - { - /// - /// 세션 ID - /// - public string sessionId; - - /// - /// 텍스트 메시지 - /// - public string text; - - /// - /// 오디오 바이너리 데이터 - /// - public byte[] audioData; - - /// - /// 오디오 지속시간 (초) - /// - public float audioDuration; - - /// - /// 텍스트가 있는지 확인 - /// - public bool HasText => !string.IsNullOrEmpty(text); - - /// - /// 오디오가 있는지 확인 - /// - public bool HasAudio => audioData != null && audioData.Length > 0; - - /// - /// 메시지 정보를 문자열로 반환 - /// - public override string ToString() - { - return $"IntegratedMessage[SessionId: {sessionId}, Text: {text?.Length ?? 0} chars, Audio: {audioData?.Length ?? 0} bytes, Duration: {audioDuration:F2}s]"; - } - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs.meta b/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs.meta deleted file mode 100644 index 385c3fb..0000000 --- a/Assets/Infrastructure/Network/DTOs/WebSocket/IntegratedMessage.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 0b929e806824f0b4fb2f4ac9a21bef23 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs b/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs deleted file mode 100644 index 15e1be6..0000000 --- a/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; - -namespace ProjectVG.Infrastructure.Network.DTOs.WebSocket -{ - /// - /// WebSocket 메시지 기본 구조 - /// - [Serializable] - public class WebSocketMessage - { - public string type; - public string sessionId; - public long timestamp; - public string data; - } - - /// - /// 세션 ID 메시지 (더미 클라이언트와 동일) - /// - [Serializable] - public class SessionIdMessage : WebSocketMessage - { - public string session_id; - } - - /// - /// 채팅 메시지 타입 - /// - [Serializable] - public class ChatMessage : WebSocketMessage - { - public string characterId; - public string userId; - public string message; - public string actor; - } - - /// - /// 시스템 메시지 타입 - /// - [Serializable] - public class SystemMessage : WebSocketMessage - { - public string status; - public string description; - } - - /// - /// 연결 상태 메시지 타입 - /// - [Serializable] - public class ConnectionMessage : WebSocketMessage - { - public string status; // "connected", "disconnected", "error" - public string reason; - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs.meta b/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs.meta deleted file mode 100644 index 910b644..0000000 --- a/Assets/Infrastructure/Network/DTOs/WebSocket/WebSocketMessage.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3982d097d6fd94242b7827400f78306d \ No newline at end of file diff --git a/Assets/Infrastructure/Network/README.md b/Assets/Infrastructure/Network/README.md index 9789ff1..b546cf2 100644 --- a/Assets/Infrastructure/Network/README.md +++ b/Assets/Infrastructure/Network/README.md @@ -1,6 +1,7 @@ # ProjectVG Network Module Unity 클라이언트와 서버 간의 통신을 위한 네트워크 모듈입니다. +강제된 JSON 형식 `{type: "xxx", data: {...}}`을 사용합니다. ## 📦 설치 @@ -27,8 +28,7 @@ Assets/Infrastructure/Network/ ├── DTOs/ # 데이터 전송 객체들 │ ├── BaseApiResponse.cs # 기본 API 응답 │ ├── Chat/ # 채팅 관련 DTO -│ ├── Character/ # 캐릭터 관련 DTO -│ └── WebSocket/ # WebSocket 메시지 DTO +│ └── Character/ # 캐릭터 관련 DTO ├── Http/ # HTTP 클라이언트 │ └── HttpApiClient.cs # HTTP API 클라이언트 ├── Services/ # API 서비스들 @@ -36,10 +36,9 @@ Assets/Infrastructure/Network/ │ ├── ChatApiService.cs # 채팅 API 서비스 │ └── CharacterApiService.cs # 캐릭터 API 서비스 └── WebSocket/ # WebSocket 관련 - ├── WebSocketManager.cs # WebSocket 매니저 + ├── WebSocketManager.cs # WebSocket 매니저 (단순화됨) ├── WebSocketFactory.cs # 플랫폼별 WebSocket 팩토리 - ├── IWebSocketHandler.cs # WebSocket 핸들러 인터페이스 - ├── DefaultWebSocketHandler.cs # 기본 핸들러 + ├── INativeWebSocket.cs # 플랫폼별 WebSocket 인터페이스 └── Platforms/ # 플랫폼별 WebSocket 구현 ├── DesktopWebSocket.cs # 데스크톱용 (.NET ClientWebSocket) ├── WebGLWebSocket.cs # WebGL용 (UnityWebRequest) @@ -85,30 +84,67 @@ var wsUrlWithVersion = NetworkConfig.GetWebSocketUrlWithVersion(); var wsUrlWithSession = NetworkConfig.GetWebSocketUrlWithSession("session-123"); ``` -### 2. 전체 흐름 테스트 (권장) +### 2. WebSocket 사용 (단순화됨) + +#### 기본 사용법 ```csharp -// NetworkTestManager 사용 -var testManager = FindObjectOfType(); +// WebSocket 매니저 사용 +var wsManager = WebSocketManager.Instance; -// 1. WebSocket 연결 -await testManager.ConnectWebSocket(); +// 이벤트 구독 +wsManager.OnConnected += () => Debug.Log("연결됨"); +wsManager.OnDisconnected += () => Debug.Log("연결 해제됨"); +wsManager.OnError += (error) => Debug.LogError($"오류: {error}"); +wsManager.OnSessionIdReceived += (sessionId) => Debug.Log($"세션 ID: {sessionId}"); +wsManager.OnChatMessageReceived += (message) => Debug.Log($"채팅: {message}"); -// 2. HTTP 요청 전송 -await testManager.SendChatRequest(); +// 연결 +await wsManager.ConnectAsync(); -// 3. WebSocket으로 결과 수신 (자동) -// 서버가 비동기 작업 완료 후 WebSocket으로 결과 전송 +// 메시지 전송 +await wsManager.SendChatMessageAsync("안녕하세요!"); + +// 연결 해제 +await wsManager.DisconnectAsync(); +``` + +#### 강제된 JSON 형식 사용 +```csharp +// 메시지 전송 (강제된 형식) +await wsManager.SendMessageAsync("chat", new ChatData +{ + message = "안녕하세요!", + sessionId = "session-123", + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() +}); + +// 서버에서 받는 메시지 형식 +// { +// "type": "session_id", +// "data": { +// "session_id": "session_123456789" +// } +// } +// +// { +// "type": "chat", +// "data": { +// "message": "안녕하세요!", +// "sessionId": "session-123", +// "timestamp": 1703123456789 +// } +// } ``` -### 3. 개별 모듈 사용 +### 3. HTTP API 사용 -#### HTTP API 사용 +#### API 서비스 매니저 사용 ```csharp // API 서비스 매니저 사용 var apiManager = ApiServiceManager.Instance; // 채팅 API 사용 -var chatResponse = await apiManager.ChatApiService.SendChatAsync( +var chatResponse = await apiManager.Chat.SendChatAsync( new ChatRequest { message = "안녕하세요!", @@ -119,27 +155,22 @@ var chatResponse = await apiManager.ChatApiService.SendChatAsync( ); // 캐릭터 API 사용 -var character = await apiManager.CharacterApiService.GetCharacterAsync("char-456"); +var character = await apiManager.Character.GetCharacterAsync("char-456"); ``` -#### WebSocket 사용 +### 4. 전체 흐름 테스트 (권장) ```csharp -// WebSocket 매니저 사용 -var wsManager = WebSocketManager.Instance; +// NetworkTestManager 사용 +var testManager = FindObjectOfType(); -// 핸들러 등록 -var handler = gameObject.AddComponent(); -wsManager.RegisterHandler(handler); +// 1. WebSocket 연결 +await testManager.ConnectWebSocket(); -// 연결 -await wsManager.ConnectAsync("session-123"); +// 2. HTTP 요청 전송 +await testManager.SendChatRequest(); -// 메시지 전송 -await wsManager.SendChatMessageAsync( - message: "안녕하세요!", - characterId: "char-456", - userId: "user-789" -); +// 3. WebSocket으로 결과 수신 (자동) +// 서버가 비동기 작업 완료 후 WebSocket으로 결과 전송 ``` ## ⚙️ 설정 @@ -178,16 +209,19 @@ await wsManager.SendChatMessageAsync( - System.Net.WebSockets.ClientWebSocket 사용 - Windows/Mac/Linux 지원 - 최고 성능 +- JSON 메시지만 처리 ### 2. WebGLWebSocket (브라우저) - UnityWebRequest.WebSocket 사용 - WebGL 플랫폼 지원 - 브라우저 제약사항 대응 +- JSON 메시지만 처리 ### 3. MobileWebSocket (모바일) - 네이티브 WebSocket 라이브러리 사용 - iOS/Android 지원 - 네이티브 성능 +- JSON 메시지만 처리 ### 4. WebSocketFactory - 플랫폼별 WebSocket 구현 생성 @@ -223,4 +257,13 @@ WebSocket 연결 성공 모든 로그는 한국어로 출력됩니다: - `Debug.Log("WebSocket 연결 성공")` - `Debug.LogError("연결 실패")` -- `Debug.LogWarning("재연결 시도")` \ No newline at end of file +- `Debug.LogWarning("재연결 시도")` + +## 🔄 변경 사항 + +### 주요 변경사항 (v2.0) +1. **바이너리 방식 완전 제거**: JSON 메시지만 처리 +2. **강제된 JSON 형식**: `{type: "xxx", data: {...}}` 형식 사용 +3. **MessageRouter 제거**: WebSocketManager에서 직접 처리 +4. **단순화된 구조**: 불필요한 복잡성 제거 +5. **확장 가능한 설계**: 추후 기능 추가 용이 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs b/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs deleted file mode 100644 index db5c125..0000000 --- a/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs +++ /dev/null @@ -1,175 +0,0 @@ -using UnityEngine; -using ProjectVG.Infrastructure.Network.DTOs.WebSocket; - -namespace ProjectVG.Infrastructure.Network.WebSocket -{ - /// - /// 기본 WebSocket 핸들러 구현체 - /// 기본적인 로깅과 이벤트 처리를 제공합니다. - /// - public class DefaultWebSocketHandler : MonoBehaviour, IWebSocketHandler - { - [Header("Handler Configuration")] - [SerializeField] private bool enableLogging = true; - [SerializeField] private bool autoRegister = true; - - // 이벤트 - public System.Action OnConnectedEvent; - public System.Action OnDisconnectedEvent; - public System.Action OnErrorEvent; - public System.Action OnMessageReceivedEvent; - public System.Action OnChatMessageReceivedEvent; - public System.Action OnSystemMessageReceivedEvent; - public System.Action OnConnectionMessageReceivedEvent; - public System.Action OnSessionIdMessageReceivedEvent; - public System.Action OnAudioDataReceivedEvent; - public System.Action OnIntegratedMessageReceivedEvent; - - private void Start() - { - if (autoRegister) - { - RegisterToManager(); - } - } - - private void OnDestroy() - { - UnregisterFromManager(); - } - - /// - /// WebSocket 매니저에 등록 - /// - public void RegisterToManager() - { - if (WebSocketManager.Instance != null) - { - WebSocketManager.Instance.RegisterHandler(this); - if (enableLogging) - { - Debug.Log("DefaultWebSocketHandler가 WebSocketManager에 등록되었습니다."); - } - } - } - - /// - /// WebSocket 매니저에서 해제 - /// - public void UnregisterFromManager() - { - if (WebSocketManager.Instance != null) - { - WebSocketManager.Instance.UnregisterHandler(this); - if (enableLogging) - { - Debug.Log("DefaultWebSocketHandler가 WebSocketManager에서 해제되었습니다."); - } - } - } - - #region IWebSocketHandler 구현 - - public void OnConnected() - { - if (enableLogging) - { - Debug.Log("WebSocket 연결됨"); - } - - OnConnectedEvent?.Invoke(); - } - - public void OnDisconnected() - { - if (enableLogging) - { - Debug.Log("WebSocket 연결 해제됨"); - } - - OnDisconnectedEvent?.Invoke(); - } - - public void OnError(string error) - { - if (enableLogging) - { - Debug.LogError($"WebSocket 오류: {error}"); - } - - OnErrorEvent?.Invoke(error); - } - - public void OnMessageReceived(WebSocketMessage message) - { - if (enableLogging) - { - Debug.Log($"WebSocket 메시지 수신: {message.type} - {message.data}"); - } - - OnMessageReceivedEvent?.Invoke(message); - } - - public void OnChatMessageReceived(ChatMessage message) - { - if (enableLogging) - { - Debug.Log($"채팅 메시지 수신: {message.characterId} - {message.message}"); - } - - OnChatMessageReceivedEvent?.Invoke(message); - } - - public void OnSystemMessageReceived(SystemMessage message) - { - if (enableLogging) - { - Debug.Log($"시스템 메시지 수신: {message.status} - {message.description}"); - } - - OnSystemMessageReceivedEvent?.Invoke(message); - } - - public void OnConnectionMessageReceived(ConnectionMessage message) - { - if (enableLogging) - { - Debug.Log($"연결 상태 메시지 수신: {message.status} - {message.reason}"); - } - - OnConnectionMessageReceivedEvent?.Invoke(message); - } - - public void OnSessionIdMessageReceived(SessionIdMessage message) - { - if (enableLogging) - { - Debug.Log($"세션 ID 메시지 수신: {message.session_id}"); - } - - OnSessionIdMessageReceivedEvent?.Invoke(message); - } - - public void OnAudioDataReceived(byte[] audioData) - { - if (enableLogging) - { - Debug.Log($"오디오 데이터 수신: {audioData.Length} bytes"); - } - - OnAudioDataReceivedEvent?.Invoke(audioData); - } - - public void OnIntegratedMessageReceived(IntegratedMessage message) - { - if (enableLogging) - { - Debug.Log($"통합 메시지 수신 - 텍스트: {message.text?.Length ?? 0}자, 오디오: {message.audioData?.Length ?? 0}바이트, 지속시간: {message.audioDuration:F2}초"); - } - - OnIntegratedMessageReceivedEvent?.Invoke(message); - } - - #endregion - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs.meta b/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs.meta deleted file mode 100644 index c54a2f5..0000000 --- a/Assets/Infrastructure/Network/WebSocket/DefaultWebSocketHandler.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 1209f48068ad90a4ea717118d80332d9 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Handlers.meta b/Assets/Infrastructure/Network/WebSocket/Handlers.meta new file mode 100644 index 0000000..3e9543f --- /dev/null +++ b/Assets/Infrastructure/Network/WebSocket/Handlers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45c015a8db87cc9409bf35790fcb2470 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs index 4e2bce3..12fd0d1 100644 --- a/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs +++ b/Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs @@ -18,7 +18,6 @@ public interface INativeWebSocket : IDisposable event Action OnDisconnected; event Action OnError; event Action OnMessageReceived; - event Action OnBinaryDataReceived; // 연결 관리 UniTask ConnectAsync(string url, CancellationToken cancellationToken = default); @@ -26,6 +25,5 @@ public interface INativeWebSocket : IDisposable // 메시지 전송 UniTask SendMessageAsync(string message, CancellationToken cancellationToken = default); - UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs b/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs deleted file mode 100644 index 4311cf7..0000000 --- a/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -using ProjectVG.Infrastructure.Network.DTOs.WebSocket; - -namespace ProjectVG.Infrastructure.Network.WebSocket -{ - /// - /// WebSocket 이벤트 핸들러 인터페이스 - /// - public interface IWebSocketHandler - { - /// - /// 연결 성공 시 호출 - /// - void OnConnected(); - - /// - /// 연결 해제 시 호출 - /// - void OnDisconnected(); - - /// - /// 연결 오류 시 호출 - /// - /// 오류 메시지 - void OnError(string error); - - /// - /// 메시지 수신 시 호출 - /// - /// 수신된 메시지 - void OnMessageReceived(WebSocketMessage message); - - /// - /// 채팅 메시지 수신 시 호출 - /// - /// 채팅 메시지 - void OnChatMessageReceived(ChatMessage message); - - /// - /// 시스템 메시지 수신 시 호출 - /// - /// 시스템 메시지 - void OnSystemMessageReceived(SystemMessage message); - - /// - /// 연결 상태 메시지 수신 시 호출 - /// - /// 연결 상태 메시지 - void OnConnectionMessageReceived(ConnectionMessage message); - - /// - /// 세션 ID 메시지 수신 시 호출 - /// - /// 세션 ID 메시지 - void OnSessionIdMessageReceived(SessionIdMessage message); - - /// - /// 오디오 데이터 수신 시 호출 - /// - /// 오디오 바이트 데이터 - void OnAudioDataReceived(byte[] audioData); - - /// - /// 통합 메시지 수신 시 호출 (텍스트 + 오디오) - /// - /// 통합 메시지 - void OnIntegratedMessageReceived(IntegratedMessage message); - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs.meta b/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs.meta deleted file mode 100644 index 34988b4..0000000 --- a/Assets/Infrastructure/Network/WebSocket/IWebSocketHandler.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 90c60eb0de0f434479cda3d51cf4c95f \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs index d147294..4c8bacf 100644 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs @@ -11,6 +11,7 @@ namespace ProjectVG.Infrastructure.Network.WebSocket.Platforms /// /// 데스크톱 플랫폼용 WebSocket 구현체 /// System.Net.WebSockets.ClientWebSocket을 사용합니다. + /// JSON 메시지만 처리합니다. /// public class DesktopWebSocket : INativeWebSocket { @@ -21,7 +22,6 @@ public class DesktopWebSocket : INativeWebSocket public event Action OnDisconnected; public event Action OnError; public event Action OnMessageReceived; - public event Action OnBinaryDataReceived; private ClientWebSocket _webSocket; private CancellationTokenSource _cancellationTokenSource; @@ -116,26 +116,6 @@ public async UniTask SendMessageAsync(string message, CancellationToken ca } } - public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) - { - if (!IsConnected || _webSocket.State != WebSocketState.Open) - { - Debug.LogWarning("Desktop WebSocket이 연결되지 않았습니다."); - return false; - } - - try - { - await _webSocket.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, cancellationToken); - return true; - } - catch (Exception ex) - { - Debug.LogError($"Desktop WebSocket 바이너리 전송 실패: {ex.Message}"); - return false; - } - } - private async Task ReceiveLoopAsync() { var buffer = new byte[4096]; @@ -158,9 +138,8 @@ private async Task ReceiveLoopAsync() } else if (result.MessageType == WebSocketMessageType.Binary) { - var data = new byte[result.Count]; - Array.Copy(buffer, data, result.Count); - OnBinaryDataReceived?.Invoke(data); + // 바이너리 메시지는 무시 (JSON만 처리) + Debug.LogWarning("Desktop WebSocket: 바이너리 메시지 수신됨 (무시됨)"); } } } diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs index 753b562..7d94fad 100644 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs @@ -19,7 +19,6 @@ public class MobileWebSocket : INativeWebSocket public event Action OnError; #pragma warning disable CS0067 public event Action OnMessageReceived; - public event Action OnBinaryDataReceived; #pragma warning restore CS0067 private CancellationTokenSource _cancellationTokenSource; @@ -82,7 +81,7 @@ public async UniTask DisconnectAsync() // TODO : 네이티브 WebSocket 연결 해제 // TODO : 실제 구현 시 네이티브 플러그인 호출 - await UniTask.CompletedTask; // 비동기 작업 시뮬레이션 + await UniTask.CompletedTask; OnDisconnected?.Invoke(); } @@ -104,7 +103,7 @@ public async UniTask SendMessageAsync(string message, CancellationToken ca { // TODO : 네이티브 WebSocket 메시지 전송 // TODO : 실제 구현 시 네이티브 플러그인 호출 - await UniTask.CompletedTask; // 비동기 작업 시뮬레이션 + await UniTask.CompletedTask; return true; } catch (Exception ex) @@ -114,28 +113,6 @@ public async UniTask SendMessageAsync(string message, CancellationToken ca } } - public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) - { - if (!IsConnected) - { - Debug.LogWarning("모바일 WebSocket이 연결되지 않았습니다."); - return false; - } - - try - { - // TODO : 네이티브 WebSocket 바이너리 전송 - // TODO : 실제 구현 시 네이티브 플러그인 호출 - await UniTask.CompletedTask; // 비동기 작업 시뮬레이션 - return true; - } - catch (Exception ex) - { - Debug.LogError($"모바일 WebSocket 바이너리 전송 실패: {ex.Message}"); - return false; - } - } - private async UniTask ReceiveLoopAsync() { try diff --git a/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs index bbf7e0b..849911d 100644 --- a/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs +++ b/Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs @@ -20,7 +20,6 @@ public class WebGLWebSocket : INativeWebSocket public event Action OnError; #pragma warning disable CS0067 public event Action OnMessageReceived; - public event Action OnBinaryDataReceived; #pragma warning restore CS0067 private UnityWebRequest _webRequest; @@ -129,26 +128,6 @@ public async UniTask SendMessageAsync(string message, CancellationToken ca } } - public async UniTask SendBinaryAsync(byte[] data, CancellationToken cancellationToken = default) - { - if (!IsConnected) - { - Debug.LogWarning("WebGL WebSocket이 연결되지 않았습니다."); - return false; - } - - try - { - await UniTask.CompletedTask; - return true; - } - catch (Exception ex) - { - Debug.LogError($"WebGL WebSocket 바이너리 전송 실패: {ex.Message}"); - return false; - } - } - private async UniTask ReceiveLoopAsync() { try diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs b/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs deleted file mode 100644 index 52155cf..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Text; -using UnityEngine; -using ProjectVG.Infrastructure.Network.DTOs.WebSocket; - -namespace ProjectVG.Infrastructure.Network.WebSocket.Processors -{ - /// - /// 바이너리 메시지 처리기 (Bridge Pattern의 구현체) - /// - public class BinaryMessageProcessor : IMessageProcessor - { - public string MessageType => "binary"; - - // 메시지 타입 상수 - private const byte MESSAGE_TYPE_INTEGRATED = 0x03; - private const byte MESSAGE_TYPE_TEXT_ONLY = 0x01; - private const byte MESSAGE_TYPE_AUDIO_ONLY = 0x02; - - public void ProcessMessage(string message, System.Collections.Generic.List handlers) - { - // 바이너리 프로세서는 문자열 메시지를 처리하지 않음 - Debug.LogWarning("바이너리 프로세서는 문자열 메시지를 처리하지 않습니다."); - } - - public void ProcessBinaryMessage(byte[] data, System.Collections.Generic.List handlers) - { - try - { - Debug.Log($"바이너리 메시지 처리: {data.Length} bytes"); - - // 바이너리 메시지 파싱 시도 - var integratedMessage = ParseBinaryMessage(data); - if (integratedMessage != null) - { - ProcessIntegratedMessage(integratedMessage, handlers); - return; - } - - // 바이너리 파싱 실패 시 순수 오디오 데이터로 처리 - Debug.Log("바이너리 메시지 파싱 실패 - 순수 오디오 데이터로 처리"); - ProcessAudioData(data, handlers); - } - catch (Exception ex) - { - Debug.LogError($"바이너리 메시지 처리 실패: {ex.Message}"); - } - } - - public string ExtractSessionId(string message) - { - // 바이너리 프로세서는 문자열에서 세션 ID를 추출하지 않음 - return null; - } - - /// - /// 바이너리 메시지 파싱 - /// - private IntegratedMessage ParseBinaryMessage(byte[] data) - { - try - { - if (data.Length < 5) // 최소 길이 체크 - { - Debug.LogWarning("바이너리 메시지가 너무 짧습니다."); - return null; - } - - int offset = 0; - - // 메시지 타입 확인 (1바이트) - byte messageType = data[offset]; - offset += 1; - - if (messageType != MESSAGE_TYPE_INTEGRATED) - { - Debug.LogWarning($"지원하지 않는 메시지 타입: {messageType}"); - return null; - } - - // 세션 ID 읽기 - if (offset + 4 > data.Length) return null; - int sessionIdLength = BitConverter.ToInt32(data, offset); - offset += 4; - - if (offset + sessionIdLength > data.Length) return null; - string sessionId = Encoding.UTF8.GetString(data, offset, sessionIdLength); - offset += sessionIdLength; - - // 텍스트 읽기 - if (offset + 4 > data.Length) return null; - int textLength = BitConverter.ToInt32(data, offset); - offset += 4; - - string text = null; - if (textLength > 0) - { - if (offset + textLength > data.Length) return null; - text = Encoding.UTF8.GetString(data, offset, textLength); - offset += textLength; - } - - // 오디오 데이터 읽기 - if (offset + 4 > data.Length) return null; - int audioLength = BitConverter.ToInt32(data, offset); - offset += 4; - - byte[] audioData = null; - if (audioLength > 0) - { - if (offset + audioLength > data.Length) return null; - audioData = new byte[audioLength]; - Array.Copy(data, offset, audioData, 0, audioLength); - offset += audioLength; - } - - // 오디오 지속시간 읽기 (float) - if (offset + 4 > data.Length) return null; - float audioDuration = BitConverter.ToSingle(data, offset); - - return new IntegratedMessage - { - sessionId = sessionId, - text = text, - audioData = audioData, - audioDuration = audioDuration - }; - } - catch (Exception ex) - { - Debug.LogError($"바이너리 메시지 파싱 실패: {ex.Message}"); - return null; - } - } - - /// - /// 통합 메시지 처리 (텍스트 + 오디오) - /// - private void ProcessIntegratedMessage(IntegratedMessage integratedMessage, System.Collections.Generic.List handlers) - { - try - { - Debug.Log($"통합 메시지 수신 - 텍스트: {integratedMessage.text?.Length ?? 0}자, 오디오: {integratedMessage.audioData?.Length ?? 0}바이트"); - - // 이벤트 발생 - foreach (var handler in handlers) - { - handler.OnIntegratedMessageReceived(integratedMessage); - } - - // 텍스트가 있는 경우 텍스트 메시지로도 처리 - if (!string.IsNullOrEmpty(integratedMessage.text)) - { - var chatMessage = new ChatMessage - { - type = "chat", - sessionId = integratedMessage.sessionId, - message = integratedMessage.text, - timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - }; - - foreach (var handler in handlers) - { - handler.OnChatMessageReceived(chatMessage); - } - } - - // 오디오가 있는 경우 오디오 데이터로도 처리 - if (integratedMessage.audioData != null && integratedMessage.audioData.Length > 0) - { - ProcessAudioData(integratedMessage.audioData, handlers); - } - } - catch (Exception ex) - { - Debug.LogError($"통합 메시지 처리 실패: {ex.Message}"); - } - } - - /// - /// 오디오 데이터 처리 - /// - private void ProcessAudioData(byte[] audioData, System.Collections.Generic.List handlers) - { - try - { - Debug.Log($"오디오 데이터 수신: {audioData.Length} bytes"); - - // 이벤트 발생 - foreach (var handler in handlers) - { - handler.OnAudioDataReceived(audioData); - } - } - catch (Exception ex) - { - Debug.LogError($"오디오 데이터 처리 실패: {ex.Message}"); - } - } - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs.meta b/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs.meta deleted file mode 100644 index d925f89..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Processors/BinaryMessageProcessor.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 31ced92dddfa10a4a8b751118d8c9184 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs b/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs deleted file mode 100644 index 33c8f88..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using ProjectVG.Infrastructure.Network.DTOs.WebSocket; - -namespace ProjectVG.Infrastructure.Network.WebSocket.Processors -{ - /// - /// 메시지 처리기 인터페이스 (Bridge Pattern의 추상화) - /// - public interface IMessageProcessor - { - /// - /// 메시지 타입 - /// - string MessageType { get; } - - /// - /// 문자열 메시지 처리 - /// - /// 수신된 메시지 - /// 핸들러 목록 - void ProcessMessage(string message, System.Collections.Generic.List handlers); - - /// - /// 바이너리 메시지 처리 - /// - /// 수신된 바이너리 데이터 - /// 핸들러 목록 - void ProcessBinaryMessage(byte[] data, System.Collections.Generic.List handlers); - - /// - /// 세션 ID 추출 - /// - /// 메시지 - /// 세션 ID - string ExtractSessionId(string message); - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs.meta b/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs.meta deleted file mode 100644 index 87f346b..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Processors/IMessageProcessor.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 4a6d71367a30ec447bd3598805727650 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs b/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs deleted file mode 100644 index 21d4175..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using UnityEngine; -using ProjectVG.Infrastructure.Network.DTOs.WebSocket; - -namespace ProjectVG.Infrastructure.Network.WebSocket.Processors -{ - /// - /// JSON 메시지 처리기 (Bridge Pattern의 구현체) - /// - public class JsonMessageProcessor : IMessageProcessor - { - public string MessageType => "json"; - - public void ProcessMessage(string message, System.Collections.Generic.List handlers) - { - try - { - Debug.Log($"JSON 메시지 처리: {message}"); - - // 세션 ID 메시지 특별 처리 - if (message.Contains("\"type\":\"session_id\"")) - { - ProcessSessionIdMessage(message, handlers); - return; - } - - // JSON 메시지 파싱 및 처리 - var baseMessage = ParseJsonMessage(message); - if (baseMessage != null) - { - ProcessReceivedMessage(baseMessage, handlers); - } - } - catch (Exception ex) - { - Debug.LogError($"JSON 메시지 처리 실패: {ex.Message}"); - } - } - - public void ProcessBinaryMessage(byte[] data, System.Collections.Generic.List handlers) - { - // JSON 프로세서는 바이너리 메시지를 처리하지 않음 - Debug.LogWarning("JSON 프로세서는 바이너리 메시지를 처리하지 않습니다."); - } - - public string ExtractSessionId(string message) - { - try - { - if (message.Contains("\"type\":\"session_id\"")) - { - int sessionIdStart = message.IndexOf("\"session_id\":\"") + 14; - int sessionIdEnd = message.IndexOf("\"", sessionIdStart); - if (sessionIdStart > 13 && sessionIdEnd > sessionIdStart) - { - return message.Substring(sessionIdStart, sessionIdEnd - sessionIdStart); - } - } - return null; - } - catch (Exception ex) - { - Debug.LogError($"세션 ID 추출 실패: {ex.Message}"); - return null; - } - } - - private void ProcessSessionIdMessage(string message, System.Collections.Generic.List handlers) - { - Debug.Log("세션 ID 메시지 감지됨"); - - var sessionId = ExtractSessionId(message); - if (!string.IsNullOrEmpty(sessionId)) - { - Debug.Log($"세션 ID 저장됨: {sessionId}"); - - // 핸들러들에게 세션 ID 메시지 전달 - var sessionMessage = new SessionIdMessage { session_id = sessionId }; - foreach (var handler in handlers) - { - Debug.Log($"핸들러에게 세션 ID 전달: {handler.GetType().Name}"); - handler.OnSessionIdMessageReceived(sessionMessage); - } - } - else - { - Debug.LogError("세션 ID 추출 실패 - JSON 형식 확인 필요"); - } - } - - private WebSocketMessage ParseJsonMessage(string message) - { - try - { - return JsonUtility.FromJson(message); - } - catch (Exception ex) - { - Debug.LogError($"JSON 메시지 파싱 실패: {ex.Message}"); - return null; - } - } - - private void ProcessReceivedMessage(WebSocketMessage baseMessage, System.Collections.Generic.List handlers) - { - try - { - Debug.Log($"메시지 수신: {baseMessage.type} - {baseMessage.data}"); - - // 메시지 타입에 따른 처리 - switch (baseMessage.type?.ToLower()) - { - case "session_id": - Debug.Log("세션 ID 메시지 처리 중..."); - var sessionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); - foreach (var handler in handlers) - { - handler.OnSessionIdMessageReceived(sessionMessage); - } - break; - - case "chat": - var chatMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); - foreach (var handler in handlers) - { - handler.OnChatMessageReceived(chatMessage); - } - break; - - case "system": - var systemMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); - foreach (var handler in handlers) - { - handler.OnSystemMessageReceived(systemMessage); - } - break; - - case "connection": - var connectionMessage = JsonUtility.FromJson(JsonUtility.ToJson(baseMessage)); - foreach (var handler in handlers) - { - handler.OnConnectionMessageReceived(connectionMessage); - } - break; - } - } - catch (Exception ex) - { - Debug.LogError($"메시지 처리 실패: {ex.Message}"); - } - } - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs.meta b/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs.meta deleted file mode 100644 index bb8ca30..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Processors/JsonMessageProcessor.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 937f58ba9a4ac1e42a3ebfe4496e8f52 \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs b/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs deleted file mode 100644 index 9c7f767..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace ProjectVG.Infrastructure.Network.WebSocket.Processors -{ - /// - /// 메시지 처리기 팩토리 (Factory Pattern) - /// - public static class MessageProcessorFactory - { - private static readonly Dictionary _processors = new Dictionary(); - - static MessageProcessorFactory() - { - // 기본 프로세서 등록 - RegisterProcessor(new JsonMessageProcessor()); - RegisterProcessor(new BinaryMessageProcessor()); - } - - /// - /// 프로세서 등록 - /// - public static void RegisterProcessor(IMessageProcessor processor) - { - if (processor != null && !string.IsNullOrEmpty(processor.MessageType)) - { - _processors[processor.MessageType.ToLower()] = processor; - Debug.Log($"메시지 프로세서 등록: {processor.MessageType}"); - } - } - - /// - /// 메시지 타입에 따른 프로세서 생성 - /// - public static IMessageProcessor CreateProcessor(string messageType) - { - if (string.IsNullOrEmpty(messageType)) - { - Debug.LogWarning("메시지 타입이 null입니다. JSON 프로세서를 사용합니다."); - return GetDefaultProcessor(); - } - - var key = messageType.ToLower(); - if (_processors.TryGetValue(key, out var processor)) - { - Debug.Log($"메시지 프로세서 생성: {messageType}"); - return processor; - } - - Debug.LogWarning($"지원하지 않는 메시지 타입: {messageType}. JSON 프로세서를 사용합니다."); - return GetDefaultProcessor(); - } - - /// - /// 기본 프로세서 (JSON) 반환 - /// - public static IMessageProcessor GetDefaultProcessor() - { - return _processors["json"]; - } - - /// - /// 등록된 모든 프로세서 타입 반환 - /// - public static IEnumerable GetAvailableProcessors() - { - return _processors.Keys; - } - - /// - /// 프로세서 존재 여부 확인 - /// - public static bool HasProcessor(string messageType) - { - if (string.IsNullOrEmpty(messageType)) - return false; - - return _processors.ContainsKey(messageType.ToLower()); - } - } -} \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs.meta b/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs.meta deleted file mode 100644 index 2879dbe..0000000 --- a/Assets/Infrastructure/Network/WebSocket/Processors/MessageProcessorFactory.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 050f5d1205efd4f49b766a692fbd5cef \ No newline at end of file diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs index 2356a85..6f5b1b7 100644 --- a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs @@ -6,55 +6,40 @@ using UnityEngine.Networking; using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.Configs; -using ProjectVG.Infrastructure.Network.DTOs.WebSocket; -using ProjectVG.Infrastructure.Network.WebSocket.Processors; namespace ProjectVG.Infrastructure.Network.WebSocket { /// - /// WebSocket 연결 및 메시지 관리자 (Bridge Pattern 적용) - /// 메시지 처리기를 통해 JSON과 바이너리 메시지를 분리하여 처리합니다. + /// WebSocket 연결 및 메시지 관리자 + /// 강제된 JSON 형식 {type: "xxx", data: {...}}을 사용합니다. /// public class WebSocketManager : MonoBehaviour { - [Header("WebSocket Configuration")] - // NetworkConfig를 사용하여 설정을 관리합니다. - private INativeWebSocket _nativeWebSocket; private CancellationTokenSource _cancellationTokenSource; - private List _handlers = new List(); - - // Bridge Pattern: 메시지 처리기 - private IMessageProcessor _messageProcessor; - // 메시지 버퍼링을 위한 필드 추가 + // 메시지 버퍼링을 위한 필드 private readonly StringBuilder _messageBuffer = new StringBuilder(); private readonly object _bufferLock = new object(); - private bool _isProcessingMessage = false; private bool _isConnected = false; private bool _isConnecting = false; private int _reconnectAttempts = 0; private string _sessionId; - - public static WebSocketManager Instance { get; private set; } // 이벤트 public event Action OnConnected; public event Action OnDisconnected; public event Action OnError; - public event Action OnMessageReceived; - public event Action OnAudioDataReceived; - public event Action OnIntegratedMessageReceived; - + public event Action OnSessionIdReceived; + public event Action OnChatMessageReceived; public bool IsConnected => _isConnected; public bool IsConnecting => _isConnecting; public string SessionId => _sessionId; - private void Awake() { if (Instance == null) @@ -79,69 +64,17 @@ private void OnDestroy() private void InitializeManager() { _cancellationTokenSource = new CancellationTokenSource(); - - // NetworkConfig 기반으로 메시지 처리기 설정 - InitializeMessageProcessor(); - - // Native WebSocket 초기화 InitializeNativeWebSocket(); } - - /// - /// NetworkConfig 기반으로 메시지 처리기 초기화 - /// - private void InitializeMessageProcessor() - { - var messageType = NetworkConfig.WebSocketMessageType; - _messageProcessor = MessageProcessorFactory.CreateProcessor(messageType); - Debug.Log($"NetworkConfig 기반 메시지 처리기 초기화: {messageType}"); - - // 현재 설정 로그 출력 - Debug.Log($"=== WebSocket 메시지 처리 설정 ==="); - Debug.Log($"NetworkConfig 메시지 타입: {messageType}"); - Debug.Log($"JSON 형식 지원: {NetworkConfig.IsJsonMessageType}"); - Debug.Log($"바이너리 형식 지원: {NetworkConfig.IsBinaryMessageType}"); - Debug.Log($"====================================="); - } private void InitializeNativeWebSocket() { - // 플랫폼별 WebSocket 구현체 생성 _nativeWebSocket = WebSocketFactory.Create(); - // 이벤트 연결 _nativeWebSocket.OnConnected += OnNativeConnected; _nativeWebSocket.OnDisconnected += OnNativeDisconnected; _nativeWebSocket.OnError += OnNativeError; _nativeWebSocket.OnMessageReceived += OnNativeMessageReceived; - _nativeWebSocket.OnBinaryDataReceived += OnNativeBinaryDataReceived; - } - - - - - - /// - /// 핸들러 등록 - /// - public void RegisterHandler(IWebSocketHandler handler) - { - if (!_handlers.Contains(handler)) - { - _handlers.Add(handler); - Debug.Log($"WebSocket 핸들러 등록: {handler.GetType().Name}"); - } - } - - /// - /// 핸들러 해제 - /// - public void UnregisterHandler(IWebSocketHandler handler) - { - if (_handlers.Remove(handler)) - { - Debug.Log($"WebSocket 핸들러 해제: {handler.GetType().Name}"); - } } /// @@ -165,7 +98,6 @@ public async UniTask ConnectAsync(string sessionId = null, CancellationTok var wsUrl = GetWebSocketUrl(sessionId); Debug.Log($"WebSocket 연결 시도: {wsUrl}"); - // Native WebSocket을 통한 실제 연결 var success = await _nativeWebSocket.ConnectAsync(wsUrl, combinedCancellationToken); if (success) @@ -173,12 +105,7 @@ public async UniTask ConnectAsync(string sessionId = null, CancellationTok _isConnected = true; _isConnecting = false; _reconnectAttempts = 0; - Debug.Log("WebSocket 연결 성공"); - - // 연결 후 서버 설정 로드 시도 (선택적) - // LoadServerConfigAsync().Forget(); - return true; } else @@ -196,13 +123,7 @@ public async UniTask ConnectAsync(string sessionId = null, CancellationTok { var error = $"WebSocket 연결 중 예외 발생: {ex.Message}"; Debug.LogError(error); - OnError?.Invoke(error); - foreach (var handler in _handlers) - { - handler.OnError(error); - } - return false; } finally @@ -224,27 +145,19 @@ public async UniTask DisconnectAsync() _isConnected = false; _isConnecting = false; - // Native WebSocket 연결 해제 if (_nativeWebSocket != null) { await _nativeWebSocket.DisconnectAsync(); } Debug.Log("WebSocket 연결 해제"); - OnDisconnected?.Invoke(); - foreach (var handler in _handlers) - { - handler.OnDisconnected(); - } - - await UniTask.CompletedTask; } /// /// 메시지 전송 /// - public async UniTask SendMessageAsync(WebSocketMessage message, CancellationToken cancellationToken = default) + public async UniTask SendMessageAsync(string type, object data, CancellationToken cancellationToken = default) { if (!_isConnected) { @@ -254,6 +167,12 @@ public async UniTask SendMessageAsync(WebSocketMessage message, Cancellati try { + var message = new WebSocketMessage + { + type = type, + data = data + }; + var jsonMessage = JsonUtility.ToJson(message); return await _nativeWebSocket.SendMessageAsync(jsonMessage, cancellationToken); } @@ -267,20 +186,16 @@ public async UniTask SendMessageAsync(WebSocketMessage message, Cancellati /// /// 채팅 메시지 전송 /// - public async UniTask SendChatMessageAsync(string message, string characterId, string userId, string actor = null, CancellationToken cancellationToken = default) + public async UniTask SendChatMessageAsync(string message, CancellationToken cancellationToken = default) { - var chatMessage = new ChatMessage + var chatData = new ChatData { - type = "chat", - sessionId = _sessionId, - timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), message = message, - characterId = characterId, - userId = userId, - actor = actor + sessionId = _sessionId, + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; - return await SendMessageAsync(chatMessage, cancellationToken); + return await SendMessageAsync("chat", chatData, cancellationToken); } /// @@ -290,24 +205,12 @@ private string GetWebSocketUrl(string sessionId = null) { string baseUrl = NetworkConfig.GetWebSocketUrl(); - Debug.Log($"=== WebSocket URL 생성 ==="); - Debug.Log($"기본 URL: {baseUrl}"); - Debug.Log($"세션 ID: {sessionId}"); - - string finalUrl; if (!string.IsNullOrEmpty(sessionId)) { - finalUrl = $"{baseUrl}?sessionId={sessionId}"; - } - else - { - finalUrl = baseUrl; + return $"{baseUrl}?sessionId={sessionId}"; } - Debug.Log($"최종 URL: {finalUrl}"); - Debug.Log($"================================"); - - return finalUrl; + return baseUrl; } /// @@ -343,15 +246,8 @@ private void OnNativeConnected() _isConnecting = false; _reconnectAttempts = 0; - Debug.Log("WebSocket 연결 성공 - 핸들러 수: " + _handlers.Count); - - // 이벤트 발생 + Debug.Log("WebSocket 연결 성공"); OnConnected?.Invoke(); - foreach (var handler in _handlers) - { - Debug.Log($"핸들러에게 연결 이벤트 전달: {handler.GetType().Name}"); - handler.OnConnected(); - } } private void OnNativeDisconnected() @@ -359,14 +255,7 @@ private void OnNativeDisconnected() _isConnected = false; _isConnecting = false; - // 이벤트 발생 OnDisconnected?.Invoke(); - foreach (var handler in _handlers) - { - handler.OnDisconnected(); - } - - // 자동 재연결 시도 TryReconnectAsync().Forget(); } @@ -375,26 +264,14 @@ private void OnNativeError(string error) _isConnected = false; _isConnecting = false; - // 이벤트 발생 OnError?.Invoke(error); - foreach (var handler in _handlers) - { - handler.OnError(error); - } } private void OnNativeMessageReceived(string message) { try { - Debug.Log($"=== 메시지 수신 디버깅 ==="); - Debug.Log($"원시 메시지 길이: {message?.Length ?? 0}"); - Debug.Log($"원시 메시지 (처음 100자): {message?.Substring(0, Math.Min(100, message?.Length ?? 0))}"); - Debug.Log($"핸들러 수: {_handlers.Count}"); - Debug.Log($"현재 메시지 처리기: {_messageProcessor.MessageType}"); - Debug.Log($"================================"); - - // 메시지 버퍼링 처리 + Debug.Log($"메시지 수신: {message?.Length ?? 0} bytes"); ProcessBufferedMessage(message); } catch (Exception ex) @@ -411,34 +288,24 @@ private void ProcessBufferedMessage(string message) { lock (_bufferLock) { - // 메시지를 버퍼에 추가 _messageBuffer.Append(message); string bufferedMessage = _messageBuffer.ToString(); Debug.Log($"버퍼링된 메시지 길이: {bufferedMessage.Length}"); - Debug.Log($"버퍼링된 메시지 (처음 100자): {bufferedMessage.Substring(0, Math.Min(100, bufferedMessage.Length))}"); - // 완전한 JSON 메시지인지 확인 if (IsCompleteJsonMessage(bufferedMessage)) { Debug.Log("완전한 JSON 메시지 감지됨. 처리 시작."); - // JSON 형식인지 확인 if (IsValidJsonMessage(bufferedMessage)) { - Debug.Log("JSON 형식으로 처리합니다."); - // Bridge Pattern: 메시지 처리기에 위임 - _messageProcessor.ProcessMessage(bufferedMessage, _handlers); + ProcessMessage(bufferedMessage); } else { - Debug.LogWarning("JSON 형식이 아닌 메시지가 문자열로 수신됨. 바이너리 처리기로 전달합니다."); - // 바이너리 처리기로 전달 - var binaryProcessor = MessageProcessorFactory.CreateProcessor("binary"); - binaryProcessor.ProcessMessage(bufferedMessage, _handlers); + Debug.LogWarning("JSON 형식이 아닌 메시지가 수신됨"); } - // 버퍼 초기화 _messageBuffer.Clear(); Debug.Log("메시지 처리 완료. 버퍼 초기화됨."); } @@ -457,7 +324,6 @@ private bool IsCompleteJsonMessage(string message) if (string.IsNullOrEmpty(message)) return false; - // 중괄호 개수로 완전한 JSON인지 확인 int openBraces = 0; int closeBraces = 0; bool inString = false; @@ -496,39 +362,121 @@ private bool IsValidJsonMessage(string message) message = message.Trim(); - // JSON 객체 시작/끝 확인 if (message.StartsWith("{") && message.EndsWith("}")) return true; - // JSON 배열 시작/끝 확인 if (message.StartsWith("[") && message.EndsWith("]")) return true; - // 세션 ID 메시지 특별 처리 - if (message.Contains("\"type\":\"session_id\"")) - return true; - return false; } - private void OnNativeBinaryDataReceived(byte[] data) + /// + /// 메시지 타입에 따른 처리 + /// + private void ProcessMessage(string message) { try { - Debug.Log($"바이너리 데이터 수신: {data.Length} bytes"); - Debug.Log($"현재 메시지 처리기: {_messageProcessor.MessageType}"); - - // Bridge Pattern: 메시지 처리기에 위임 - _messageProcessor.ProcessBinaryMessage(data, _handlers); + var webSocketMessage = JsonUtility.FromJson(message); + if (webSocketMessage == null) + { + Debug.LogError("WebSocket 메시지 파싱 실패"); + return; + } + + Debug.Log($"메시지 타입: {webSocketMessage.type}"); + + switch (webSocketMessage.type) + { + case "session_id": + ProcessSessionIdMessage(webSocketMessage.data); + break; + case "chat": + ProcessChatMessage(webSocketMessage.data); + break; + default: + Debug.LogWarning($"알 수 없는 메시지 타입: {webSocketMessage.type}"); + break; + } } catch (Exception ex) { - Debug.LogError($"바이너리 데이터 처리 실패: {ex.Message}"); + Debug.LogError($"메시지 처리 중 오류: {ex.Message}"); + } + } + + /// + /// 세션 ID 메시지 처리 + /// + private void ProcessSessionIdMessage(object data) + { + try + { + var sessionIdData = JsonUtility.FromJson(JsonUtility.ToJson(data)); + if (sessionIdData != null) + { + _sessionId = sessionIdData.session_id; + Debug.Log($"세션 ID 수신: {sessionIdData.session_id}"); + OnSessionIdReceived?.Invoke(sessionIdData.session_id); + } + } + catch (Exception ex) + { + Debug.LogError($"세션 ID 메시지 처리 중 오류: {ex.Message}"); + } + } + + /// + /// 채팅 메시지 처리 + /// + private void ProcessChatMessage(object data) + { + try + { + var chatData = JsonUtility.FromJson(JsonUtility.ToJson(data)); + if (chatData != null) + { + Debug.Log($"채팅 메시지 수신: {chatData.message}"); + OnChatMessageReceived?.Invoke(chatData.message); + } + } + catch (Exception ex) + { + Debug.LogError($"채팅 메시지 처리 중 오류: {ex.Message}"); } } #endregion } + /// + /// WebSocket 메시지 기본 구조 + /// + [Serializable] + public class WebSocketMessage + { + public string type; + public object data; + } + /// + /// 세션 ID 데이터 + /// + [Serializable] + public class SessionIdData + { + public string session_id; + } + + /// + /// 채팅 데이터 + /// + [Serializable] + public class ChatData + { + public string message; + public string sessionId; + public long timestamp; + } } \ No newline at end of file diff --git a/Assets/Tests/Runtime/NetworkTestManager.cs b/Assets/Tests/Runtime/NetworkTestManager.cs index 131d73f..e60a124 100644 --- a/Assets/Tests/Runtime/NetworkTestManager.cs +++ b/Assets/Tests/Runtime/NetworkTestManager.cs @@ -6,25 +6,24 @@ using ProjectVG.Infrastructure.Network.Services; using ProjectVG.Infrastructure.Network.Configs; using ProjectVG.Infrastructure.Network.Http; -using ProjectVG.Infrastructure.Network.DTOs.WebSocket; +using ProjectVG.Infrastructure.Network.DTOs.Chat; namespace ProjectVG.Tests.Runtime { /// /// WebSocket + HTTP 통합 테스트 매니저 - /// WebSocket 연결 → 세션 ID 수신 → HTTP 요청 → WebSocket으로 결과 수신 - /// JSON과 바이너리 메시지를 모두 처리합니다. + /// 더미 클라이언트와 동일한 방식으로 테스트합니다. /// public class NetworkTestManager : MonoBehaviour { [Header("테스트 설정")] - [SerializeField] private string testSessionId = "test-session-123"; [SerializeField] private string testCharacterId = "44444444-4444-4444-4444-444444444444"; // 제로 [SerializeField] private string testUserId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; + [SerializeField] private string testMessage = "안녕하세요! 테스트 메시지입니다."; [Header("자동 테스트")] [SerializeField] private bool autoTest = false; - [SerializeField] private float testInterval = 10f; // 테스트 완료 시간을 고려하여 10초로 증가 + [SerializeField] private float testInterval = 15f; // 더 긴 간격으로 변경 // UI에서 접근할 수 있도록 public 프로퍼티 추가 public bool AutoTest @@ -41,15 +40,14 @@ public float TestInterval private WebSocketManager _webSocketManager; private ApiServiceManager _apiServiceManager; - private DefaultWebSocketHandler _webSocketHandler; private CancellationTokenSource _cancellationTokenSource; private bool _isTestRunning = false; - private string _receivedSessionId = null; // WebSocket에서 받은 세션 ID - private bool _chatResponseReceived = false; // 채팅 응답 수신 여부 - private string _lastChatResponse = null; // 마지막 채팅 응답 - private bool _integratedMessageReceived = false; // 통합 메시지 수신 여부 - private bool _audioDataReceived = false; // 오디오 데이터 수신 여부 - + private string _receivedSessionId = null; + private bool _chatResponseReceived = false; + private string _lastChatResponse = null; + private int _reconnectAttempts = 0; + private const int MAX_RECONNECT_ATTEMPTS = 3; + private bool _isIntentionalDisconnect = false; // 의도적인 연결 해제 여부 private void Awake() { @@ -129,7 +127,7 @@ private void InitializeManagers() try { // NetworkConfig 초기화 (앱 시작 시 환경 설정) - NetworkConfig.SetDevelopmentEnvironment(); // 또는 SetTestEnvironment(), SetProductionEnvironment() + NetworkConfig.SetDevelopmentEnvironment(); // WebSocket 매니저 초기화 _webSocketManager = WebSocketManager.Instance; @@ -156,27 +154,12 @@ private void InitializeManagers() return; } - // WebSocket 핸들러 생성 및 등록 - _webSocketHandler = gameObject.AddComponent(); - if (_webSocketHandler == null) - { - Debug.LogError("DefaultWebSocketHandler 생성에 실패했습니다."); - return; - } - - _webSocketManager.RegisterHandler(_webSocketHandler); - - // 이벤트 구독 - _webSocketHandler.OnConnectedEvent += OnWebSocketConnected; - _webSocketHandler.OnDisconnectedEvent += OnWebSocketDisconnected; - _webSocketHandler.OnErrorEvent += OnWebSocketError; - _webSocketHandler.OnChatMessageReceivedEvent += OnChatMessageReceived; - _webSocketHandler.OnSystemMessageReceivedEvent += OnSystemMessageReceived; - _webSocketHandler.OnSessionIdMessageReceivedEvent += OnSessionIdMessageReceived; - _webSocketHandler.OnAudioDataReceivedEvent += OnAudioDataReceived; - _webSocketHandler.OnIntegratedMessageReceivedEvent += OnIntegratedMessageReceived; - - + // WebSocket 이벤트 구독 + _webSocketManager.OnConnected += OnWebSocketConnected; + _webSocketManager.OnDisconnected += OnWebSocketDisconnected; + _webSocketManager.OnError += OnWebSocketError; + _webSocketManager.OnSessionIdReceived += OnSessionIdReceived; + _webSocketManager.OnChatMessageReceived += OnChatMessageReceived; Debug.Log("NetworkTestManager 초기화 완료"); NetworkConfig.LogCurrentSettings(); @@ -189,8 +172,6 @@ private void InitializeManagers() #region 수동 테스트 메서드들 - - [ContextMenu("1. WebSocket 연결 (더미 클라이언트 방식)")] public async void ConnectWebSocket() { @@ -215,6 +196,8 @@ public async void ConnectWebSocket() Debug.Log($"WebSocket 서버: {NetworkConfig.GetWebSocketUrl()}"); _receivedSessionId = null; // 세션 ID 초기화 + _reconnectAttempts = 0; + _isIntentionalDisconnect = false; // 의도적 해제 플래그 초기화 // 더미 클라이언트처럼 세션 ID 없이 연결 bool connected = await _webSocketManager.ConnectAsync(); @@ -222,7 +205,6 @@ public async void ConnectWebSocket() if (connected) { Debug.Log("✅ WebSocket 연결 성공! 세션 ID 대기 중..."); - Debug.Log($"현재 메시지 처리기: {NetworkConfig.WebSocketMessageType}"); } else { @@ -266,11 +248,9 @@ public async void SendChatRequest() Debug.Log($"API 서버: {NetworkConfig.GetFullApiUrl("chat")}"); Debug.Log($"세션 ID: {_receivedSessionId}"); - - - var chatRequest = new ProjectVG.Infrastructure.Network.DTOs.Chat.ChatRequest + var chatRequest = new ChatRequest { - message = "안녕하세요! 테스트 메시지입니다.", + message = testMessage, characterId = testCharacterId, userId = testUserId, sessionId = _receivedSessionId, // WebSocket에서 받은 세션 ID 사용 @@ -342,9 +322,7 @@ public async void SendWebSocketMessage() Debug.Log("=== WebSocket 메시지 전송 시작 ==="); bool sent = await _webSocketManager.SendChatMessageAsync( - message: "WebSocket으로 직접 전송하는 테스트 메시지", - characterId: testCharacterId, - userId: testUserId + message: "WebSocket으로 직접 전송하는 테스트 메시지" ); if (sent) @@ -368,6 +346,7 @@ public async void DisconnectWebSocket() try { Debug.Log("=== WebSocket 연결 해제 시작 ==="); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 await _webSocketManager.DisconnectAsync(); _receivedSessionId = null; // 세션 ID 초기화 Debug.Log("✅ WebSocket 연결 해제 완료!"); @@ -378,45 +357,6 @@ public async void DisconnectWebSocket() } } - [ContextMenu("6. 통합 메시지 처리 테스트")] - public async void TestIntegratedMessageHandling() - { - if (!_webSocketManager.IsConnected) - { - Debug.LogWarning("WebSocket이 연결되지 않았습니다. 먼저 연결해주세요."); - return; - } - - try - { - Debug.Log("=== 통합 메시지 처리 테스트 시작 ==="); - - // 응답 상태 초기화 - _chatResponseReceived = false; - _integratedMessageReceived = false; - _audioDataReceived = false; - _lastChatResponse = null; - - // HTTP 채팅 요청으로 통합 메시지 유도 - if (!string.IsNullOrEmpty(_receivedSessionId)) - { - Debug.Log("HTTP 채팅 요청으로 통합 메시지 유도 중..."); - await SendChatRequestInternal(); - - // 통합 메시지 수신 대기 - await WaitForIntegratedMessage(15); - } - else - { - Debug.LogWarning("세션 ID가 없습니다. 먼저 WebSocket 연결을 통해 세션 ID를 받아주세요."); - } - } - catch (Exception ex) - { - Debug.LogError($"통합 메시지 처리 테스트 중 오류: {ex.Message}"); - } - } - [ContextMenu("더미 클라이언트 방식 전체 테스트")] public async void RunDummyClientTest() { @@ -441,6 +381,7 @@ public async void RunDummyClientTest() if (_webSocketManager.IsConnected) { Debug.Log("0️⃣ 기존 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 await _webSocketManager.DisconnectAsync(); await UniTask.Delay(1000); // 연결 해제 완료 대기 } @@ -457,7 +398,7 @@ public async void RunDummyClientTest() // 2. 세션 ID 수신 대기 (최대 10초) Debug.Log("2️⃣ 세션 ID 수신 대기 중..."); int waitCount = 0; - while (string.IsNullOrEmpty(_receivedSessionId) && waitCount < 100) + while (string.IsNullOrEmpty(_receivedSessionId) && waitCount < 100) // 10초로 증가 { await UniTask.Delay(100); waitCount++; @@ -493,6 +434,7 @@ public async void RunDummyClientTest() // 5. WebSocket 연결 해제 Debug.Log("5️⃣ WebSocket 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 await _webSocketManager.DisconnectAsync(); _receivedSessionId = null; @@ -533,35 +475,57 @@ public async void RunFullTest() // 1. WebSocket 연결 Debug.Log("1️⃣ WebSocket 연결 중..."); - bool connected = await _webSocketManager.ConnectAsync(testSessionId); + bool connected = await _webSocketManager.ConnectAsync(); if (!connected) { Debug.LogError("WebSocket 연결 실패로 테스트 중단"); return; } - await UniTask.Delay(1000); // 연결 안정화 대기 + // 2. 세션 ID 수신 대기 (최대 10초) + Debug.Log("2️⃣ 세션 ID 수신 대기 중..."); + int waitCount = 0; + while (string.IsNullOrEmpty(_receivedSessionId) && waitCount < 100) + { + await UniTask.Delay(100); + waitCount++; + if (waitCount % 10 == 0) // 1초마다 로그 + { + Debug.Log($"2️⃣ 세션 ID 대기 중... ({waitCount/10}초 경과)"); + } + } + + if (string.IsNullOrEmpty(_receivedSessionId)) + { + Debug.LogError("세션 ID를 받지 못했습니다. (10초 타임아웃)"); + return; + } - // 2. HTTP 채팅 요청 - Debug.Log("2️⃣ HTTP 채팅 요청 중..."); + Debug.Log($"✅ 세션 ID 수신: {_receivedSessionId}"); + await UniTask.Delay(1000); // 안정화 대기 + + // 3. HTTP 채팅 요청 + Debug.Log("3️⃣ HTTP 채팅 요청 중..."); await SendChatRequestInternal(); - await UniTask.Delay(1000); + // 채팅 응답을 기다림 + await WaitForChatResponse(15); // 15초 타임아웃 - // 3. HTTP 캐릭터 정보 요청 - Debug.Log("3️⃣ HTTP 캐릭터 정보 요청 중..."); + // 4. HTTP 캐릭터 정보 요청 + Debug.Log("4️⃣ HTTP 캐릭터 정보 요청 중..."); await GetCharacterInfoInternal(); await UniTask.Delay(1000); - // 4. WebSocket 메시지 전송 - Debug.Log("4️⃣ WebSocket 메시지 전송 중..."); + // 5. WebSocket 메시지 전송 + Debug.Log("5️⃣ WebSocket 메시지 전송 중..."); await SendWebSocketMessageInternal(); await UniTask.Delay(1000); - // 5. WebSocket 연결 해제 - Debug.Log("5️⃣ WebSocket 연결 해제 중..."); + // 6. WebSocket 연결 해제 + Debug.Log("6️⃣ WebSocket 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 await _webSocketManager.DisconnectAsync(); Debug.Log("✅ === 전체 테스트 완료 ==="); @@ -657,6 +621,7 @@ private async UniTask RunDummyClientTestInternal() if (_webSocketManager.IsConnected) { Debug.Log("자동 테스트 - 기존 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 await _webSocketManager.DisconnectAsync(); await UniTask.Delay(1000); } @@ -677,7 +642,7 @@ private async UniTask RunDummyClientTestInternal() { await UniTask.Delay(100); waitCount++; - if (waitCount % 10 == 0) // 1초마다 로그 + if (waitCount % 10 == 0) { Debug.Log($"자동 테스트 - 세션 ID 대기 중... ({waitCount/10}초 경과)"); } @@ -708,6 +673,7 @@ private async UniTask RunDummyClientTestInternal() // 5. 연결 해제 (더 오래 기다린 후) Debug.Log("자동 테스트 - WebSocket 응답 대기 완료, 연결 해제 중..."); + _isIntentionalDisconnect = true; // 의도적 해제 플래그 설정 await UniTask.Delay(2000); // 추가 대기 시간 await _webSocketManager.DisconnectAsync(); _receivedSessionId = null; @@ -739,7 +705,7 @@ private async UniTask SendChatRequestInternal() return; } - var chatRequest = new ProjectVG.Infrastructure.Network.DTOs.Chat.ChatRequest + var chatRequest = new ChatRequest { message = $"자동 테스트 메시지 - {DateTime.Now:HH:mm:ss}", characterId = testCharacterId, @@ -797,36 +763,6 @@ private async UniTask WaitForChatResponse(int timeoutSeconds = 15) } } - /// - /// 통합 메시지를 기다리는 메서드 - /// - private async UniTask WaitForIntegratedMessage(int timeoutSeconds = 15) - { - Debug.Log($"통합 메시지 대기 시작 (타임아웃: {timeoutSeconds}초)"); - - var startTime = DateTime.Now; - while (!_integratedMessageReceived && (DateTime.Now - startTime).TotalSeconds < timeoutSeconds) - { - await UniTask.Delay(100); - - // WebSocket 연결 상태 확인 - if (_webSocketManager != null && !_webSocketManager.IsConnected) - { - Debug.LogWarning("WebSocket 연결이 끊어졌습니다. 응답 대기 중단"); - break; - } - } - - if (_integratedMessageReceived) - { - Debug.Log($"✅ 통합 메시지 수신 완료"); - } - else - { - Debug.LogWarning($"⚠️ 통합 메시지 타임아웃 ({timeoutSeconds}초)"); - } - } - private async UniTask GetCharacterInfoInternal() { if (_apiServiceManager?.Character == null) @@ -851,9 +787,7 @@ private async UniTask SendWebSocketMessageInternal() } await _webSocketManager.SendChatMessageAsync( - message: $"자동 WebSocket 메시지 - {DateTime.Now:HH:mm:ss}", - characterId: testCharacterId, - userId: testUserId + message: $"자동 WebSocket 메시지 - {DateTime.Now:HH:mm:ss}" ); } @@ -864,11 +798,29 @@ await _webSocketManager.SendChatMessageAsync( private void OnWebSocketConnected() { Debug.Log("🎉 WebSocket 연결됨!"); + _reconnectAttempts = 0; // 연결 성공 시 재연결 시도 횟수 초기화 } private void OnWebSocketDisconnected() { Debug.Log("🔌 WebSocket 연결 해제됨!"); + + // 의도적인 연결 해제가 아닌 경우에만 자동 재연결 시도 + if (!_isIntentionalDisconnect && _reconnectAttempts < MAX_RECONNECT_ATTEMPTS) + { + _reconnectAttempts++; + Debug.Log($"재연결 시도 {_reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS}"); + ConnectWebSocket(); + } + else if (_isIntentionalDisconnect) + { + Debug.Log("의도적인 연결 해제로 재연결을 시도하지 않습니다."); + _isIntentionalDisconnect = false; // 플래그 초기화 + } + else + { + Debug.LogError("최대 재연결 시도 횟수 초과"); + } } private void OnWebSocketError(string error) @@ -876,57 +828,20 @@ private void OnWebSocketError(string error) Debug.LogError($"❌ WebSocket 오류: {error}"); } - private void OnChatMessageReceived(ProjectVG.Infrastructure.Network.DTOs.WebSocket.ChatMessage message) + private void OnSessionIdReceived(string sessionId) { - Debug.Log($"💬 WebSocket 채팅 메시지 수신: {message.message}"); - _chatResponseReceived = true; - _lastChatResponse = message.message; - } - - private void OnSystemMessageReceived(ProjectVG.Infrastructure.Network.DTOs.WebSocket.SystemMessage message) - { - Debug.Log($"🔧 WebSocket 시스템 메시지 수신: {message.description}"); - } - - private void OnSessionIdMessageReceived(ProjectVG.Infrastructure.Network.DTOs.WebSocket.SessionIdMessage message) - { - _receivedSessionId = message.session_id; + _receivedSessionId = sessionId; Debug.Log($"🆔 WebSocket 세션 ID 수신: {_receivedSessionId}"); Debug.Log($"✅ 세션 ID가 성공적으로 저장되었습니다!"); } - private void OnAudioDataReceived(byte[] audioData) - { - Debug.Log($"🎵 WebSocket 오디오 데이터 수신: {audioData.Length} bytes"); - _audioDataReceived = true; - } - - private void OnIntegratedMessageReceived(IntegratedMessage message) + private void OnChatMessageReceived(string message) { - Debug.Log($"🔄 WebSocket 통합 메시지 수신:"); - Debug.Log($" - 세션 ID: {message.sessionId}"); - Debug.Log($" - 텍스트: {message.text?.Length ?? 0}자"); - Debug.Log($" - 오디오: {message.audioData?.Length ?? 0}바이트"); - Debug.Log($" - 지속시간: {message.audioDuration:F2}초"); - - _integratedMessageReceived = true; - - // 텍스트가 있으면 채팅 메시지로도 처리 - if (!string.IsNullOrEmpty(message.text)) - { - _chatResponseReceived = true; - _lastChatResponse = message.text; - } - - // 오디오가 있으면 오디오 데이터로도 처리 - if (message.audioData != null && message.audioData.Length > 0) - { - _audioDataReceived = true; - } + Debug.Log($"💬 WebSocket 채팅 메시지 수신: {message}"); + _chatResponseReceived = true; + _lastChatResponse = message; } - - #endregion } } \ No newline at end of file diff --git a/Assets/Tests/Runtime/NetworkTestUI.cs b/Assets/Tests/Runtime/NetworkTestUI.cs index 90bae8d..eebc0b0 100644 --- a/Assets/Tests/Runtime/NetworkTestUI.cs +++ b/Assets/Tests/Runtime/NetworkTestUI.cs @@ -6,6 +6,7 @@ namespace ProjectVG.Tests.Runtime { /// /// 네트워크 테스트를 위한 UI 매니저 + /// 더미 클라이언트 방식 테스트를 지원합니다. /// public class NetworkTestUI : MonoBehaviour { @@ -16,6 +17,7 @@ public class NetworkTestUI : MonoBehaviour [SerializeField] private Button characterInfoButton; [SerializeField] private Button webSocketMessageButton; [SerializeField] private Button fullTestButton; + [SerializeField] private Button dummyClientTestButton; [SerializeField] private Button autoTestButton; [Header("Status Display")] @@ -24,11 +26,15 @@ public class NetworkTestUI : MonoBehaviour [SerializeField] private ScrollRect logScrollRect; [Header("Input Fields")] - [SerializeField] private TMP_InputField sessionIdInput; [SerializeField] private TMP_InputField characterIdInput; [SerializeField] private TMP_InputField userIdInput; [SerializeField] private TMP_InputField messageInput; + [Header("Test Settings")] + [SerializeField] private Toggle autoTestToggle; + [SerializeField] private Slider testIntervalSlider; + [SerializeField] private TextMeshProUGUI intervalText; + private NetworkTestManager _testManager; private bool _isAutoTestRunning = false; @@ -66,22 +72,38 @@ private void InitializeUI() if (fullTestButton != null) fullTestButton.onClick.AddListener(OnFullTestButtonClicked); + if (dummyClientTestButton != null) + dummyClientTestButton.onClick.AddListener(OnDummyClientTestButtonClicked); + if (autoTestButton != null) autoTestButton.onClick.AddListener(OnAutoTestButtonClicked); // 초기값 설정 - if (sessionIdInput != null) - sessionIdInput.text = "test-session-123"; - if (characterIdInput != null) - characterIdInput.text = "test-character-456"; + characterIdInput.text = "44444444-4444-4444-4444-444444444444"; // 제로 if (userIdInput != null) - userIdInput.text = "test-user-789"; + userIdInput.text = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; if (messageInput != null) messageInput.text = "안녕하세요! 테스트 메시지입니다."; + // 자동 테스트 설정 + if (autoTestToggle != null) + { + autoTestToggle.isOn = _testManager.AutoTest; + autoTestToggle.onValueChanged.AddListener(OnAutoTestToggleChanged); + } + + if (testIntervalSlider != null) + { + testIntervalSlider.minValue = 5f; + testIntervalSlider.maxValue = 30f; + testIntervalSlider.value = _testManager.TestInterval; + testIntervalSlider.onValueChanged.AddListener(OnTestIntervalChanged); + UpdateIntervalText(); + } + // 초기 버튼 상태 설정 UpdateButtonStates(false); } @@ -106,6 +128,9 @@ private void UpdateButtonStates(bool isConnected) if (fullTestButton != null) fullTestButton.interactable = !_isAutoTestRunning; + if (dummyClientTestButton != null) + dummyClientTestButton.interactable = !_isAutoTestRunning; + if (autoTestButton != null) { autoTestButton.interactable = !_isAutoTestRunning; @@ -137,11 +162,19 @@ private void AddLog(string message) } } + private void UpdateIntervalText() + { + if (intervalText != null && testIntervalSlider != null) + { + intervalText.text = $"테스트 간격: {testIntervalSlider.value:F1}초"; + } + } + #region Button Event Handlers private void OnConnectButtonClicked() { - AddLog("WebSocket 연결 시도..."); + AddLog("WebSocket 연결 시도 (더미 클라이언트 방식)..."); UpdateStatus("연결 중..."); _testManager.ConnectWebSocket(); } @@ -155,7 +188,7 @@ private void OnDisconnectButtonClicked() private void OnChatRequestButtonClicked() { - AddLog("HTTP 채팅 요청 전송..."); + AddLog("HTTP 채팅 요청 전송 (더미 클라이언트 방식)..."); UpdateStatus("채팅 요청 중..."); _testManager.SendChatRequest(); } @@ -182,12 +215,19 @@ private void OnFullTestButtonClicked() _testManager.RunFullTest(); } + private void OnDummyClientTestButtonClicked() + { + AddLog("더미 클라이언트 방식 전체 테스트 시작..."); + UpdateStatus("더미 클라이언트 테스트 실행 중..."); + _testManager.RunDummyClientTest(); + } + private void OnAutoTestButtonClicked() { if (!_isAutoTestRunning) { _isAutoTestRunning = true; - AddLog("자동 테스트 시작..."); + AddLog("자동 테스트 시작 (더미 클라이언트 방식)..."); UpdateStatus("자동 테스트 실행 중..."); UpdateButtonStates(true); @@ -207,6 +247,19 @@ private void OnAutoTestButtonClicked() } } + private void OnAutoTestToggleChanged(bool isOn) + { + _testManager.AutoTest = isOn; + AddLog($"자동 테스트 설정: {(isOn ? "활성화" : "비활성화")}"); + } + + private void OnTestIntervalChanged(float value) + { + _testManager.TestInterval = value; + UpdateIntervalText(); + AddLog($"테스트 간격 변경: {value:F1}초"); + } + #endregion #region Public Methods for External Updates @@ -236,11 +289,6 @@ public void OnChatMessageReceived(string message) AddLog($"💬 채팅 메시지 수신: {message}"); } - public void OnSystemMessageReceived(string message) - { - AddLog($"🔧 시스템 메시지 수신: {message}"); - } - public void OnSessionIdMessageReceived(string sessionId) { AddLog($"🆔 세션 ID 수신: {sessionId}"); @@ -256,6 +304,18 @@ public void OnHttpRequestFailed(string operation, string error) AddLog($"❌ HTTP {operation} 실패: {error}"); } + public void OnReconnectAttempt(int attempt, int maxAttempts) + { + AddLog($"🔄 재연결 시도 {attempt}/{maxAttempts}"); + UpdateStatus($"재연결 시도 중... ({attempt}/{maxAttempts})"); + } + + public void OnReconnectFailed() + { + AddLog("❌ 최대 재연결 시도 횟수 초과"); + UpdateStatus("재연결 실패"); + } + #endregion private void OnDestroy() @@ -279,8 +339,17 @@ private void OnDestroy() if (fullTestButton != null) fullTestButton.onClick.RemoveListener(OnFullTestButtonClicked); + if (dummyClientTestButton != null) + dummyClientTestButton.onClick.RemoveListener(OnDummyClientTestButtonClicked); + if (autoTestButton != null) autoTestButton.onClick.RemoveListener(OnAutoTestButtonClicked); + + if (autoTestToggle != null) + autoTestToggle.onValueChanged.RemoveListener(OnAutoTestToggleChanged); + + if (testIntervalSlider != null) + testIntervalSlider.onValueChanged.RemoveListener(OnTestIntervalChanged); } } } \ No newline at end of file