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/NetworkConfig.cs b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs
new file mode 100644
index 0000000..e69bdde
--- /dev/null
+++ b/Assets/Infrastructure/Network/Configs/NetworkConfig.cs
@@ -0,0 +1,404 @@
+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;
+ [SerializeField] private string wsMessageType = "json"; // "json" 또는 "binary"
+
+ [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;
+
+ ///
+ /// WebSocket 메시지 타입
+ ///
+ public static string WebSocketMessageType => Instance.wsMessageType;
+
+ ///
+ /// JSON 메시지 타입인지 확인
+ ///
+ public static bool IsJsonMessageType => Instance.wsMessageType?.ToLower() == "json";
+
+ ///
+ /// 바이너리 메시지 타입인지 확인
+ ///
+ public static bool IsBinaryMessageType => Instance.wsMessageType?.ToLower() == "binary";
+
+ ///
+ /// 사용자 에이전트
+ ///
+ 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/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..d5a07db
--- /dev/null
+++ b/Assets/Infrastructure/Network/Configs/ServerConfigLoader.cs
@@ -0,0 +1,111 @@
+using System;
+using UnityEngine;
+using UnityEngine.Networking;
+using Cysharp.Threading.Tasks;
+using ProjectVG.Infrastructure.Network.Configs;
+
+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.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/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..3db5e85
--- /dev/null
+++ b/Assets/Infrastructure/Network/DTOs/Character/CharacterInfo.cs
@@ -0,0 +1,18 @@
+using System;
+using UnityEngine;
+
+namespace ProjectVG.Infrastructure.Network.DTOs.Character
+{
+ ///
+ /// 캐릭터 정보 DTO
+ ///
+ [Serializable]
+ public class CharacterData
+ {
+ [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/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..247f357
--- /dev/null
+++ b/Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs
@@ -0,0 +1,37 @@
+using System;
+using UnityEngine;
+using Newtonsoft.Json;
+
+namespace ProjectVG.Infrastructure.Network.DTOs.Chat
+{
+ ///
+ /// 채팅 요청 DTO (Newtonsoft.Json을 사용하여 snake_case 지원)
+ ///
+ [Serializable]
+ public class ChatRequest
+ {
+ [JsonProperty("session_id")]
+ [SerializeField] public string sessionId;
+
+ [JsonProperty("message")]
+ [SerializeField] public string message;
+
+ [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/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/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/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..9983bfa
--- /dev/null
+++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs
@@ -0,0 +1,386 @@
+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;
+using Newtonsoft.Json;
+
+namespace ProjectVG.Infrastructure.Network.Http
+{
+ ///
+ /// HTTP API 클라이언트
+ /// UnityWebRequest를 사용하여 서버와 통신하며, UniTask 기반 비동기 처리를 지원합니다.
+ ///
+ public class HttpApiClient : MonoBehaviour
+ {
+ [Header("API Configuration")]
+ // NetworkConfig를 사용하여 설정을 관리합니다.
+
+ 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()
+ {
+ InitializeSingleton();
+ }
+
+ private void OnDestroy()
+ {
+ Cleanup();
+ }
+
+ #endregion
+
+ #region Public API
+
+
+
+ public void AddDefaultHeader(string key, string value)
+ {
+ defaultHeaders[key] = value;
+ }
+
+ public void SetAuthToken(string token)
+ {
+ AddDefaultHeader(AUTHORIZATION_HEADER, $"{BEARER_PREFIX}{token}");
+ }
+
+ 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 = 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 = SerializeData(data);
+ 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);
+ }
+
+ 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();
+ ApplyNetworkConfig();
+ SetupDefaultHeaders();
+ }
+
+ private void ApplyNetworkConfig()
+ {
+ Debug.Log($"NetworkConfig 적용: {NetworkConfig.CurrentEnvironment} 환경");
+ }
+
+ private void SetupDefaultHeaders()
+ {
+ defaultHeaders.Clear();
+ defaultHeaders["Content-Type"] = NetworkConfig.ContentType;
+ defaultHeaders["User-Agent"] = NetworkConfig.UserAgent;
+ defaultHeaders["Accept"] = ACCEPT_HEADER;
+ }
+
+ private string GetFullUrl(string endpoint)
+ {
+ return NetworkConfig.GetFullApiUrl(endpoint);
+ }
+
+ 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 = CreateCombinedCancellationToken(cancellationToken);
+
+ Debug.Log($"HTTP 요청 시작: {method} {url}");
+ if (!string.IsNullOrEmpty(jsonData))
+ {
+ Debug.Log($"HTTP 요청 데이터: {jsonData}");
+ }
+
+ for (int attempt = 0; attempt <= NetworkConfig.MaxRetryCount; attempt++)
+ {
+ try
+ {
+ using var request = CreateRequest(url, method, jsonData, headers);
+
+ Debug.Log($"HTTP 요청 전송 중... (시도 {attempt + 1}/{NetworkConfig.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
+ {
+ await HandleRequestFailure(request, attempt, combinedCancellationToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex) when (ex is not ApiException)
+ {
+ await HandleRequestException(ex, attempt, combinedCancellationToken);
+ }
+ }
+
+ 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 <= NetworkConfig.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)NetworkConfig.HttpTimeout;
+
+ var operation = request.SendWebRequest();
+ await operation.WithCancellation(combinedCancellationToken);
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ return ParseResponse(request);
+ }
+ else
+ {
+ await HandleFileUploadFailure(request, attempt, combinedCancellationToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex) when (ex is not ApiException)
+ {
+ await HandleFileUploadException(ex, attempt, combinedCancellationToken);
+ }
+ }
+
+ throw new ApiException($"{NetworkConfig.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 < NetworkConfig.MaxRetryCount)
+ {
+ Debug.LogWarning($"API 요청 실패 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {error.Message}");
+ await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken);
+ return;
+ }
+
+ throw error;
+ }
+
+ private async UniTask HandleRequestException(Exception ex, int attempt, CancellationToken cancellationToken)
+ {
+ if (attempt < NetworkConfig.MaxRetryCount)
+ {
+ 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($"{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 < NetworkConfig.MaxRetryCount)
+ {
+ Debug.LogWarning($"파일 업로드 실패 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {error.Message}");
+ await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken);
+ return;
+ }
+
+ throw error;
+ }
+
+ private async UniTask HandleFileUploadException(Exception ex, int attempt, CancellationToken cancellationToken)
+ {
+ if (attempt < NetworkConfig.MaxRetryCount)
+ {
+ Debug.LogWarning($"파일 업로드 예외 발생 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {ex.Message}");
+ await UniTask.Delay(TimeSpan.FromSeconds(NetworkConfig.RetryDelay * (attempt + 1)), cancellationToken: cancellationToken);
+ return;
+ }
+ throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, ex.Message);
+ }
+
+ 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)NetworkConfig.HttpTimeout;
+
+ 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 JsonConvert.DeserializeObject(responseText);
+ }
+ catch (Exception ex)
+ {
+ 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);
+ }
+ }
+
+ private bool ShouldRetry(long responseCode)
+ {
+ return responseCode >= 500 || responseCode == 429;
+ }
+
+ private void Cleanup()
+ {
+ cancellationTokenSource?.Cancel();
+ cancellationTokenSource?.Dispose();
+ }
+
+ #endregion
+ }
+
+ ///
+ /// 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/Assets/Infrastructure/Network/README.md b/Assets/Infrastructure/Network/README.md
new file mode 100644
index 0000000..b546cf2
--- /dev/null
+++ b/Assets/Infrastructure/Network/README.md
@@ -0,0 +1,269 @@
+# ProjectVG Network Module
+
+Unity 클라이언트와 서버 간의 통신을 위한 네트워크 모듈입니다.
+강제된 JSON 형식 `{type: "xxx", data: {...}}`을 사용합니다.
+
+## 📦 설치
+
+### 1. UniTask 설치
+```json
+// Packages/manifest.json
+"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask"
+```
+
+### 2. 플랫폼별 WebSocket 구현
+플랫폼에 따라 최적화된 WebSocket 구현을 사용합니다.
+
+**플랫폼별 구현:**
+- 🟩 Desktop: System.Net.WebSockets.ClientWebSocket
+- 🟩 WebGL: UnityWebRequest.WebSocket
+- 🟩 Mobile: 네이티브 WebSocket 라이브러리
+
+## 🏗️ 구조
+
+```
+Assets/Infrastructure/Network/
+├── Configs/ # 설정 파일들
+│ └── NetworkConfig.cs # Unity 표준 ScriptableObject 기반 설정
+├── DTOs/ # 데이터 전송 객체들
+│ ├── BaseApiResponse.cs # 기본 API 응답
+│ ├── Chat/ # 채팅 관련 DTO
+│ └── Character/ # 캐릭터 관련 DTO
+├── Http/ # HTTP 클라이언트
+│ └── HttpApiClient.cs # HTTP API 클라이언트
+├── Services/ # API 서비스들
+│ ├── ApiServiceManager.cs # API 서비스 매니저
+│ ├── ChatApiService.cs # 채팅 API 서비스
+│ └── CharacterApiService.cs # 캐릭터 API 서비스
+└── WebSocket/ # WebSocket 관련
+ ├── WebSocketManager.cs # WebSocket 매니저 (단순화됨)
+ ├── WebSocketFactory.cs # 플랫폼별 WebSocket 팩토리
+ ├── INativeWebSocket.cs # 플랫폼별 WebSocket 인터페이스
+ └── Platforms/ # 플랫폼별 WebSocket 구현
+ ├── DesktopWebSocket.cs # 데스크톱용 (.NET ClientWebSocket)
+ ├── WebGLWebSocket.cs # WebGL용 (UnityWebRequest)
+ └── MobileWebSocket.cs # 모바일용 (네이티브 라이브러리)
+```
+
+## 🚀 사용법
+
+### 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. WebSocket 사용 (단순화됨)
+
+#### 기본 사용법
+```csharp
+// WebSocket 매니저 사용
+var wsManager = WebSocketManager.Instance;
+
+// 이벤트 구독
+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}");
+
+// 연결
+await wsManager.ConnectAsync();
+
+// 메시지 전송
+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. HTTP API 사용
+
+#### API 서비스 매니저 사용
+```csharp
+// API 서비스 매니저 사용
+var apiManager = ApiServiceManager.Instance;
+
+// 채팅 API 사용
+var chatResponse = await apiManager.Chat.SendChatAsync(
+ new ChatRequest
+ {
+ message = "안녕하세요!",
+ characterId = "char-456",
+ userId = "user-789",
+ sessionId = "session-123"
+ }
+);
+
+// 캐릭터 API 사용
+var character = await apiManager.Character.GetCharacterAsync("char-456");
+```
+
+### 4. 전체 흐름 테스트 (권장)
+```csharp
+// NetworkTestManager 사용
+var testManager = FindObjectOfType();
+
+// 1. WebSocket 연결
+await testManager.ConnectWebSocket();
+
+// 2. HTTP 요청 전송
+await testManager.SendChatRequest();
+
+// 3. WebSocket으로 결과 수신 (자동)
+// 서버가 비동기 작업 완료 후 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 지원
+- 최고 성능
+- JSON 메시지만 처리
+
+### 2. WebGLWebSocket (브라우저)
+- UnityWebRequest.WebSocket 사용
+- WebGL 플랫폼 지원
+- 브라우저 제약사항 대응
+- JSON 메시지만 처리
+
+### 3. MobileWebSocket (모바일)
+- 네이티브 WebSocket 라이브러리 사용
+- iOS/Android 지원
+- 네이티브 성능
+- JSON 메시지만 처리
+
+### 4. WebSocketFactory
+- 플랫폼별 WebSocket 구현 생성
+- 컴파일 타임에 적절한 구현체 선택
+
+## 🐛 문제 해결
+
+### 플랫폼별 WebSocket
+```
+데스크톱 플랫폼용 WebSocket 생성
+WebSocket 연결 시도: ws://localhost:7900/ws
+WebSocket 연결 성공
+```
+**설명:** 플랫폼에 따라 적절한 WebSocket 구현체가 자동으로 선택됩니다.
+
+### 플랫폼별 특징
+- **Desktop**: .NET ClientWebSocket으로 최고 성능
+- **WebGL**: 브라우저 WebSocket API 사용
+- **Mobile**: 네이티브 라이브러리로 최적화
+
+### 테스트 실행 방법
+1. **NetworkTestManager** 컴포넌트를 씬에 추가
+2. **Context Menu**에서 테스트 실행:
+ - `1. WebSocket 연결`
+ - `2. HTTP 채팅 요청`
+ - `3. HTTP 캐릭터 정보 요청`
+ - `4. WebSocket 메시지 전송`
+ - `5. WebSocket 연결 해제`
+ - `전체 테스트 실행`
+
+## 📝 로그
+
+모든 로그는 한국어로 출력됩니다:
+- `Debug.Log("WebSocket 연결 성공")`
+- `Debug.LogError("연결 실패")`
+- `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/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/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..08f14da
--- /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 = FindFirstObjectByType();
+ 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..2e9205d
--- /dev/null
+++ b/Assets/Infrastructure/Network/Services/CharacterApiService.cs
@@ -0,0 +1,155 @@
+using System.Threading;
+using UnityEngine;
+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;
+ if (_httpClient == null)
+ {
+ Debug.LogError("HttpApiClient.Instance가 null입니다. HttpApiClient가 생성되지 않았습니다.");
+ }
+ }
+
+ ///
+ /// 모든 캐릭터 목록 조회
+ ///
+ /// 취소 토큰
+ /// 캐릭터 목록
+ public async UniTask GetAllCharactersAsync(CancellationToken cancellationToken = default)
+ {
+ if (_httpClient == null)
+ {
+ Debug.LogError("HttpApiClient가 null입니다. 초기화를 확인해주세요.");
+ return null;
+ }
+
+ return await _httpClient.GetAsync("character", cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// 특정 캐릭터 정보 조회
+ ///
+ /// 캐릭터 ID
+ /// 취소 토큰
+ /// 캐릭터 정보
+ public async UniTask GetCharacterAsync(string characterId, CancellationToken cancellationToken = default)
+ {
+ if (_httpClient == null)
+ {
+ Debug.LogError("HttpApiClient가 null입니다. 초기화를 확인해주세요.");
+ return null;
+ }
+
+ 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