From a52734304dcb61144d243c9545f733e54f100bb6 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 06:41:59 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`feature?= =?UTF-8?q?/auth`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @ImGdevel. * https://github.com/ProjectVG/ProjectVG-Server/pull/5#issuecomment-3226333873 The following files were modified: * `ProjectVG.Api/ApiServiceCollectionExtensions.cs` * `ProjectVG.Api/Controllers/AuthController.cs` * `ProjectVG.Api/Controllers/ChatController.cs` * `ProjectVG.Api/Controllers/OAuthController.cs` * `ProjectVG.Api/Filters/JwtAuthenticationFilter.cs` * `ProjectVG.Api/Middleware/GlobalExceptionHandler.cs` * `ProjectVG.Api/Middleware/WebSocketMiddleware.cs` * `ProjectVG.Api/Models/Auth/Request/RegisterRequest.cs` * `ProjectVG.Application/ApplicationServiceCollectionExtensions.cs` * `ProjectVG.Application/Models/Chat/ChatMessageSegment.cs` * `ProjectVG.Application/Models/Chat/ChatProcessContext.cs` * `ProjectVG.Application/Models/Chat/ProcessChatCommand.cs` * `ProjectVG.Application/Models/Chat/UserInputAnalysis.cs` * `ProjectVG.Application/Models/User/UserDto.cs` * `ProjectVG.Application/Services/Auth/AuthService.cs` * `ProjectVG.Application/Services/Auth/IAuthService.cs` * `ProjectVG.Application/Services/Auth/IOAuth2Provider.cs` * `ProjectVG.Application/Services/Auth/IOAuth2Service.cs` * `ProjectVG.Application/Services/Auth/OAuth2ProviderFactory.cs` * `ProjectVG.Application/Services/Auth/OAuth2Service.cs` * `ProjectVG.Application/Services/Auth/Providers/AppleOAuth2Provider.cs` * `ProjectVG.Application/Services/Auth/Providers/GoogleOAuth2Provider.cs` * `ProjectVG.Application/Services/Chat/ChatService.cs` * `ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs` * `ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs` * `ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecoratorFactory.cs` * `ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs` * `ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs` * `ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs` * `ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs` * `ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs` * `ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs` * `ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs` * `ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs` * `ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs` * `ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs` * `ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs` * `ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs` * `ProjectVG.Application/Services/Session/ConnectionRegistry.cs` * `ProjectVG.Application/Services/Session/IConnectionRegistry.cs` * `ProjectVG.Application/Services/User/IUserService.cs` * `ProjectVG.Application/Services/User/UserService.cs` * `ProjectVG.Application/Services/WebSocket/IWebSocketManager.cs` * `ProjectVG.Application/Services/WebSocket/WebSocketManager.cs` * `ProjectVG.Common/Constants/ErrorCodes.cs` * `ProjectVG.Common/Constants/LLMModelInfo.cs` * `ProjectVG.Common/Constants/TTSCostInfo.cs` * `ProjectVG.Common/Exceptions/AuthenticationException.cs` * `ProjectVG.Common/Exceptions/ValidationException.cs` * `ProjectVG.Common/Models/Session/IClientConnection.cs` * `ProjectVG.Common/Utils/UidGenerator.cs` * `ProjectVG.Infrastructure/Auth/IRefreshTokenStorage.cs` * `ProjectVG.Infrastructure/Auth/ITokenService.cs` * `ProjectVG.Infrastructure/Auth/InMemoryRefreshTokenStorage.cs` * `ProjectVG.Infrastructure/Auth/JwtProvider.cs` * `ProjectVG.Infrastructure/Auth/JwtService.cs` * `ProjectVG.Infrastructure/Auth/RedisRefreshTokenStorage.cs` * `ProjectVG.Infrastructure/Auth/TokenService.cs` * `ProjectVG.Infrastructure/InfrastructureServiceCollectionExtensions.cs` * `ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs` * `ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs` * `ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Data/ProjectVGDbContext.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.Designer.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.Designer.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.Designer.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.Designer.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.cs` * `ProjectVG.Infrastructure/Persistence/EfCore/Migrations/ProjectVGDbContextModelSnapshot.cs` * `ProjectVG.Infrastructure/Persistence/Repositories/Character/SqlServerCharacterRepository.cs` * `ProjectVG.Infrastructure/Persistence/Repositories/User/IUserRepository.cs` * `ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs` * `ProjectVG.Infrastructure/Persistence/Session/InMemorySessionStorage.cs` * `ProjectVG.Infrastructure/Realtime/WebSocketConnection/WebSocketClientConnection.cs` * `test-clients/start-oauth2-client.py` --- .../ApiServiceCollectionExtensions.cs | 11 ++ ProjectVG.Api/Controllers/AuthController.cs | 28 +++++ ProjectVG.Api/Controllers/ChatController.cs | 9 ++ ProjectVG.Api/Controllers/OAuthController.cs | 48 ++++++++ .../Filters/JwtAuthenticationFilter.cs | 17 +++ .../Middleware/GlobalExceptionHandler.cs | 29 +++++ .../Middleware/WebSocketMiddleware.cs | 33 ++++++ .../Models/Auth/Request/RegisterRequest.cs | 4 + .../ApplicationServiceCollectionExtensions.cs | 8 ++ .../Models/Chat/ChatMessageSegment.cs | 12 ++ .../Models/Chat/ChatProcessContext.cs | 32 ++++++ .../Models/Chat/ProcessChatCommand.cs | 22 +++- .../Models/Chat/UserInputAnalysis.cs | 17 +++ ProjectVG.Application/Models/User/UserDto.cs | 8 ++ .../Services/Auth/AuthService.cs | 33 ++++++ .../Services/Auth/IAuthService.cs | 19 +++- .../Services/Auth/IOAuth2Provider.cs | 17 ++- .../Services/Auth/IOAuth2Service.cs | 70 ++++++++++-- .../Services/Auth/OAuth2ProviderFactory.cs | 46 +++++++- .../Services/Auth/OAuth2Service.cs | 104 +++++++++++++++++- .../Auth/Providers/AppleOAuth2Provider.cs | 15 +++ .../Auth/Providers/GoogleOAuth2Provider.cs | 18 +++ .../Services/Chat/ChatService.cs | 29 +++++ .../Chat/CostTracking/ChatMetricsService.cs | 43 ++++++++ .../CostTracking/CostTrackingDecorator.cs | 32 ++++++ .../CostTrackingDecoratorFactory.cs | 11 ++ .../Chat/CostTracking/IChatMetricsService.cs | 49 ++++++++- .../CostTracking/ICostTrackingDecorator.cs | 17 ++- .../Services/Chat/Factories/ChatLLMFormat.cs | 62 +++++++++++ .../Services/Chat/Factories/ILLMFormat.cs | 16 ++- .../Factories/UserInputAnalysisLLMFormat.cs | 12 ++ .../Chat/Handlers/ChatFailureHandler.cs | 17 +++ .../MemoryContextPreprocessor.cs | 26 +++++ .../UserInputAnalysisProcessor.cs | 6 + .../Chat/Processors/ChatLLMProcessor.cs | 12 ++ .../Chat/Processors/ChatResultProcessor.cs | 20 ++++ .../Chat/Processors/ChatTTSProcessor.cs | 28 +++++ .../Chat/Validators/ChatRequestValidator.cs | 11 ++ .../Services/Session/ConnectionRegistry.cs | 30 +++++ .../Services/Session/IConnectionRegistry.cs | 23 +++- .../Services/User/IUserService.cs | 77 +++++++++++-- .../Services/User/UserService.cs | 84 +++++++++++++- .../Services/WebSocket/IWebSocketManager.cs | 25 ++++- .../Services/WebSocket/WebSocketManager.cs | 39 +++++++ ProjectVG.Common/Constants/ErrorCodes.cs | 5 + ProjectVG.Common/Constants/LLMModelInfo.cs | 35 ++++++ ProjectVG.Common/Constants/TTSCostInfo.cs | 14 +++ .../Exceptions/AuthenticationException.cs | 23 ++++ .../Exceptions/ValidationException.cs | 19 ++++ .../Models/Session/IClientConnection.cs | 14 ++- ProjectVG.Common/Utils/UidGenerator.cs | 6 + .../Auth/IRefreshTokenStorage.cs | 34 +++++- .../Auth/ITokenService.cs | 36 +++++- .../Auth/InMemoryRefreshTokenStorage.cs | 51 +++++++++ ProjectVG.Infrastructure/Auth/JwtProvider.cs | 63 +++++++++-- ProjectVG.Infrastructure/Auth/JwtService.cs | 9 ++ .../Auth/RedisRefreshTokenStorage.cs | 40 +++++++ ProjectVG.Infrastructure/Auth/TokenService.cs | 42 +++++++ ...frastructureServiceCollectionExtensions.cs | 42 +++++++ .../Integrations/LLMClient/LLMClient.cs | 12 ++ .../MemoryClient/IMemoryClient.cs | 39 ++++++- .../MemoryClient/VectorMemoryClient.cs | 66 +++++++++++ .../EfCore/Data/ProjectVGDbContext.cs | 19 ++++ .../20250825023623_AddUIDToUser.Designer.cs | 7 +- .../Migrations/20250825023623_AddUIDToUser.cs | 20 +++- ...250825023833_AddUIDToUserTable.Designer.cs | 11 +- .../20250825023833_AddUIDToUserTable.cs | 10 +- .../20250825023955_AddUIDFieldToUser.cs | 11 +- ...dateUserEntityWithUIDAndStatus.Designer.cs | 10 +- ...250825135004_IncreaseUIDLength.Designer.cs | 7 +- .../20250825135004_IncreaseUIDLength.cs | 10 +- .../ProjectVGDbContextModelSnapshot.cs | 9 +- .../Character/SqlServerCharacterRepository.cs | 6 + .../Repositories/User/IUserRepository.cs | 48 ++++++-- .../User/SqlServerUserRepository.cs | 45 ++++++++ .../Session/InMemorySessionStorage.cs | 23 ++++ .../WebSocketClientConnection.cs | 4 + test-clients/start-oauth2-client.py | 18 +++ 78 files changed, 1967 insertions(+), 110 deletions(-) diff --git a/ProjectVG.Api/ApiServiceCollectionExtensions.cs b/ProjectVG.Api/ApiServiceCollectionExtensions.cs index b92eec4..b468ee2 100644 --- a/ProjectVG.Api/ApiServiceCollectionExtensions.cs +++ b/ProjectVG.Api/ApiServiceCollectionExtensions.cs @@ -34,7 +34,10 @@ public static IServiceCollection AddApiServices(this IServiceCollection services /// /// 인증 및 인가 서비스 + /// + /// Negotiate(Windows) 인증 스킴을 추가하고 전역 대체 권한 정책(FallbackPolicy)을 제거하여 애플리케이션의 인증/인가를 구성합니다. /// + /// 구성된 IServiceCollection 인스턴스. public static IServiceCollection AddApiAuthentication(this IServiceCollection services) { services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) @@ -49,7 +52,10 @@ public static IServiceCollection AddApiAuthentication(this IServiceCollection se /// /// OAuth2 인증 서비스 (선택적) + /// + /// 쿠키 인증을 기본 인증 방식으로 설정하여 OAuth2 흐름을 별도 컨트롤러에서 처리할 수 있도록 구성합니다. /// + /// 구성된 IServiceCollection을 반환합니다. public static IServiceCollection AddOAuth2Authentication(this IServiceCollection services) { // OAuth2는 별도 컨트롤러에서 처리하므로 기본 인증만 설정 @@ -64,7 +70,12 @@ public static IServiceCollection AddOAuth2Authentication(this IServiceCollection /// /// 개발용 CORS 정책 + /// + /// 개발 환경에서 사용하도록 모든 출처, 모든 HTTP 메서드 및 모든 헤더를 허용하고 + /// "X-Access-Token", "X-Refresh-Token", "X-Expires-In", "X-UID" 응답 헤더를 노출하는 + /// "AllowAll" CORS 정책을 DI 컨테이너에 등록합니다. /// + /// 구성된 IServiceCollection을 반환합니다. public static IServiceCollection AddDevelopmentCors(this IServiceCollection services) { services.AddCors(options => { diff --git a/ProjectVG.Api/Controllers/AuthController.cs b/ProjectVG.Api/Controllers/AuthController.cs index 98ec04f..495bf84 100644 --- a/ProjectVG.Api/Controllers/AuthController.cs +++ b/ProjectVG.Api/Controllers/AuthController.cs @@ -9,11 +9,22 @@ public class AuthController : ControllerBase { private readonly IAuthService _authService; + /// + /// 컨트롤러에 인증 서비스 의존성을 주입하여 초기화합니다. + /// public AuthController(IAuthService authService) { _authService = authService; } + /// + /// 요청 헤더의 리프레시 토큰으로 액세스/리프레시 토큰을 갱신하고 사용자 정보를 반환합니다. + /// + /// + /// 요청 헤더 "X-Refresh-Token"에서 리프레시 토큰을 읽어 IAuthService.RefreshTokenAsync를 호출합니다. + /// 응답은 { success = true, tokens = ..., user = ... } 형태의 200 OK 입니다. + /// + /// 갱신된 토큰과 사용자 정보를 포함한 200 OK IActionResult. [HttpPost("refresh")] public async Task RefreshToken() { @@ -28,6 +39,13 @@ public async Task RefreshToken() }); } + /// + /// 요청 헤더의 "X-Refresh-Token"에서 리프레시 토큰을 읽어 해당 토큰의 로그아웃(무효화)을 수행하고 결과를 반환합니다. + /// + /// + /// HTTP 200 응답을 반환합니다. 본문은 익명 객체로 { success: bool, message: string } 형태이며, + /// success는 로그아웃 성공 여부, message는 "Logout successful" 또는 "Logout failed"입니다. + /// [HttpPost("logout")] public async Task Logout() { @@ -41,6 +59,12 @@ public async Task Logout() }); } + /// + /// 게스트 식별자(guestId)를 사용해 게스트 OAuth 로그인으로 사용자와 토큰을 발급하여 반환합니다. + /// + /// 클라이언트에서 전달된 게스트 고유 식별자(빈 값이면 유효하지 않음). + /// 성공 시 HTTP 200 응답으로 { success = true, tokens, user } 형태의 페이로드를 반환합니다. + /// guestId가 null 또는 빈 문자열인 경우 ErrorCode.GUEST_ID_INVALID와 함께 던져집니다. [HttpPost("guest-login")] public async Task GuestLogin([FromBody] string guestId) { @@ -59,6 +83,10 @@ public async Task GuestLogin([FromBody] string guestId) }); } + /// + /// HTTP 요청 헤더 "X-Refresh-Token"에서 리프레시 토큰을 읽어 반환합니다. + /// + /// 헤더에 지정된 첫 번째 토큰 값 또는 헤더가 없을 경우 빈 문자열. private string GetRefreshTokenFromHeader() { return Request.Headers["X-Refresh-Token"].FirstOrDefault() ?? string.Empty; diff --git a/ProjectVG.Api/Controllers/ChatController.cs b/ProjectVG.Api/Controllers/ChatController.cs index 0758e3f..bc5a7d7 100644 --- a/ProjectVG.Api/Controllers/ChatController.cs +++ b/ProjectVG.Api/Controllers/ChatController.cs @@ -13,11 +13,20 @@ public class ChatController : ControllerBase { private readonly IChatService _chatService; + /// + /// IChatService를 주입 받아 컨트롤러의 채팅 서비스 의존성을 설정합니다. + /// public ChatController(IChatService chatService) { _chatService = chatService; } + /// + /// 현재 인증된 사용자로부터 채팅 요청을 받아 채팅 처리를 큐에 등록하고 결과를 반환합니다. + /// + /// 클라이언트가 보낸 채팅 요청 객체 (Message, CharacterId 포함). + /// 큐에 등록된 작업의 결과를 포함한 HTTP 200 응답(IActionResult). + /// 사용자 식별자(ClaimTypes.NameIdentifier)가 없거나 GUID로 파싱할 수 없을 경우 발생하며, ErrorCode.AUTHENTICATION_FAILED를 포함합니다. [HttpPost] [JwtAuthentication] public async Task ProcessChat([FromBody] ChatRequest request) diff --git a/ProjectVG.Api/Controllers/OAuthController.cs b/ProjectVG.Api/Controllers/OAuthController.cs index b1e5d22..89c6046 100644 --- a/ProjectVG.Api/Controllers/OAuthController.cs +++ b/ProjectVG.Api/Controllers/OAuthController.cs @@ -12,6 +12,12 @@ public class OAuthController : ControllerBase private readonly IOAuth2Service _oauth2Service; private readonly IOAuth2ProviderFactory _providerFactory; + /// + /// OAuth2 관련 서비스와 공급자 팩토리를 사용하도록 컨트롤러를 초기화합니다. + /// + /// + /// 생성자에서 전달된 `IOptions<OAuth2ProviderSettings> oauth2Settings` 값은 현재 필드로 저장되거나 사용되지 않습니다. + /// public OAuthController( IOAuth2Service oauth2Service, IOAuth2ProviderFactory providerFactory, @@ -21,6 +27,10 @@ public OAuthController( _providerFactory = providerFactory; } + /// + /// 지원되는 OAuth2 제공자 목록을 조회하여 성공 여부와 함께 반환합니다. + /// + /// HTTP 200 응답으로 { success = true, providers = [...] } 형태의 JSON 결과를 포함한 IActionResult를 반환합니다. [HttpGet("oauth2/providers")] public IActionResult GetSupportedProviders() { @@ -32,6 +42,17 @@ public IActionResult GetSupportedProviders() }); } + /// + /// 지정된 OAuth2 공급자에 대한 PKCE 검증을 수행하고, 공급자별 인증 URL을 생성하여 반환합니다. + /// + /// 요청할 OAuth2 공급자 이름. 지원되지 않는 공급자면 ValidationException(ErrorCode.OAUTH2_PROVIDER_NOT_SUPPORTED)이 발생합니다. + /// 클라이언트에서 전달한 상태 토큰(예: CSRF/상태 검증용 식별자). + /// PKCE 코드 챌린지(필수). 비어 있으면 ValidationException(ErrorCode.OAUTH2_PKCE_INVALID)이 발생합니다. + /// PKCE 메서드(현재 "S256"만 허용). 다른 값이면 ValidationException(ErrorCode.OAUTH2_PKCE_INVALID)이 발생합니다. + /// 옵션인 PKCE 코드 베리파이어(후속 흐름에서 사용될 수 있음). + /// 클라이언트가 원하는 리디렉션 URI(옵션, 공급자별 처리). + /// HTTP 200 응답으로 { success = true, provider, auth_url } 형태의 JSON을 반환합니다. auth_url은 클라이언트가 리디렉션해야 할 공급자 인증 URL입니다. + /// 지원되지 않는 공급자 또는 PKCE 검증 실패 시 발생합니다. 사용되는 ErrorCode: OAUTH2_PROVIDER_NOT_SUPPORTED, OAUTH2_PKCE_INVALID. [HttpGet("oauth2/authorize/{provider}")] public async Task OAuth2Authorize( string provider, @@ -62,6 +83,19 @@ public async Task OAuth2Authorize( }); } + /// + /// OAuth2 콜백 엔드포인트를 처리하고, 처리 결과의 리디렉션 URL로 리다이렉트합니다. + /// + /// 선택적 공급자 식별자(경로 매개변수). 제공되지 않아도 됩니다. + /// OAuth2 공급자가 반환한 인증 코드(쿼리). 필수입니다. + /// 요청 시 전달된 상태 값(쿼리). 필수입니다. + /// 공급자가 반환한 오류 메시지(쿼리). 기본값은 null입니다. 존재하면 예외가 발생합니다. + /// 처리 결과의 RedirectUrl로 리다이렉트하는 를 반환합니다. + /// + /// error 파라미터가 비어있지 않거나(code/state가 누락된 경우) 검증 실패 시 각각 다음 오류 코드를 발생시킵니다: + /// - OAUTH2_CALLBACK_FAILED: callback에서 error가 전달된 경우 + /// - REQUIRED_PARAMETER_MISSING: code 또는 state가 누락된 경우 + /// [HttpGet("oauth2/callback")] [HttpGet("oauth2/callback/{provider}")] public async Task OAuth2Callback( @@ -85,6 +119,20 @@ public async Task OAuth2Callback( return Redirect(result.RedirectUrl!); } + /// + /// 주어진 상태(state)에 대응하는 OAuth2 토큰 데이터를 조회하여 응답 헤더로 반환하고 해당 토큰 데이터를 삭제합니다. + /// + /// + /// - 요청 쿼리의 state가 필수입니다. + /// - 조회한 토큰 데이터는 반환 직후 삭제됩니다. + /// - 반환 시 다음 헤더를 추가합니다: X-Access-Token, X-Refresh-Token, X-Expires-In, X-UID. + /// + /// 토큰 조회를 식별하는 상태 식별자(쿼리 파라미터). 빈값이면 예외가 발생합니다. + /// 요청이 성공하면 HTTP 200과 { success = true }를 반환합니다. + /// 다음 상황에서 발생합니다: + /// - : state가 비어있을 때. + /// - : 해당 state에 대한 토큰 데이터가 없을 때. + /// [HttpGet("oauth2/token")] public async Task GetOAuth2Token([FromQuery] string state) { diff --git a/ProjectVG.Api/Filters/JwtAuthenticationFilter.cs b/ProjectVG.Api/Filters/JwtAuthenticationFilter.cs index 9877304..54a758f 100644 --- a/ProjectVG.Api/Filters/JwtAuthenticationFilter.cs +++ b/ProjectVG.Api/Filters/JwtAuthenticationFilter.cs @@ -7,6 +7,18 @@ namespace ProjectVG.Api.Filters [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class JwtAuthenticationAttribute : Attribute, IAsyncAuthorizationFilter { + /// + /// 요청에서 Bearer JWT를 추출·검증하고 성공 시 HttpContext.User에 ClaimsPrincipal을 설정하여 인증을 적용합니다. + /// + /// 현재 요청의 컨텍스트(요청 헤더에서 토큰을 읽고, HttpContext.User를 설정하는 데 사용). + /// 비동기 작업을 나타내는 Task. + /// 다음 조건 중 하나일 때 발생: + /// + /// ErrorCode.TOKEN_MISSING: 토큰이 헤더에 없거나 비어 있을 때. + /// ErrorCode.TOKEN_INVALID: 토큰이 유효하지 않을 때. + /// ErrorCode.AUTHENTICATION_FAILED: 토큰에서 사용자 ID를 얻을 수 없을 때. + /// + /// public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { var logger = context.HttpContext.RequestServices.GetRequiredService>(); @@ -36,6 +48,11 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) logger.LogInformation("JWT 인증 성공 - 사용자: {UserId}", userId.Value); } + /// + /// 요청 헤더들에서 Bearer 토큰을 찾아 토큰 문자열을 반환합니다. + /// + /// 토큰을 추출할 HTTP 요청. + /// 헤더에서 찾은 토큰 문자열(접두사 "Bearer " 제거) 또는 찾지 못하면 null. private string? ExtractToken(HttpRequest request) { var possibleHeaders = new[] diff --git a/ProjectVG.Api/Middleware/GlobalExceptionHandler.cs b/ProjectVG.Api/Middleware/GlobalExceptionHandler.cs index 60d381f..3c3d5b4 100644 --- a/ProjectVG.Api/Middleware/GlobalExceptionHandler.cs +++ b/ProjectVG.Api/Middleware/GlobalExceptionHandler.cs @@ -46,6 +46,14 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception await context.Response.WriteAsync(jsonResponse); } + /// + /// 전달된 예외를 적절한 전용 핸들러로 매핑하여 표준화된 ErrorResponse를 생성합니다. + /// + /// + /// ValidationException, NotFoundException, AuthenticationException, ProjectVGException, ExternalServiceException, + /// DbUpdateException 등 알려진 예외는 각 전용 처리 메서드로 위임되고, 해당하지 않는 예외는 HandleGenericException에서 처리됩니다. + /// 반환되는 ErrorResponse는 클라이언트에 직렬화되어 HTTP 응답 페이로드로 사용됩니다. + /// private ErrorResponse CreateErrorResponse(Exception exception, HttpContext context) { if (exception is ValidationException validationEx) { @@ -115,6 +123,15 @@ private ErrorResponse HandleValidationException(ValidationException exception, H }; } + /// + /// NotFoundException을 ErrorResponse로 변환하여 반환합니다. + /// + /// 발생한 NotFoundException(내부에 ErrorCode, Message, StatusCode 포함). + /// 응답에 포함할 TraceIdentifier를 가져오기 위한 HttpContext. + /// + /// 요청된 리소스를 찾을 수 없음을 나타내는 ErrorResponse: + /// ErrorCode, Message, StatusCode, UTC 타임스탬프, TraceId를 설정하여 반환합니다. + /// private ErrorResponse HandleNotFoundException(NotFoundException exception, HttpContext context) { _logger.LogWarning(exception, "리소스를 찾을 수 없음: {ErrorCode} - {Message}", exception.ErrorCode.ToString(), exception.Message); @@ -128,6 +145,12 @@ private ErrorResponse HandleNotFoundException(NotFoundException exception, HttpC }; } + /// + /// 인증 관련 예외(AuthenticationException)를 표준화된 ErrorResponse로 변환하고 경고를 기록합니다. + /// + /// 처리할 AuthenticationException 인스턴스. + /// 현재 HTTP 요청의 HttpContext; 응답에 포함할 TraceIdentifier를 제공합니다. + /// 예외 정보를 기반으로 생성된 ErrorResponse(에러 코드, 메시지, 상태 코드, 타임스탬프, TraceId 포함). private ErrorResponse HandleAuthenticationException(AuthenticationException exception, HttpContext context) { _logger.LogWarning(exception, "인증 실패: {ErrorCode} - {Message}", exception.ErrorCode.ToString(), exception.Message); @@ -141,6 +164,12 @@ private ErrorResponse HandleAuthenticationException(AuthenticationException exce }; } + /// + /// ProjectVG 전용 예외를 표준 ErrorResponse로 변환하여 반환합니다. + /// + /// ErrorCode, Message, StatusCode 등을 포함한 ProjectVG 예외; 응답의 주요 필드 값으로 사용됩니다. + /// 응답에 포함할 TraceId를 가져오기 위해 사용되는 HttpContext. + /// 예외 정보를 매핑한 ErrorResponse(UTC 타임스탬프와 TraceId 포함). private ErrorResponse HandleProjectVGException(ProjectVGException exception, HttpContext context) { _logger.LogWarning(exception, "ProjectVG 예외 발생: {ErrorCode} - {Message}", exception.ErrorCode.ToString(), exception.Message); diff --git a/ProjectVG.Api/Middleware/WebSocketMiddleware.cs b/ProjectVG.Api/Middleware/WebSocketMiddleware.cs index a21f0f0..0e6a040 100644 --- a/ProjectVG.Api/Middleware/WebSocketMiddleware.cs +++ b/ProjectVG.Api/Middleware/WebSocketMiddleware.cs @@ -14,6 +14,12 @@ public class WebSocketMiddleware private readonly IConnectionRegistry _connectionRegistry; private readonly IJwtProvider _jwtProvider; + /// + /// WebSocket 미들웨어의 새 인스턴스를 초기화합니다. + /// + /// + /// 요청 파이프라인 델리게이트, 로거, 웹소켓 서비스, 연결 레지스트리 및 JWT 제공자를 주입받아 내부 필드에 저장합니다. + /// public WebSocketMiddleware( RequestDelegate next, ILogger logger, @@ -28,6 +34,11 @@ public WebSocketMiddleware( _jwtProvider = jwtProvider; } + /// + /// "/ws" 경로로 들어온 WebSocket 업그레이드 요청을 처리하고 인증된 사용자의 연결을 등록한 뒤 세션 루프를 실행합니다. + /// + /// 현재 HTTP 요청/응답 컨텍스트. WebSocket 업그레이드 요청이어야 하며 토큰은 쿼리 문자열 또는 Authorization 헤더의 Bearer 토큰에서 추출됩니다. + /// 미들웨어 처리가 완료될 때까지 완료되지 않는 비동기 작업. (연결 수명 주기 동안 실행됩니다.) public async Task InvokeAsync(HttpContext context) { if (context.Request.Path != "/ws") { @@ -54,7 +65,13 @@ public async Task InvokeAsync(HttpContext context) /// /// JWT 토큰 검증 및 사용자 ID 추출 + /// + /// 요청의 JWT를 검증하고 토큰에서 사용자 ID를 추출하여 Guid로 반환합니다. /// + /// 토큰을 포함할 수 있는 요청 컨텍스트(HttpContext). + /// + /// 토큰이 존재하고 유효하면 해당 사용자 ID(Guid). 그렇지 않으면 null을 반환합니다. + /// private Guid? ValidateAndExtractUserId(HttpContext context) { var token = ExtractToken(context); @@ -75,7 +92,15 @@ public async Task InvokeAsync(HttpContext context) /// /// QueryString 또는 Authorization 헤더에서 토큰 추출 + /// + /// HttpContext에서 JWT 토큰을 추출합니다. /// + /// + /// 먼저 쿼리 문자열의 "token" 매개변수를 확인하고, 존재하지 않으면 Authorization 헤더의 "Bearer {token}" 부분을 사용합니다. + /// 토큰이 없으면 빈 문자열을 반환합니다. + /// + /// 요청 정보가 포함된 HttpContext. + /// 발견된 토큰 문자열 또는 토큰이 없으면 빈 문자열. private string ExtractToken(HttpContext context) { var token = context.Request.Query["token"].FirstOrDefault(); @@ -90,7 +115,11 @@ private string ExtractToken(HttpContext context) /// /// 기존 연결 정리 후 새 연결 등록 + /// + /// 주어진 사용자 ID로 웹소켓 연결을 등록한다. 동일한 사용자에 대한 기존 연결이 있으면 먼저 비동기으로 끊고 새 연결을 등록한 뒤 서버 측 연결을 생성한다. /// + /// 등록할 사용자의 GUID 식별자. + /// 등록할 클라이언트의 열린 WebSocket 인스턴스. private async Task RegisterConnection(Guid userId, WebSocket socket) { if (_connectionRegistry.TryGet(userId.ToString(), out var existing) && existing != null) { @@ -105,7 +134,11 @@ private async Task RegisterConnection(Guid userId, WebSocket socket) /// /// 세션 루프 실행 + /// + /// 인증된 사용자에 대한 WebSocket 세션을 비동기로 유지하고, 클라이언트의 종료 요청을 감지해 세션을 정리합니다. /// + /// 활성화된 WebSocket 연결. + /// JWT에서 추출된 사용자 ID(문자열 형태의 GUID). 세션이 종료되면 이 ID로 연결이 해제됩니다. private async Task RunSessionLoop(WebSocket socket, string userId) { var buffer = new byte[1024]; diff --git a/ProjectVG.Api/Models/Auth/Request/RegisterRequest.cs b/ProjectVG.Api/Models/Auth/Request/RegisterRequest.cs index 1b00636..0e0d0c0 100644 --- a/ProjectVG.Api/Models/Auth/Request/RegisterRequest.cs +++ b/ProjectVG.Api/Models/Auth/Request/RegisterRequest.cs @@ -18,6 +18,10 @@ public class RegisterRequest [JsonPropertyName("password")] public string Password { get; set; } = string.Empty; + /// + /// 현재 요청 데이터를 기반으로 새 사용자 전송 객체(UserDto)를 생성합니다. + /// + /// Username, Email을 복사하고 Provider는 "local", ProviderId는 Username으로 설정하며 Status는 AccountStatus.Active로 설정된 UserDto 인스턴스. public UserDto ToUserDto() { return new UserDto diff --git a/ProjectVG.Application/ApplicationServiceCollectionExtensions.cs b/ProjectVG.Application/ApplicationServiceCollectionExtensions.cs index 29dd47a..502c333 100644 --- a/ProjectVG.Application/ApplicationServiceCollectionExtensions.cs +++ b/ProjectVG.Application/ApplicationServiceCollectionExtensions.cs @@ -16,6 +16,14 @@ namespace ProjectVG.Application { public static class ApplicationServiceCollectionExtensions { + /// + /// 애플리케이션의 핵심 서비스들을 의존성 주입 컨테이너에 등록합니다. + /// + /// + /// 등록 항목: 인증(Auth), 사용자 및 캐릭터 서비스, 채팅(코어/검증/전처리/프로세서/핸들러/비용 추적 데코레이터), 대화 및 세션(연결 레지스트리), WebSocket 관리 등. + /// 각 서비스는 코드에서 지정한 수명(scope/singleton)에 따라 등록됩니다. + /// + /// 구성된 객체를 반환합니다. public static IServiceCollection AddApplicationServices(this IServiceCollection services) { // Auth Services diff --git a/ProjectVG.Application/Models/Chat/ChatMessageSegment.cs b/ProjectVG.Application/Models/Chat/ChatMessageSegment.cs index d520e64..d5122bb 100644 --- a/ProjectVG.Application/Models/Chat/ChatMessageSegment.cs +++ b/ProjectVG.Application/Models/Chat/ChatMessageSegment.cs @@ -14,6 +14,12 @@ public class ChatMessageSegment public bool IsEmpty => !HasText && !HasAudio; + /// + /// 텍스트만 포함하는 새 ChatMessageSegment 인스턴스를 생성합니다. + /// + /// 세그먼트의 텍스트 내용(빈 문자열 허용). + /// 세그먼트의 순서(기본값 0). + /// Text가 설정되고 오디오 관련 필드는 null 또는 기본값인 ChatMessageSegment. public static ChatMessageSegment CreateTextOnly(string text, int order = 0) { return new ChatMessageSegment @@ -23,6 +29,12 @@ public static ChatMessageSegment CreateTextOnly(string text, int order = 0) }; } + /// + /// 이 인스턴스의 오디오 관련 속성(AudioData, AudioContentType, AudioLength)을 설정하거나 null을 전달해 해당 값을 제거합니다. + /// + /// 원시 오디오 바이트 배열. null이면 기존 오디오 데이터를 제거합니다. + /// 오디오의 MIME 타입(예: "audio/mpeg"). null이면 기존 값을 제거합니다. + /// 오디오 길이(초 단위). null이면 기존 값을 제거합니다. public void SetAudioData(byte[]? audioData, string? audioContentType, float? audioLength) { AudioData = audioData; diff --git a/ProjectVG.Application/Models/Chat/ChatProcessContext.cs b/ProjectVG.Application/Models/Chat/ChatProcessContext.cs index d0900ec..54b60dd 100644 --- a/ProjectVG.Application/Models/Chat/ChatProcessContext.cs +++ b/ProjectVG.Application/Models/Chat/ChatProcessContext.cs @@ -25,6 +25,10 @@ public class ChatProcessContext public bool HasText => Segments.Any(s => s.HasText); + /// + /// 주어진 ProcessChatCommand에서 세션, 사용자, 캐릭터 식별자와 메시지, 메모리 키 및 TTS 사용 여부를 초기화하여 ChatProcessContext 인스턴스를 생성합니다. + /// + /// 초기화에 사용할 입력 명령(세션Id, UserId, CharacterId, Message, UseTTS 포함). public ChatProcessContext(ProcessChatCommand command) { SessionId = command.SessionId; @@ -35,6 +39,17 @@ public ChatProcessContext(ProcessChatCommand command) UseTTS = command.UseTTS; } + /// + /// 주어진 입력으로 ChatProcessContext 인스턴스를 초기화합니다. + /// + /// 세션, 사용자, 캐릭터 식별자와 메시지 및 UseTTS 설정을 포함한 처리 명령. + /// 대화에 사용할 캐릭터 데이터(없을 수 있음). + /// 대화 기록 항목 시퀀스(없을 수 있음). + /// 메모리 컨텍스트 항목 시퀀스(없을 수 있음). + /// + /// 생성자는 Command에서 SessionId, UserId, CharacterId, Message, UseTTS 값을 복사하고 MemoryStore는 UserId의 문자열 표현으로 설정합니다. + /// 제공된 캐릭터, 대화 기록, 메모리 컨텍스트는 각각 대응하는 프로퍼티에 할당됩니다. + /// public ChatProcessContext( ProcessChatCommand command, CharacterDto character, @@ -53,6 +68,12 @@ public ChatProcessContext( MemoryContext = memoryContext; } + /// + /// 처리된 응답 텍스트와 응답을 구성하는 세그먼트 목록 및 해당 비용을 현재 컨텍스트에 설정합니다. + /// + /// 생성된 전체 응답 텍스트(읽기/표시용). + /// 응답을 구성하는 텍스트/오디오 세그먼트의 리스트. + /// 이 응답에 할당된 총 비용. public void SetResponse(string response, List segments, double cost) { Response = response; @@ -60,11 +81,22 @@ public void SetResponse(string response, List segments, doub Cost = cost; } + /// + /// 누적 비용에 지정한 값을 더합니다. + /// + /// 더할 비용(음수일 경우 비용을 감소시킵니다). public void AddCost(double additionalCost) { Cost += additionalCost; } + /// + /// 대화 기록에서 최근 항목을 지정된 개수만큼 추출해 "Role: Content" 형식의 문자열 시퀀스로 반환합니다. + /// + /// 반환할 최대 항목 수(기본값: 5). ConversationHistory의 항목 수보다 크면 가능한 만큼 반환합니다. + /// + /// 각 항목을 "Role: Content"로 포맷한 문자열의 열거. ConversationHistory가 null이면 빈 열거를 반환합니다. + /// public IEnumerable ParseConversationHistory(int count = 5) { if (ConversationHistory == null) return Enumerable.Empty(); diff --git a/ProjectVG.Application/Models/Chat/ProcessChatCommand.cs b/ProjectVG.Application/Models/Chat/ProcessChatCommand.cs index e03e34a..6c5748c 100644 --- a/ProjectVG.Application/Models/Chat/ProcessChatCommand.cs +++ b/ProjectVG.Application/Models/Chat/ProcessChatCommand.cs @@ -17,6 +17,10 @@ public class ProcessChatCommand public CharacterDto? Character { get; private set; } + /// + /// 명령에 대한 캐릭터 데이터를 연결합니다. + /// + /// 이 ProcessChatCommand에 설정할 CharacterDto 객체. internal void SetCharacter(CharacterDto character) { Character = character; @@ -24,14 +28,28 @@ internal void SetCharacter(CharacterDto character) public bool IsCharacterLoaded => Character != null; - // 기본 생성자 + /// + /// 빈 요청으로 ProcessChatCommand 인스턴스를 생성합니다. + /// + /// + /// 생성 시 RequestId에 새 GUID를 할당하고 RequestedAt을 UTC 현재 시각으로 설정합니다. + /// public ProcessChatCommand() { RequestId = Guid.NewGuid(); RequestedAt = DateTime.UtcNow; } - // 주요 값들을 받는 생성자 + /// + /// 새 채팅 처리 요청을 생성합니다. + /// + /// 요청을 보낸 사용자 식별자. + /// 명령에 연관된 캐릭터의 식별자. + /// 사용자 메시지 내용. + /// 선택적 세션 식별자(기본값: 빈 문자열). + /// + /// 생성 시 RequestId는 새 GUID로 설정되며 RequestedAt은 UTC 현재 시각으로 초기화됩니다. + /// public ProcessChatCommand(Guid userId, Guid characterId, string message, string sessionId = "") { RequestId = Guid.NewGuid(); diff --git a/ProjectVG.Application/Models/Chat/UserInputAnalysis.cs b/ProjectVG.Application/Models/Chat/UserInputAnalysis.cs index f48a095..0cf659e 100644 --- a/ProjectVG.Application/Models/Chat/UserInputAnalysis.cs +++ b/ProjectVG.Application/Models/Chat/UserInputAnalysis.cs @@ -13,10 +13,27 @@ public class UserInputAnalysis public string ContainsTemporalExpression { get; set; } = string.Empty; public List Emotions { get; set; } = new List(); + /// + /// 외부에서 직접 인스턴스화하는 것을 막기 위한 비공개 기본 생성자입니다. + /// + /// + /// 인스턴스 생성은 CreateValid, CreateIgnore, CreateReject 같은 정적 팩토리 메서드를 통해 이루어져야 합니다. + /// private UserInputAnalysis() { } + /// + /// 사용자 입력 분석 결과의 유효한 인스턴스를 생성합니다. + /// + /// 분석된 대화 문맥(예: 이전 대화 요약). + /// 식별된 사용자 의도(예: 요청의 목적). + /// 분석에 따라 수행할 동작(예: 채팅, 무시, 거부 등). + /// 추출된 키워드 목록. + /// 선택적 향상된 질의 문자열(있을 경우). + /// 선택적 문맥 관련 타임스탬프. + /// 분석과 관련된 비용 값(기본값 0). + /// 주어진 값들로 초기화된 유효한 인스턴스. public static UserInputAnalysis CreateValid( string conversationContext, string userIntent, diff --git a/ProjectVG.Application/Models/User/UserDto.cs b/ProjectVG.Application/Models/User/UserDto.cs index add9df3..651835e 100644 --- a/ProjectVG.Application/Models/User/UserDto.cs +++ b/ProjectVG.Application/Models/User/UserDto.cs @@ -18,6 +18,10 @@ public UserDto() { } + /// + /// 도메인 User 엔티티의 값을 사용해 UserDto를 초기화합니다. + /// + /// 초기화에 사용할 도메인 User 엔티티(널이 아님). Id, UID, Username, Email, ProviderId, Provider, Status 값을 DTO에 복사합니다. CreatedAt/UpdatedAt는 복사하지 않습니다. public UserDto(Domain.Entities.Users.User user) { Id = user.Id; @@ -29,6 +33,10 @@ public UserDto(Domain.Entities.Users.User user) Status = user.Status; } + /// + /// 현재 DTO를 기반으로 새로운 도메인 User 엔티티 인스턴스를 생성하여 반환합니다. + /// + /// Id, UID, Username, Email, ProviderId, Provider, Status 필드가 복사된 새 인스턴스. CreatedAt 및 UpdatedAt 필드는 설정되지 않습니다. public Domain.Entities.Users.User ToEntity() { return new Domain.Entities.Users.User { diff --git a/ProjectVG.Application/Services/Auth/AuthService.cs b/ProjectVG.Application/Services/Auth/AuthService.cs index 675b097..8760aea 100644 --- a/ProjectVG.Application/Services/Auth/AuthService.cs +++ b/ProjectVG.Application/Services/Auth/AuthService.cs @@ -14,6 +14,9 @@ public class AuthService : IAuthService private readonly ITokenService _tokenService; private readonly ILogger _logger; + /// + /// AuthService를 생성합니다. 필요한 서비스(IUserService, ITokenService)와 로거를 주입받아 내부 필드에 설정합니다. + /// public AuthService(IUserService userService, ITokenService tokenService, ILogger logger) { _userService = userService; @@ -21,6 +24,19 @@ public AuthService(IUserService userService, ITokenService tokenService, ILogger _logger = logger; } + /// + /// 지정된 OAuth 프로바이더로 사용자 인증(또는 게스트 흐름)을 수행하고 토큰 및 사용자 정보를 반환합니다. + /// + /// 인증 프로바이더 식별자. 지원 값: "guest", "google", "apple". + /// 프로바이더에서 제공한 사용자 식별자(또는 게스트 ID). + /// 생성되거나 조회된 사용자 정보와 액세스/리프레시 토큰을 포함한 AuthResult. + /// 다음 경우에 발생합니다: + /// + /// provider가 "guest"이고 providerUserId가 비어있을 경우: ErrorCode.GUEST_ID_INVALID + /// provider가 "google" 또는 "apple"이고 providerUserId가 비어있을 경우: ErrorCode.PROVIDER_USER_ID_INVALID + /// 지원되지 않는 프로바이더가 지정된 경우: ErrorCode.OAUTH2_PROVIDER_NOT_SUPPORTED + /// + /// public async Task LoginWithOAuthAsync(string provider, string providerUserId) { // OAuth 프로바이더별 사용자 처리 @@ -100,6 +116,17 @@ public async Task LoginWithOAuthAsync(string provider, string provid }; } + /// + /// 리프레시 토큰으로 액세스 토큰을 재발급하고 새 토큰과 연관된 사용자 정보를 반환합니다. + /// + /// 재발급에 사용할 리프레시 토큰(빈 값일 수 없음). + /// 재발급된 토큰(`Tokens`)과 해당 토큰에 연결된 사용자(`User`)를 포함하는 . + /// + /// + /// 토큰이 null 또는 빈 문자열인 경우: . + /// 리프레시 토큰으로부터 새로운 액세스 토큰을 얻지 못한 경우(유효하지 않거나 만료된 토큰): . + /// + /// public async Task RefreshTokenAsync(string refreshToken) { if (string.IsNullOrEmpty(refreshToken)) @@ -123,6 +150,12 @@ public async Task RefreshTokenAsync(string refreshToken) }; } + /// + /// 주어진 리프레시 토큰을 폐기하여 사용자 로그아웃을 처리합니다. + /// + /// 폐기할 리프레시 토큰(빈 문자열 또는 null일 수 없습니다). + /// 토큰 폐기가 성공하면 true, 그렇지 않으면 false를 반환합니다. + /// refreshToken이 null 또는 빈 문자열인 경우(ErrorCode.TOKEN_MISSING). public async Task LogoutAsync(string refreshToken) { if (string.IsNullOrEmpty(refreshToken)) diff --git a/ProjectVG.Application/Services/Auth/IAuthService.cs b/ProjectVG.Application/Services/Auth/IAuthService.cs index 2c9554a..c35fb40 100644 --- a/ProjectVG.Application/Services/Auth/IAuthService.cs +++ b/ProjectVG.Application/Services/Auth/IAuthService.cs @@ -14,21 +14,34 @@ public interface IAuthService /// /// 인증 제공자 (google, github, microsoft, guest, test) /// 제공자별 사용자 ID - /// 로그인 결과 (토큰, 사용자 정보 포함) + /// +/// 지정된 OAuth 공급자 정보로 비동기 로그인 수행하여 토큰과 사용자 정보를 반환합니다. +/// +/// OAuth 공급자 식별자(예: "google", "github", "microsoft", "guest", "test"). +/// 해당 공급자에서 발급한 사용자 고유 ID(공급자별 식별자). +/// 액세스/리프레시 토큰 및 로그인된 사용자 정보를 포함한 AuthResult. Task LoginWithOAuthAsync(string provider, string providerUserId); /// /// 리프레시 토큰을 사용하여 새로운 액세스 토큰 발급 /// /// 유효한 리프레시 토큰 - /// 새로운 토큰 쌍과 사용자 정보 + /// +/// 주어진 리프레시 토큰으로 새 액세스/리프레시 토큰 쌍을 발급하고 관련 사용자 정보를 반환합니다. +/// +/// 토큰 갱신에 사용할 리프레시 토큰(널일 수 있음). +/// 발급된 토큰 정보(TokenResponse)와 사용자 정보(UserDto)를 포함한 AuthResult. Task RefreshTokenAsync(string? refreshToken); /// /// 사용자 로그아웃 처리 (리프레시 토큰 무효화) /// /// 무효화할 리프레시 토큰 - /// 로그아웃 성공 여부 + /// +/// 제공된 리프레시 토큰을 무효화하여 로그아웃을 수행합니다. +/// +/// 무효화할 리프레시 토큰(없을 수 있음). +/// 로그아웃(토큰 무효화) 성공 시 true, 실패 시 false. Task LogoutAsync(string? refreshToken); } diff --git a/ProjectVG.Application/Services/Auth/IOAuth2Provider.cs b/ProjectVG.Application/Services/Auth/IOAuth2Provider.cs index 318daf6..01b914b 100644 --- a/ProjectVG.Application/Services/Auth/IOAuth2Provider.cs +++ b/ProjectVG.Application/Services/Auth/IOAuth2Provider.cs @@ -22,7 +22,16 @@ public interface IOAuth2Provider /// PKCE code challenge /// PKCE challenge 방법 /// 요청할 스코프 - /// 인증 URL + /// +/// OAuth2 인증을 시작하기 위한 인증(authorization) URL을 생성하여 반환합니다. +/// +/// OAuth2 클라이언트 식별자. +/// 인증 완료 후 리다이렉트할 콜백 URI. +/// CSRF 방지 및 상태 유지에 사용되는 임의 문자열. +/// PKCE 흐름에서 전송할 code challenge 값(없으면 null 또는 빈 문자열). +/// PKCE의 해시 방법(e.g. "S256"). codeChallenge가 비어있으면 무시될 수 있음. +/// 요청할 권한 범위 목록(빈 배열이면 기본 스코프 사용 가능). +/// 사용자를 인증 서버로 안내할 전체 인증 URL 문자열. string BuildAuthorizationUrl(string clientId, string redirectUri, string state, string codeChallenge, string codeChallengeMethod, string[] scopes); /// @@ -44,7 +53,11 @@ public interface IOAuth2Provider /// 사용자 정보 응답을 표준 형식으로 변환 /// /// 제공자별 JSON 응답 - /// 표준화된 사용자 정보 + /// +/// 제공자별 JSON 응답을 파싱하여 표준화된 OAuth2UserInfo 객체로 변환합니다. +/// +/// OAuth2 공급자가 반환한 사용자 정보의 JSON 문자열. +/// 표준화된 사용자 정보(OAuth2UserInfo). OAuth2UserInfo ParseUserInfo(string jsonResponse); } } diff --git a/ProjectVG.Application/Services/Auth/IOAuth2Service.cs b/ProjectVG.Application/Services/Auth/IOAuth2Service.cs index da3f68e..c294eaa 100644 --- a/ProjectVG.Application/Services/Auth/IOAuth2Service.cs +++ b/ProjectVG.Application/Services/Auth/IOAuth2Service.cs @@ -17,7 +17,16 @@ public interface IOAuth2Service /// PKCE challenge 방법 (S256) /// PKCE code verifier /// 클라이언트 리다이렉트 URI - /// OAuth2 인증 URL + /// +/// 지정된 OAuth2 제공자와 PKCE 매개변수를 사용해 인증용 URL을 생성합니다. +/// +/// 사용할 OAuth2 제공자 식별자(예: "Google", "Apple"). +/// CSRF 보호 및 요청 식별을 위한 상태 문자열. +/// PKCE 흐름에서 전송할 코드 챌린지(주로 S256으로 해시된 값). +/// 코드 챌린지의 해시 방법(예: "S256"). +/// 선택적 PKCE 코드 검증기(일부 흐름에서 사용됨). +/// 클라이언트가 인증 후 리디렉션될 URI. +/// 외부 제공자 인증을 시작할 수 있는 전체 OAuth2 인증 URL. Task BuildAuthorizationUrlAsync(string providerName, string state, string codeChallenge, string codeChallengeMethod, string codeVerifier, string clientRedirectUri); /// @@ -25,20 +34,33 @@ public interface IOAuth2Service /// /// 인증 코드 /// 상태값 - /// 콜백 처리 결과 + /// +/// OAuth2 콜백을 처리하여 인증 코드를 토큰으로 교환하고 사용자 로그인 흐름을 완료한 후 결과를 반환합니다. +/// +/// OAuth2 제공자가 전달한 일회성 인증 코드(authorization code). +/// 콜백 요청 시 전달된 상태 값(CSRF 방지용, 이전에 생성한 상태와 일치해야 함). +/// 콜백 처리 결과를 담은 (성공/실패 상태 및 관련 데이터). Task HandleOAuth2CallbackAsync(string code, string state); /// /// 상태값으로 저장된 OAuth2 토큰 데이터 조회 /// /// 상태값 - /// 토큰 데이터 (없으면 null) + /// +/// 주어진 상태값(state)에 연관된 임시 저장된 OAuth2 토큰 데이터를 비동기적으로 조회합니다. +/// +/// OAuth2 흐름에서 생성된 상태값(예: CSRF 방지용 식별자) — 이 값을 키로 토큰 데이터를 조회합니다. +/// 해당 상태에 저장된 OAuth2 토큰 데이터(OAuth2TokenData) 또는 존재하지 않으면 null. Task GetTokenDataAsync(string state); /// /// OAuth2 토큰 데이터 삭제 (사용 후 정리) /// - /// 삭제할 토큰 데이터의 상태값 + /// +/// 주어진 상태값(state)에 연관된 임시 저장된 OAuth2 토큰 데이터를 삭제합니다. +/// +/// 토큰 데이터를 조회·식별하는 고유 상태값(요청에서 사용된 state 문자열). +Task DeleteTokenDataAsync(string state); Task DeleteTokenDataAsync(string state); /// @@ -48,7 +70,14 @@ public interface IOAuth2Service /// 클라이언트 ID /// 리다이렉트 URI /// PKCE code verifier (선택적) - /// 토큰 교환 결과 + /// +/// 인증 코드(Authorization Code)를 액세스 토큰으로 교환합니다. +/// +/// OAuth2 제공자가 발급한 인증 코드. +/// 클라이언트 식별자(Client ID). +/// 토큰 교환 시 사용한 리다이렉트 URI(등록된 값과 일치해야 함). +/// PKCE 흐름에서 사용되는 코드 검증기(선택적). +/// 토큰 교환 결과를 담은 객체. Task ExchangeAuthorizationCodeAsync(string code, string clientId, string redirectUri, string codeVerifier = ""); /// @@ -56,7 +85,12 @@ public interface IOAuth2Service /// /// OAuth2 액세스 토큰 /// 제공자명 (google, apple) - /// 사용자 정보 + /// +/// 주어진 액세스 토큰으로 지정된 OAuth2 제공자에서 사용자 정보를 조회합니다. +/// +/// OAuth2 액세스 토큰(유효한 토큰이어야 함). +/// 사용자 정보를 조회할 OAuth2 제공자의 식별자(예: "Google", "Apple"). +/// 조회된 사용자 정보(OAuth2UserInfo). Task GetUserInfoAsync(string accessToken, string provider); /// @@ -64,27 +98,43 @@ public interface IOAuth2Service /// /// 상태값 /// OAuth2 요청 정보 - /// 저장된 요청 정보 + /// +/// 주어진 상태값(state)에 대해 OAuth2 인증 요청 정보를 임시 저장소에 저장합니다. +/// +/// 저장 키로 사용되는 상태값(예: CSRF 보호용 state). +/// 저장할 OAuth2 요청 정보. +/// 저장된 OAuth2 요청 정보(임시 저장소에 저장된 객체). Task StoreOAuth2RequestAsync(string state, OAuth2AuthRequest request); /// /// 저장된 OAuth2 요청 정보 조회 /// /// 상태값 - /// 요청 정보 (없으면 null) + /// +/// 상태값(state)에 저장된 OAuth2 인증 요청 정보를 조회합니다. +/// +/// 조회할 OAuth2 요청을 식별하는 상태 문자열. +/// 해당 상태에 저장된 OAuth2 요청 정보. 없으면 null을 반환합니다. Task GetOAuth2RequestAsync(string state); /// /// OAuth2 요청 정보 삭제 (사용 후 정리) /// - /// 삭제할 요청의 상태값 + /// +/// 지정된 상태값으로 임시 저장된 OAuth2 인증 요청 정보를 삭제합니다. +/// +/// 삭제할 요청을 식별하는 상태값(콜백에 사용된 state) Task DeleteOAuth2RequestAsync(string state); /// /// 토큰 데이터를 상태값과 함께 임시 저장 /// /// 상태값 - /// 저장할 토큰 데이터 + /// +/// 주어진 상태값(state)에 연결하여 토큰 관련 데이터를 비동기적으로 임시 저장합니다. +/// +/// 토큰 데이터를 조회하거나 삭제할 때 사용할 식별자(상태값). +/// 임시로 저장할 토큰 정보(액세스 토큰, 리프레시 토큰 등 임의의 형태). Task StoreTokenDataAsync(string state, object tokenData); } diff --git a/ProjectVG.Application/Services/Auth/OAuth2ProviderFactory.cs b/ProjectVG.Application/Services/Auth/OAuth2ProviderFactory.cs index 3487cd5..7fc64af 100644 --- a/ProjectVG.Application/Services/Auth/OAuth2ProviderFactory.cs +++ b/ProjectVG.Application/Services/Auth/OAuth2ProviderFactory.cs @@ -12,6 +12,13 @@ public class OAuth2ProviderFactory : IOAuth2ProviderFactory { private readonly Dictionary _providers; + /// + /// OAuth2 공급자 팩토리의 기본 생성자입니다. + /// + /// + /// 내부적으로 공급자 이름을 대소문자 구분 없이 비교하는 사전(Dictionary)을 초기화하고, + /// 기본 제공 OAuth2 공급자("google", "apple") 인스턴스를 등록합니다. + /// public OAuth2ProviderFactory() { _providers = new Dictionary(StringComparer.OrdinalIgnoreCase) @@ -26,7 +33,12 @@ public OAuth2ProviderFactory() /// /// 제공자 이름 (google, apple) /// OAuth2 제공자 인스턴스 - /// 지원하지 않는 제공자인 경우 + /// + /// 지정한 이름에 해당하는 OAuth2 제공자(IOAuth2Provider)를 반환합니다. + /// + /// 조회할 제공자 이름(대소문자 구분 없이 비교됨). 비어 있거나 null일 수 없습니다. + /// 요청한 이름에 매칭되는 IOAuth2Provider 인스턴스. + /// providerName이 null/빈 값이면 ErrorCode.OAUTH2_PROVIDER_NOT_CONFIGURED, 등록되지 않은 제공자 이름이면 ErrorCode.OAUTH2_PROVIDER_NOT_SUPPORTED를 포함하여 발생합니다. public IOAuth2Provider GetProvider(string providerName) { if (string.IsNullOrEmpty(providerName)) { @@ -43,7 +55,10 @@ public IOAuth2Provider GetProvider(string providerName) /// /// 지원하는 모든 제공자 목록 조회 /// - /// 지원하는 제공자 이름 목록 + /// + /// 팩토리가 현재 등록하고 있는 OAuth2 제공자 이름들의 컬렉션을 반환합니다. + /// + /// 등록된 제공자 이름들의 읽기 전용 컬렉션(딕셔너리의 키 컬렉션). public IEnumerable GetSupportedProviders() { return _providers.Keys; @@ -53,7 +68,11 @@ public IEnumerable GetSupportedProviders() /// 제공자가 지원되는지 확인 /// /// 확인할 제공자 이름 - /// 지원 여부 + /// + /// 지정된 OAuth2 제공자가 공장에 등록되어 있는지 확인합니다. 대소문자 구분 없이 비교합니다. + /// + /// 확인할 제공자 이름(빈 문자열 또는 null이면 false를 반환). + /// 등록되어 있으면 true, 그렇지 않으면 false. public bool IsProviderSupported(string providerName) { return !string.IsNullOrEmpty(providerName) && _providers.ContainsKey(providerName); @@ -69,20 +88,35 @@ public interface IOAuth2ProviderFactory /// 제공자 이름으로 OAuth2 제공자 인스턴스 조회 /// /// 제공자 이름 - /// OAuth2 제공자 인스턴스 + /// +/// 지정한 이름에 해당하는 OAuth2 제공자 인스턴스를 반환합니다. +/// +/// 조회할 제공자 이름(빈 문자열 또는 null이면 예외). +/// 요청한 이름에 매칭되는 인스턴스. +/// +/// providerName이 null 또는 빈 문자열일 때() +/// 또는 등록되지 않은 제공자 이름일 때(). +/// IOAuth2Provider GetProvider(string providerName); /// /// 지원하는 모든 제공자 목록 조회 /// - /// 지원하는 제공자 이름 목록 + /// +/// 팩토리에 등록된 OAuth2 제공자들의 이름을 반환합니다(대소문자 구분 없음). +/// +/// 등록된 제공자 이름(string)의 열거형 컬렉션 IEnumerable GetSupportedProviders(); /// /// 제공자가 지원되는지 확인 /// /// 확인할 제공자 이름 - /// 지원 여부 + /// +/// 지정한 OAuth2 제공자가 팩토리에 등록되어 있고 사용할 수 있는지 확인합니다. +/// +/// 확인할 공급자 이름(빈 문자열 또는 null이면 false를 반환). +/// 대소문자를 구분하지 않는 비교로 등록된 제공자이면 true, 그렇지 않으면 false. bool IsProviderSupported(string providerName); } } diff --git a/ProjectVG.Application/Services/Auth/OAuth2Service.cs b/ProjectVG.Application/Services/Auth/OAuth2Service.cs index 6c835dc..bfcbda4 100644 --- a/ProjectVG.Application/Services/Auth/OAuth2Service.cs +++ b/ProjectVG.Application/Services/Auth/OAuth2Service.cs @@ -22,6 +22,12 @@ public class OAuth2Service : IOAuth2Service private readonly Dictionary _oauth2Requests = new(); private readonly Dictionary _tokenData = new(); + /// + /// OAuth2Service의 인스턴스를 생성합니다. + /// + /// + /// 인증 흐름을 처리하기 위해 필요한 HTTP 클라이언트, 로거, 제공자 설정, 인증 서비스 및 제공자 팩토리를 주입받아 내부 필드를 초기화합니다. + /// public OAuth2Service( IHttpClientFactory httpClientFactory, ILogger logger, @@ -45,7 +51,17 @@ public OAuth2Service( /// PKCE challenge 방법 /// PKCE code verifier /// 클라이언트 리다이렉트 URI - /// OAuth2 인증 URL + /// + /// 지정된 OAuth2 공급자에 대한 인증(authorization) URL을 생성합니다. + /// + /// 사용할 OAuth2 공급자 식별자(예: "google"). + /// 클라이언트에서 전달한 상태값(state). 요청 식별 및 CSRF 방지에 사용됩니다. + /// PKCE에서 사용되는 code_challenge 문자열(없으면 빈 문자열 가능). + /// PKCE 코드 챌린지 방식(e.g. "S256"). + /// PKCE에서 사용되는 code_verifier(서버에 저장됨, 필요 시 빈 문자열 가능). + /// 클라이언트 측 리디렉션 URI(인증 후 클라이언트로 돌아갈 주소, 내부 요청에 저장됨). + /// 공급자별로 생성된 OAuth2 인증 URL 문자열. + /// 지정한 공급자가 구성되지 않았거나 비활성화된 경우, 또는 공급자의 클라이언트 ID가 유효하지 않은 경우 발생합니다. public async Task BuildAuthorizationUrlAsync(string providerName, string state, string codeChallenge, string codeChallengeMethod, string codeVerifier, string clientRedirectUri) { var provider = _providerFactory.GetProvider(providerName); @@ -82,6 +98,26 @@ public async Task BuildAuthorizationUrlAsync(string providerName, string return authUrl; } + /// + /// OAuth2 콜백을 처리하고 클라이언트로 리디렉션할 URL을 반환합니다. + /// + /// + /// 저장된 OAuth2 요청(state)을 조회하고, 받은 authorization code로 토큰을 교환한 뒤 + /// 공급자에서 사용자 정보를 조회하여 애플리케이션에 로그인합니다. 로그인 결과의 토큰 정보를 + /// 상태(state)에 연계하여 저장하고, 클라이언트 리디렉션용 URL을 생성해 반환합니다. + /// + /// OAuth2 공급자가 콜백으로 전달한 authorization code. + /// 초기에 생성되어 전달된 state 값 (요청 식별자). + /// + /// OAuth2CallbackResult: + /// - Success: 처리 성공 여부 (성공 시 true) + /// - RedirectUrl: 클라이언트로 리디렉트할 URL (성공/상태 파라미터 포함) + /// + /// 다음 상황에서 발생: + /// - ErrorCode.OAUTH2_REQUEST_NOT_FOUND: state로 저장된 요청을 찾을 수 없을 때. + /// - ErrorCode.OAUTH2_TOKEN_EXCHANGE_FAILED: authorization code로 토큰 교환에 실패했을 때. + /// - ErrorCode.OAUTH2_USER_INFO_FAILED: 공급자에서 가져온 사용자 정보에 ID가 없을 때. + /// public async Task HandleOAuth2CallbackAsync(string code, string state) { var authRequest = await GetOAuth2RequestAsync(state); @@ -129,6 +165,19 @@ public async Task HandleOAuth2CallbackAsync(string code, s }; } + /// + /// 주어진 인증 코드로 제공자 토큰 엔드포인트에 교환 요청을 수행하여 액세스/리프레시 토큰을 반환합니다. + /// + /// + /// clientId로 구성된 제공자를 찾아 토큰 교환 요청을 구성하고 POST합니다. PKCE를 사용하는 경우 를 포함합니다. + /// + /// OAuth2 인증 서버가 콜백으로 제공한 authorization code. + /// 요청에 사용되는 클라이언트 ID(설정에서 제공자 식별에 사용됨). + /// 토큰 교환에 사용된 리디렉션 URI(인증 요청과 동일해야 함). + /// 선택적 PKCE code_verifier(사용하는 경우 전달). + /// 교환이 성공하면 Success=true 및 Tokens에 액세스/리프레시 토큰과 만료 정보를 포함한 TokenResponse. + /// clientId가 구성에서 발견되지 않을 경우(OAUTH2_CLIENT_ID_INVALID). + /// 토큰 엔드포인트 호출이 실패한 경우(OAUTH2_TOKEN_EXCHANGE_FAILED). public async Task ExchangeAuthorizationCodeAsync(string code, string clientId, string redirectUri, string codeVerifier = "") { var providerSettings = GetProviderByClientId(clientId); @@ -180,6 +229,13 @@ public async Task ExchangeAuthorizationCodeAsync(string code, str ); } + /// + /// 지정된 OAuth2 공급자에서 액세스 토큰으로 사용자 정보를 조회하여 공급자별 파싱 결과를 반환합니다. + /// + /// 요청에 사용할 Bearer 액세스 토큰. + /// IOAuth2ProviderFactory에서 조회할 공급자 식별자(예: "google"). + /// 공급자 구현에 의해 파싱된 OAuth2UserInfo 객체. + /// 공급자의 사용자 정보 엔드포인트 호출이 실패할 경우 던져집니다. public async Task GetUserInfoAsync(string accessToken, string providerName) { var provider = _providerFactory.GetProvider(providerName); @@ -201,6 +257,12 @@ public async Task GetUserInfoAsync(string accessToken, string pr ); } + /// + /// 지정된 상태(state)에 대한 OAuth2 인증 요청을 직렬화하여 인메모리 저장소에 보관합니다. + /// + /// 요청을 식별하는 고유한 상태 토큰. + /// 저장할 OAuth2 인증 요청 객체. + /// 저장된 동일한 OAuth2AuthRequest 객체를 반환합니다. public async Task StoreOAuth2RequestAsync(string state, OAuth2AuthRequest request) { try @@ -216,6 +278,11 @@ public async Task StoreOAuth2RequestAsync(string state, OAuth } } + /// + /// 주어진 상태(state)에 연관된 저장된 OAuth2 인증 요청을 조회하여 역직렬화된 OAuth2AuthRequest를 반환합니다. + /// + /// 조회할 OAuth2 인증 요청과 연관된 상태 문자열(일치하는 키). + /// 찾으면 역직렬화된 인스턴스, 없으면 null을 반환합니다. public async Task GetOAuth2RequestAsync(string state) { if (_oauth2Requests.TryGetValue(state, out var json)) { @@ -228,24 +295,43 @@ public async Task StoreOAuth2RequestAsync(string state, OAuth return null; } + /// + /// 지정된 상태 키에 연관된 저장된 OAuth2 인증 요청을 삭제합니다. + /// + /// 삭제할 OAuth2 인증 요청을 식별하는 상태(state) 문자열 키. public async Task DeleteOAuth2RequestAsync(string state) { _oauth2Requests.Remove(state); await Task.CompletedTask; } + /// + /// 주어진 토큰 관련 데이터를 JSON으로 직렬화하여 서비스의 인메모리 토큰 저장소에 지정된 state 키로 저장합니다. + /// + /// 토큰을 식별할 클라이언트 상태값(state) 문자열. + /// 저장할 토큰 정보(예: 액세스/리프레시 토큰, 만료 정보 등)를 포함한 객체. public async Task StoreTokenDataAsync(string state, object tokenData) { _tokenData[state] = JsonSerializer.Serialize(tokenData); await Task.CompletedTask; } + /// + /// 지정한 OAuth2 상태 키와 연관된 저장된 토큰 데이터를 삭제합니다. + /// + /// 삭제할 토큰 데이터가 저장된 OAuth2 요청의 상태(state) 키. + public async Task DeleteTokenDataAsync(string state) public async Task DeleteTokenDataAsync(string state) { _tokenData.Remove(state); await Task.CompletedTask; } + /// + /// 주어진 상태(state)에 연관된 저장된 OAuth2 토큰 데이터를 조회합니다. + /// + /// OAuth2 인증 흐름에서 사용한 상태 값(state) — 토큰 데이터의 키로 사용됩니다. + /// 해당 상태에 연관된 인스턴스 또는 존재하지 않으면 null. public async Task GetTokenDataAsync(string state) { if (_tokenData.TryGetValue(state, out var json)) { @@ -256,17 +342,33 @@ public async Task DeleteTokenDataAsync(string state) return null; } + /// + /// 지정된 클라이언트 ID와 일치하는 OAuth2 공급자 설정을 조회합니다. + /// + /// 조회할 공급자의 클라이언트 ID. + /// 클라이언트 ID에 매칭되는 객체 또는 찾을 수 없으면 null. private OAuth2Settings? GetProviderByClientId(string clientId) { return _settings.Providers.Values.FirstOrDefault(p => p.ClientId == clientId); } + /// + /// 주어진 클라이언트 ID에 매핑된 OAuth2 제공자 이름을 반환합니다. + /// + /// 조회할 제공자의 클라이언트 ID. + /// 클라이언트 ID에 매칭되는 제공자 이름. 매칭되는 항목이 없으면 기본값 "google"을 반환합니다. private string GetProviderName(string clientId) { var provider = _settings.Providers.FirstOrDefault(p => p.Value.ClientId == clientId); return provider.Key ?? "google"; } + /// + /// 지정된 클라이언트 ID에 대응하는 OAuth2 공급자 이름을 반환합니다. 구성된 프로바이더 목록을 검색하여 일치하는 항목을 찾습니다. + /// 기본값은 찾지 못한 경우 "google"입니다. + /// + /// 검색할 공급자의 클라이언트 ID. + /// 클라이언트 ID에 매핑된 공급자 이름, 없으면 "google". private string GetProviderNameFromClientId(string clientId) { foreach (var provider in _settings.Providers) { diff --git a/ProjectVG.Application/Services/Auth/Providers/AppleOAuth2Provider.cs b/ProjectVG.Application/Services/Auth/Providers/AppleOAuth2Provider.cs index 2b5a000..cc804a8 100644 --- a/ProjectVG.Application/Services/Auth/Providers/AppleOAuth2Provider.cs +++ b/ProjectVG.Application/Services/Auth/Providers/AppleOAuth2Provider.cs @@ -17,6 +17,16 @@ public class AppleOAuth2Provider : IOAuth2Provider public string[] DefaultScopes => new[] { "name", "email" }; + /// + /// Apple OAuth2 승인(authorization) 엔드포인트로 리디렉션할 수 있는 인증 URL을 생성합니다. + /// + /// Apple에 등록된 클라이언트(앱) 식별자. + /// 인증 후 Apple이 결과를 전송할 리디렉션 URI. + /// CSRF 보호 및 상태 전달용 임의 문자열. + /// PKCE 흐름의 코드 챌린지 값. + /// 코드 챌린지 방법(예: "S256"). + /// 요청할 권한(예: "name", "email") 목록; 내부적으로 공백으로 결합되어 전송됩니다. + /// 모든 매개변수를 URL 인코딩하여 구성한 Apple 인증(authorization) URL. public string BuildAuthorizationUrl(string clientId, string redirectUri, string state, string codeChallenge, string codeChallengeMethod, string[] scopes) { var scopeString = string.Join(" ", scopes); @@ -32,6 +42,11 @@ public string BuildAuthorizationUrl(string clientId, string redirectUri, string $"&response_mode=form_post"; } + /// + /// JSON 응답을 파싱하여 OAuth2UserInfo 객체를 생성합니다. + /// + /// Apple의 userinfo 또는 ID 토큰에서 디코딩한 JSON 문자열(최소한 "sub" 필드를 포함). + /// 파싱된 사용자 정보를 담은 OAuth2UserInfo 객체(Provider는 "apple"). public OAuth2UserInfo ParseUserInfo(string jsonResponse) { var userData = JsonSerializer.Deserialize>(jsonResponse); diff --git a/ProjectVG.Application/Services/Auth/Providers/GoogleOAuth2Provider.cs b/ProjectVG.Application/Services/Auth/Providers/GoogleOAuth2Provider.cs index 3fe9288..fd1857c 100644 --- a/ProjectVG.Application/Services/Auth/Providers/GoogleOAuth2Provider.cs +++ b/ProjectVG.Application/Services/Auth/Providers/GoogleOAuth2Provider.cs @@ -16,6 +16,16 @@ public class GoogleOAuth2Provider : IOAuth2Provider public string[] DefaultScopes => new[] { "openid", "email", "profile" }; + /// + /// Google OAuth2 인증 요청용 Authorization URL을 생성합니다. + /// + /// OAuth 클라이언트 ID. + /// 인증 후 리디렉션될 URI. + /// CSRF 보호를 위한 상태 값. + /// PKCE 코드 챌린지 (base64/url-safe 또는 SHA256 기반값). + /// PKCE 코드 챌린지 방식 (예: "S256"). + /// 요청할 권한 범위들; 내부에서 공백으로 결합되어 전송됩니다. + /// Google OAuth2 인증 엔드포인트로의 완전한 URL 문자열(모든 파라미터는 URL 인코딩됨). public string BuildAuthorizationUrl(string clientId, string redirectUri, string state, string codeChallenge, string codeChallengeMethod, string[] scopes) { var scopeString = string.Join(" ", scopes); @@ -30,6 +40,14 @@ public string BuildAuthorizationUrl(string clientId, string redirectUri, string $"&code_challenge_method={Uri.EscapeDataString(codeChallengeMethod)}"; } + /// + /// JSON 응답에서 Google 사용자 정보를 파싱하여 OAuth2UserInfo 객체로 반환합니다. + /// + /// Google UserInfo 엔드포인트에서 반환된 JSON 문자열(객체 형태, 최소한 "id"와 "email" 필드가 문자열로 포함되어야 함). + /// 파싱된 사용자 정보(OAuth2UserInfo). Id와 Email은 JSON의 각각 "id", "email" 값을 사용하고 Provider는 해당 프로바이더 이름으로 설정됩니다. + /// + /// 전달된 jsonResponse가 유효한 JSON이 아니거나 기대하는 필드가 없거나 타입이 맞지 않으면 JsonException, KeyNotFoundException 또는 InvalidOperationException 등이 발생할 수 있습니다. + /// public OAuth2UserInfo ParseUserInfo(string jsonResponse) { var userData = JsonSerializer.Deserialize>(jsonResponse); diff --git a/ProjectVG.Application/Services/Chat/ChatService.cs b/ProjectVG.Application/Services/Chat/ChatService.cs index a9def06..dcd238d 100644 --- a/ProjectVG.Application/Services/Chat/ChatService.cs +++ b/ProjectVG.Application/Services/Chat/ChatService.cs @@ -29,6 +29,9 @@ public class ChatService : IChatService private readonly IChatMetricsService _metricsService; private readonly ChatFailureHandler _failureHandler; + /// + /// ChatService의 인스턴스를 초기화합니다. 채팅 요청의 검증, 컨텍스트 준비, 입력/LLM/TTS 처리, 결과 전송 및 실패/메트릭 관리를 위한 의존성을 주입하고 필드를 설정합니다. + /// public ChatService( IServiceScopeFactory scopeFactory, ILogger logger, @@ -60,6 +63,15 @@ ChatFailureHandler failureHandler _failureHandler = failureHandler; } + /// + /// 채팅 요청을 큐에 등록하고 비동기 처리 작업을 시작합니다. + /// + /// + /// 함수는 요청 메트릭을 시작하고 요청 유효성 검사를 수행한 뒤 내부 처리에 필요한 컨텍스트를 준비합니다. + /// 이후 실제 처리(LLM/TTS/전송/영속화)는 백그라운드 작업으로 비동기 실행되며, 호출자는 즉시 수락 응답을 받습니다. + /// + /// 처리할 채팅 요청 정보(세션, 사용자, 캐릭터 식별자와 메시지 등)를 포함합니다. + /// 요청이 수락되었음을 나타내는 ChatRequestResponse(세션Id, 사용자Id, 캐릭터Id 포함)를 반환합니다. public async Task EnqueueChatRequestAsync(ProcessChatCommand command) { _metricsService.StartChatMetrics(command.SessionId, command.UserId.ToString(), command.CharacterId.ToString()); @@ -78,7 +90,16 @@ public async Task EnqueueChatRequestAsync(ProcessChatComman /// /// 채팅 요청 준비 + /// + /// 채팅 요청을 처리하기 위한 실행 컨텍스트를 준비합니다. /// + /// + /// 지정된 명령에서 캐릭터 데이터를 조회하고(캐릭터 ID), + /// 최근 대화 내역을 가져온 후(사용자·캐릭터), + /// 사용자 입력 분석과 연관된 액션을 처리하고 메모리 컨텍스트를 수집합니다. + /// 반환되는 ChatProcessContext는 이후 LLM/TTS 및 결과 처리 파이프라인에서 사용됩니다. + /// + /// LLM·TTS 처리에 필요한 명령, 캐릭터 정보, 대화 기록 및 메모리 컨텍스트를 포함한 ChatProcessContext 객체. private async Task PrepareChatRequestAsync(ProcessChatCommand command) { var characterDto = await _characterService.GetCharacterByIdAsync(command.CharacterId); @@ -94,7 +115,15 @@ private async Task PrepareChatRequestAsync(ProcessChatComman /// /// 채팅 요청 처리 + /// + /// 채팅 처리 파이프라인(LLM → TTS)을 실행하고 결과를 전송·영속화하며 메트릭과 실패 처리를 관리합니다. /// + /// 처리에 필요한 명령, 캐릭터, 대화 내역 및 메모리 컨텍스트를 포함한 실행 컨텍스트. + /// + /// - LLM과 TTS 처리 후 DI 범위를 생성하여 ChatResultProcessor로 결과를 전송하고 저장합니다. + /// - 실행 중 발생한 예외는 ChatFailureHandler에 위임되어 처리되며, 예외는 호출자에게 전파되지 않습니다. + /// - 호출이 완료되면 메트릭 종료 및 로깅이 수행됩니다. + /// private async Task ProcessChatRequestInternalAsync(ChatProcessContext context) { try { diff --git a/ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs b/ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs index 9884df6..e7e310f 100644 --- a/ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs +++ b/ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs @@ -8,11 +8,20 @@ public class ChatMetricsService : IChatMetricsService private readonly ILogger _logger; private readonly AsyncLocal _currentMetrics = new(); + /// + /// ChatMetricsService의 새 인스턴스를 생성합니다. + /// public ChatMetricsService(ILogger logger) { _logger = logger; } + /// + /// 현재 비동기 컨텍스트에 대한 새 채팅 메트릭을 초기화하고 저장합니다. + /// + /// 해당 대화의 고유 세션 식별자. + /// 대화를 시작한 사용자 식별자. + /// 대화에 사용된 캐릭터 또는 에이전트 식별자. public void StartChatMetrics(string sessionId, string userId, string characterId) { _currentMetrics.Value = new ChatMetrics @@ -25,6 +34,10 @@ public void StartChatMetrics(string sessionId, string userId, string characterId Console.WriteLine($"[METRICS] 채팅 메트릭 시작: {sessionId}"); } + /// + /// 현재 비동기 컨텍스트의 채팅 메트릭에 새로운 프로세스 항목을 생성하여 시작 시간을 기록합니다. + /// + /// 측정할 프로세스의 식별명(예: "Tokenization", "ModelCall" 등). 현재 컨텍스트에 활성 ChatMetrics가 없으면 호출 시 아무 작업도 수행하지 않습니다. public void StartProcessMetrics(string processName) { if (_currentMetrics.Value == null) return; @@ -39,6 +52,17 @@ public void StartProcessMetrics(string processName) Console.WriteLine($"[METRICS] 프로세스 시작: {processName}"); } + /// + /// 현재 컨텍스트의 활성 채팅 메트릭에서 지정된 이름의 미종료 프로세스를 종료하고 관련 속성(종료시간, 지속시간, 비용, 오류, 추가 데이터)을 설정합니다. + /// + /// 종료할 프로세스의 이름(식별자). + /// 해당 프로세스에 기록할 비용(기본값 0). + /// 프로세스 종료 시 기록할 오류 메시지(있을 경우). + /// 프로세스에 연관된 추가 메타데이터(있을 경우). + /// + /// 현재 컨텍스트에 활성 ChatMetrics가 없거나 해당 이름의 미종료 프로세스를 찾지 못하면 아무 작업도 수행하지 않고 반환합니다. + /// 종료 시점은 UTC 기준 현재 시간으로 설정되며, Duration은 StartTime으로부터의 차이로 계산됩니다. + /// public void EndProcessMetrics(string processName, decimal cost = 0, string? errorMessage = null, Dictionary? additionalData = null) { if (_currentMetrics.Value == null) return; @@ -56,6 +80,13 @@ public void EndProcessMetrics(string processName, decimal cost = 0, string? erro } } + /// + /// 현재 비동기 컨텍스트의 채팅 메트릭을 종료하고 요약 값을 계산합니다. + /// + /// + /// 활성 메트릭이 없으면 아무 작업도 수행하지 않습니다. 종료 시점(UTC)을 기록하고 전체 지속 시간과 + /// 모든 프로세스의 비용 합계를 TotalDuration, TotalCost에 각각 저장합니다. + /// public void EndChatMetrics() { if (_currentMetrics.Value == null) return; @@ -65,11 +96,23 @@ public void EndChatMetrics() _currentMetrics.Value.TotalCost = _currentMetrics.Value.ProcessMetrics.Sum(p => p.Cost); } + /// + /// 현재 비동기 컨텍스트에 연결된 ChatMetrics 인스턴스를 반환합니다. + /// + /// 현재 컨텍스트의 ChatMetrics 객체 또는 존재하지 않으면 null. public ChatMetrics? GetCurrentChatMetrics() { return _currentMetrics.Value; } + /// + /// 현재 비동기 컨텍스트에 저장된 채팅 메트릭을 조회하여 콘솔과 로거에 요약 및 프로세스별 세부 비용/시간을 기록합니다. + /// + /// + /// 메트릭이 없으면 아무 동작도 하지 않습니다. + /// 총비용과 프로세스별 비용은 내부 단위(정수형 비용)를 달러 단위로 변환하기 위해 100_000.0으로 나누어 로그에 표시합니다. + /// 출력 형식은 총비용(소수점 6자리)과 각 프로세스의 지속시간(밀리초) 및 비용(달러)입니다. + /// public void LogChatMetrics() { var metrics = _currentMetrics.Value; diff --git a/ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs b/ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs index b2e7779..93879f5 100644 --- a/ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs +++ b/ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs @@ -10,6 +10,10 @@ public class CostTrackingDecorator : ICostTrackingDecorator where T : clas private readonly IChatMetricsService _metricsService; private readonly string _processName; + /// + /// 지정된 서비스 인스턴스를 감싸고, 주어진 메트릭 서비스로 비용 추적을 수행하는 데코레이터를 초기화합니다. + /// + /// 메트릭에서 사용될 프로세스 식별자(이름). End/Start 호출에서 동일하게 사용됩니다. public CostTrackingDecorator(T service, IChatMetricsService metricsService, string processName) { _service = service; @@ -19,6 +23,11 @@ public CostTrackingDecorator(T service, IChatMetricsService metricsService, stri public T Service => _service; + /// + /// 주어진 객체에서 "Cost" 프로퍼티를 찾아 숫자값을 소수(decimal)로 반환합니다. + /// + /// Cost 프로퍼티를 검색할 대상 객체(널 허용). + /// Cost 프로퍼티가 존재하고 값이 있으면 해당 값을 decimal로 변환한 값, 그렇지 않으면 0. private decimal ExtractCost(object? result) { if (result == null) return 0; @@ -40,6 +49,18 @@ private decimal ExtractCost(object? result) return 0; } + /// + /// 지정된 ChatProcessContext를 사용해 내부 서비스의 ProcessAsync를 호출하고 해당 실행의 비용을 추적하여 메트릭을 기록합니다. + /// + /// 내부 서비스에 전달할 처리 컨텍스트. 이 컨텍스트에 있는 `Cost` 속성(있을 경우)을 읽어 비용으로 사용합니다. + /// + /// 내부 서비스에서 ChatProcessContext를 인수로 받는 `ProcessAsync` 메서드를 찾을 수 없거나, + /// 해당 메서드 호출 결과가 null이거나 Task가 아닌 경우 발생합니다. + /// + /// + /// - 메서드 시작 시 StartProcessMetrics를 호출하고, 완료 시 ExtractCost로 추출한 비용을 EndProcessMetrics에 전달합니다. + /// - 내부 호출에서 발생한 예외는 EndProcessMetrics에 0과 예외 메시지를 전달한 뒤 그대로 다시 throw됩니다. + /// public async Task ProcessAsync(ChatProcessContext context) { _metricsService.StartProcessMetrics(_processName); @@ -76,6 +97,17 @@ public async Task ProcessAsync(ChatProcessContext context) + /// + /// 주어진 사용자 입력과 대화 기록을 내부 서비스의 `ProcessAsync(string, IEnumerable{ConversationHistory})`에 위임하여 처리하고, + /// 호출 결과에서 `Cost` 값을 추출해 메트릭을 기록한 뒤 결과를 반환합니다. + /// + /// 분석할 사용자 입력 문자열. + /// 분석에 참조할 대화 히스토리 컬렉션. + /// 내부 서비스가 반환한 UserInputAnalysis 인스턴스. + /// + /// 내부 서비스에 적합한 `ProcessAsync(string, IEnumerable{ConversationHistory})` 메서드가 없거나, + /// 해당 메서드 호출 결과가 null이거나 기대한 반환 타입(Task<UserInputAnalysis>)이 아닌 경우 발생합니다. + /// public async Task ProcessAsync(string userInput, IEnumerable conversationHistory) { _metricsService.StartProcessMetrics(_processName); diff --git a/ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecoratorFactory.cs b/ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecoratorFactory.cs index 56f7894..37a9e41 100644 --- a/ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecoratorFactory.cs +++ b/ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecoratorFactory.cs @@ -4,6 +4,17 @@ namespace ProjectVG.Application.Services.Chat.CostTracking { public static class CostTrackingDecoratorFactory { + /// + /// 지정한 서비스 타입 T에 대해 비용 추적 데코레이터를 등록합니다. + /// + /// + /// - 원본 서비스 T를 지정된 ServiceLifetime으로 자체 구현체로 등록합니다. + /// - ICostTrackingDecorator<T>를 팩토리로 등록하며, 팩토리에서 원본 서비스 T와 IChatMetricsService를 Resolve하여 CostTrackingDecorator<T>를 생성합니다. + /// - 등록된 서비스들의 의존성이 누락되면 런타임 예외가 발생할 수 있습니다. + /// + /// 데코레이터가 비용을 집계할 때 사용할 프로세스 식별 이름입니다. + /// 원본 서비스 및 데코레이터에 적용할 ServiceLifetime(기본값: Scoped)입니다. + /// 데코레이터가 등록된 동일한 IServiceCollection을 반환합니다. public static IServiceCollection AddCostTrackingDecorator( this IServiceCollection services, string processName, diff --git a/ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs b/ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs index 67c9e8d..6199987 100644 --- a/ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs +++ b/ProjectVG.Application/Services/Chat/CostTracking/IChatMetricsService.cs @@ -4,11 +4,48 @@ namespace ProjectVG.Application.Services.Chat.CostTracking { public interface IChatMetricsService { - void StartChatMetrics(string sessionId, string userId, string characterId); - void StartProcessMetrics(string processName); - void EndProcessMetrics(string processName, decimal cost = 0, string? errorMessage = null, Dictionary? additionalData = null); - void EndChatMetrics(); - ChatMetrics? GetCurrentChatMetrics(); - void LogChatMetrics(); + /// +/// 지정된 세션, 사용자 및 캐릭터에 대한 채팅 비용/지표 수집을 초기화합니다. +/// +/// 추적할 채팅 세션의 고유 식별자. +/// 채팅을 수행하는 사용자의 고유 식별자. +/// 해당 세션에서 사용되는 캐릭터(또는 페르소나)의 식별자. +void StartChatMetrics(string sessionId, string userId, string characterId); + /// +/// 지정한 이름의 하위 프로세스에 대한 비용/시간 수집을 시작합니다. +/// +/// 측정할 하위 프로세스의 식별용 이름(예: "ModelInference", "PromptBuild"). +void StartProcessMetrics(string processName); + /// +/// 지정한 프로세스의 메트릭 수집을 종료하고 해당 데이터(비용, 오류, 추가 정보)를 기록합니다. +/// +/// 종료할 프로세스의 이름(식별자). +/// 해당 프로세스에 귀속되는 비용(기본값 0). +/// 프로세스 종료 시 발생한 오류 메시지(없으면 null). +/// 프로세스와 관련된 추가 키-값 정보(없으면 null). +/// +/// 이 호출은 현재 활성화된 채팅 메트릭 세션에 대해 지정된 프로세스의 메트릭을 마무리하고, 제공된 비용·오류·추가 데이터를 연관시킵니다. +/// +void EndProcessMetrics(string processName, decimal cost = 0, string? errorMessage = null, Dictionary? additionalData = null); + /// +/// 현재 활성화된 채팅 메트릭 세션을 종료하고 해당 세션의 메트릭 수집을 마무리합니다. +/// +/// +/// 이 메서드를 호출하면 진행 중인 프로세스 메트릭이 종료되고(있을 경우) 이후 는 null을 반환할 수 있습니다. +/// +void EndChatMetrics(); + /// +/// 현재 활성화된 채팅 메트릭스를 반환합니다. +/// +/// 활성화된 ChatMetrics 인스턴스 또는 활성화된 메트릭이 없으면 null. +ChatMetrics? GetCurrentChatMetrics(); + /// +/// 현재 수집된 채팅 메트릭(ChatMetrics)을 기록(로그 또는 외부 수집기로 전송)합니다. +/// +/// +/// 현재 활성화된 메트릭이 없으면 아무 작업도 수행하지 않습니다. +/// 호출은 메트릭의 상태를 외부로 내보내는 부작용을 발생시킵니다. +/// +void LogChatMetrics(); } } diff --git a/ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs b/ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs index 61b1e66..5640e2a 100644 --- a/ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs +++ b/ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs @@ -6,7 +6,20 @@ namespace ProjectVG.Application.Services.Chat.CostTracking public interface ICostTrackingDecorator where T : class { T Service { get; } - Task ProcessAsync(ChatProcessContext context); - Task ProcessAsync(string userInput, IEnumerable conversationHistory); + /// +/// 채팅 처리 컨텍스트에 대해 비용 추적 로직을 비동기적으로 실행합니다. +/// +/// 사용자 입력, 대화 이력 및 관련 메타데이터를 포함하는 처리 컨텍스트. +/// 처리가 완료될 때까지 대기할 수 있는 비동기 작업을 반환합니다. +Task ProcessAsync(ChatProcessContext context); + /// +/// 주어진 사용자 입력과 대화 기록을 바탕으로 입력을 분석하여 비용 추적에 필요한 정보를 생성합니다. +/// +/// 분석할 사용자의 원문 입력. +/// 분석 시 참조할 이전 대화 기록들의 열거(최근 대화부터 필요한 범위로 제공). +/// +/// 분석 결과를 담은 를 비동기적으로 반환합니다. +/// +Task ProcessAsync(string userInput, IEnumerable conversationHistory); } } diff --git a/ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs b/ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs index 1a390d7..6e7f6d5 100644 --- a/ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs +++ b/ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs @@ -7,10 +7,20 @@ namespace ProjectVG.Application.Services.Chat.Factories { public class ChatLLMFormat : ILLMFormat> { + /// + /// ChatLLMFormat의 기본 생성자입니다. + /// 새 인스턴스를 초기 상태로 초기화합니다. + /// public ChatLLMFormat() { } + /// + /// 주어진 처리 컨텍스트에서 캐릭터 정보를 읽어 시스템 메시지(캐릭터 프로필 문자열)를 생성합니다. + /// + /// 캐릭터 정보가 포함된 처리 컨텍스트. 가 null이면 예외가 발생합니다. + /// 캐릭터 이름, 설명, 역할, 성격, 말투를 각각 새 줄로 나열한 문자열. + /// input.Character가 null인 경우 발생합니다 ("캐릭터 정보가 로드되지 않았습니다."). public string GetSystemMessage(ChatProcessContext input) { var character = input.Character ?? throw new InvalidOperationException("캐릭터 정보가 로드되지 않았습니다."); @@ -25,6 +35,11 @@ public string GetSystemMessage(ChatProcessContext input) return sb.ToString(); } + /// + /// 시스템/LLM 지시문을 조합하여 반환합니다. + /// + /// 메모리(관련 기억)와 대화 기록을 포함한 처리 컨텍스트. MemoryContext와 ParseConversationHistory(5)의 결과를 사용합니다. + /// 메모리 목록(있을 경우), 최근 대화 기록(최대 5건, 있으면)과 포맷 지시문을 순서대로 포함한 지시문 문자열. public string GetInstructions(ChatProcessContext input) { var sb = new StringBuilder(); @@ -59,16 +74,36 @@ public string GetInstructions(ChatProcessContext input) public float Temperature => 0.7f; public int MaxTokens => 1000; + /// + /// LLM의 텍스트 응답을 ChatMessageSegment 목록으로 변환합니다. + /// + /// LLM이 생성한 원문 응답 텍스트. + /// 파싱 시 사용되는 컨텍스트(특히 캐릭터의 VoiceId를 통해 감정 매핑을 결정). + /// 응답에서 추출한 ChatMessageSegment 목록. 입력이 비어있거나 파싱 가능한 태그가 없으면 단일 'neutral' 세그먼트 또는 빈 목록을 반환할 수 있습니다. public List Parse(string llmResponse, ChatProcessContext input) { return ParseChatResponseToSegments(llmResponse, input.Character?.VoiceId); } + /// + /// 지정한 프롬프트 토큰 수와 완료 토큰 수를 기반으로 모델 사용 비용을 계산합니다. + /// + /// 프롬프트(입력) 토큰 수. + /// 완료(출력) 토큰 수. + /// 계산된 비용 (통화 단위: USD). public double CalculateCost(int promptTokens, int completionTokens) { return LLMModelInfo.CalculateCost(Model, promptTokens, completionTokens); } + /// + /// LLM에 반환 형식을 명시하는 지침 문자열을 생성합니다. + /// + /// + /// 결과 문자열은 응답이 반드시 감정 태그와 텍스트 쌍의 반복 형태로만 반환되도록 요구하며, + /// 사용 가능한 감정 목록(EmotionConstants.SupportedEmotions)에 기반한 허용 감정을 포함하고 예시를 제공합니다. + /// + /// LLM에게 전달할 포맷 지침을 담은 문자열. private string GetFormatInstructions() { string emotionList = string.Join(", ", EmotionConstants.SupportedEmotions); @@ -82,6 +117,17 @@ [neutral] 내가 그런다고 좋아할 것 같아? [shy] 하지만 츄 해준 "; } + /// + /// LLM 응답 텍스트를 ChatMessageSegment 목록으로 파싱합니다. + /// + /// + /// 입력 텍스트에서 "[감정] 발화" 형식의 태그들을 추출해 각 태그별로 ChatMessageSegment를 생성합니다. + /// voiceId가 주어지면 음성 프로필의 감정 매핑을 적용하고, 동일한 발화 텍스트는 대소문자 무시 기준으로 중복 제거합니다. + /// 입력이 비어있으면 빈 목록을 반환하며, 태그가 전혀 없으면 전체 응답을 단일 'neutral' 감정의 세그먼트로 반환합니다. + /// + /// 파싱할 LLM 응답 문자열. + /// (선택) 감정 매핑을 적용할 음성 프로필 ID. + /// 파싱된 ChatMessageSegment 목록. 입력이 비어있으면 빈 목록을 반환. private List ParseChatResponseToSegments(string llmText, string? voiceId = null) { if (string.IsNullOrWhiteSpace(llmText)) @@ -108,6 +154,11 @@ private List ParseChatResponseToSegments(string llmText, str return segments; } + /// + /// 음성 프로필 ID에서 감정 매핑을 조회합니다. + /// + /// 조회할 음성 프로필의 ID. null 또는 공백이면 조회하지 않습니다. + /// 프로필에 정의된 감정 매핑(Dictionary<string,string>). 프로필이 없거나 매핑이 없으면 null을 반환합니다. private Dictionary? GetEmotionMap(string? voiceId) { if (string.IsNullOrWhiteSpace(voiceId)) @@ -117,6 +168,17 @@ private List ParseChatResponseToSegments(string llmText, str return profile?.EmotionMap; } + /// + /// 정규식 매치 컬렉션을 처리하여 중복되지 않는 텍스트 세그먼트를 생성하고 감정 태그를 적용해 segments에 추가합니다. + /// + /// + /// 각 매치는 그룹 1을 감정 식별자(예: "happy"), 그룹 2를 텍스트로 취급합니다. emotionMap이 제공되면 그룹 1의 값은 매핑을 통해 변환되며, 없으면 원본 감정을 사용합니다. + /// 이미 seenTexts에 포함된 텍스트는 건너뛰어 중복 세그먼트 생성을 방지합니다. 새 세그먼트는 segments.Count를 인덱스로 하여 ChatMessageSegment.CreateTextOnly로 생성한 뒤 Emotion을 설정하고 segments에 추가됩니다. + /// + /// 정규식으로 추출된 매치 컬렉션(그룹 1: 감정, 그룹 2: 텍스트를 기대). + /// 원본 감정을 출력 감정으로 매핑하는 사전(없을 수 있음). + /// 생성된 ChatMessageSegment를 추가할 리스트(출력). + /// 이미 추가된 텍스트를 추적하는 집합(중복 방지용). private void ProcessMatches(MatchCollection matches, Dictionary? emotionMap, List segments, HashSet seenTexts) { for (int i = 0; i < matches.Count; i++) diff --git a/ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs b/ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs index 3afe36c..ace789e 100644 --- a/ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs +++ b/ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs @@ -7,7 +7,19 @@ public interface ILLMFormat string Model { get; } float Temperature { get; } int MaxTokens { get; } - TOutput Parse(string llmResponse, TInput input); - double CalculateCost(int promptTokens, int completionTokens); + /// +/// LLM 응답 문자열을 입력 정보(TInput)를 참고하여 TOutput 타입으로 변환(파싱)합니다. +/// +/// LLM에서 반환한 원본 응답 텍스트. +/// 파싱 동작에 필요한 컨텍스트 또는 제약을 담은 입력값. +/// 파싱 결과로 생성된 TOutput 인스턴스. +TOutput Parse(string llmResponse, TInput input); + /// +/// 주어진 프롬프트 토큰 수와 완료(응답) 토큰 수를 기반으로 LLM 호출 비용을 계산합니다. +/// +/// 프롬프트(입력 및 지시문)에 사용된 토큰 수. +/// LLM이 생성한 응답에 사용된 토큰 수. +/// 계산된 비용(통화 단위). 구현체는 모델별 가격 정책을 사용하여 두 토큰 수로부터 비용을 산출합니다. +double CalculateCost(int promptTokens, int completionTokens); } } diff --git a/ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs b/ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs index 3590803..53b6111 100644 --- a/ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs +++ b/ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs @@ -198,12 +198,24 @@ private List ParseKeywords(string keywordsStr) return null; } + /// + /// LLM 파싱 실패나 예외 발생 시 사용할 기본 유효 응답을 생성합니다. + /// + /// + /// "일반적인 대화" 문맥과 "대화" 의도를 가진 UserInputAnalysis 인스턴스(액션: Chat, 키워드 없음). + /// private UserInputAnalysis CreateDefaultValidResponse() { _logger?.LogInformation("기본 유효 응답 생성"); return UserInputAnalysis.CreateValid("일반적인 대화", "대화", UserInputAction.Chat, new List()); } + /// + /// 지정된 프롬프트 토큰 수와 응답 토큰 수에 대해 해당 모델의 비용을 계산합니다. + /// + /// 프롬프트(입력)에서 사용된 토큰 수. + /// LLM이 생성한 응답(출력)에서 사용된 토큰 수. + /// 모델별 요금 기준으로 계산된 비용(통화 단위). public double CalculateCost(int promptTokens, int completionTokens) { return LLMModelInfo.CalculateCost(Model, promptTokens, completionTokens); diff --git a/ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs b/ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs index 2263767..0164024 100644 --- a/ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs +++ b/ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs @@ -13,6 +13,12 @@ public class ChatFailureHandler private readonly IConversationService _conversationService; private readonly IMemoryClient _memoryClient; + /// + /// ChatFailureHandler 인스턴스를 초기화합니다. + /// + /// + /// 필요한 외부 서비스(로거, 웹소켓 매니저, 대화 및 메모리 서비스)를 주입받아 내부 필드에 할당합니다. + /// public ChatFailureHandler( ILogger logger, IWebSocketManager webSocketService, @@ -25,12 +31,23 @@ public ChatFailureHandler( _memoryClient = memoryClient; } + /// + /// 채팅 처리 중 발생한 예외를 기록하고 사용자에게 에러 메시지를 WebSocket으로 전송합니다. + /// + /// 에러가 발생한 채팅 세션의 컨텍스트(세션/사용자 식별자 포함). + /// 기록할 예외 객체. + /// 에러 처리 및 WebSocket 알림 전송 작업을 나타내는 비동기 . public Task HandleFailureAsync(ChatProcessContext context, Exception exception) { _logger.LogError(exception, "채팅 처리 실패: 세션 {UserId}", context.SessionId); return SendErrorMessageAsync(context, "요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); } + /// + /// 지정된 사용자에게 WebSocket을 통해 에러 메시지 페이로드를 전송합니다. + /// + /// 전송 대상의 사용자 식별자(UserId)와 로그용 세션 식별자(SessionId)를 포함하는 처리 컨텍스트. + /// 사용자에게 전달할 에러 텍스트 메시지. private async Task SendErrorMessageAsync(ChatProcessContext context, string errorMessage) { try diff --git a/ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs b/ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs index 873304a..ac778cd 100644 --- a/ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs +++ b/ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs @@ -19,6 +19,15 @@ public MemoryContextPreprocessor( _logger = logger; } + /// + — 사용자 입력 분석을 기반으로 메모리에서 관련 컨텍스트 텍스트를 비동기로 수집합니다. + /// + /// 검색할 메모리 컬렉션 이름. + /// 원본 사용자 메시지(향상된 쿼리나 키워드가 없을 때 대체로 사용됨). + /// 향상된 쿼리, 키워드, 감정, 시간 표현 등 검색 쿼리와 메모리 유형을 결정하기 위한 분석 결과. + /// + /// 검색된 메모리 항목들의 텍스트 목록을 반환합니다. 검색 실패나 예외 발생 시 빈 목록을 반환합니다. + /// public async Task> CollectMemoryContextAsync(string collection, string userMessage, UserInputAnalysis analysis) { try { @@ -37,6 +46,15 @@ public async Task> CollectMemoryContextAsync(string collection, str } } + /// + /// 검색에 사용할 쿼리를 결정합니다. + /// + /// + /// 우선적으로 UserInputAnalysis.EnhancedQuery가 비어있지 않으면 이를 반환하고, 그렇지 않으면 분석된 키워드들이 존재하면 키워드를 공백으로 결합한 문자열을 반환합니다. 둘 다 없으면 원본 사용자 메시지를 그대로 쿼리로 사용합니다. + /// + /// 사용자 원문 메시지(모든 다른 소스가 없을 때의 최종 폴백). + /// 사용자 입력에 대한 분석 결과(EnhancedQuery 또는 Keywords를 검사). + /// 검색에 사용할 쿼리 문자열. private string DetermineSearchQuery(string originalMessage, UserInputAnalysis analysis) { if (!string.IsNullOrWhiteSpace(analysis.EnhancedQuery)) { @@ -50,6 +68,14 @@ private string DetermineSearchQuery(string originalMessage, UserInputAnalysis an return originalMessage; } + /// + /// 주어진 사용자 입력 분석에 따라 검색에 사용할 메모리 유형을 결정합니다. + /// + /// 사용자 입력 분석 결과(감정 목록 및 시간 표현 포함). + /// + /// analysis.Emotions가 비어있지 않거나 analysis.ContainsTemporalExpression가 빈 문자열인 경우 을 반환하고, + /// 그렇지 않으면 을 반환합니다. + /// private MemoryType ChooseMemoryType(UserInputAnalysis analysis) { if (analysis.Emotions?.Any() == true || analysis.ContainsTemporalExpression.Equals(String.Empty)) { diff --git a/ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs b/ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs index adb69d6..88bc8b5 100644 --- a/ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs +++ b/ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs @@ -19,6 +19,12 @@ public UserInputAnalysisProcessor( _logger = logger; } + /// + /// 주어진 사용자 입력과 최근 대화 맥락을 바탕으로 LLM에 질의하여 입력의 의도·액션·대화 맥락 등을 분석한 결과를 비동기로 반환합니다. + /// + /// 분석할 원문 사용자 입력 문자열. + /// 최근 대화 맥락으로 사용할 대화 이력 컬렉션(내부에서 최대 5건만 사용). + /// 분석 결과를 담은 를 반환하는 . 오류 발생 시 기본 유효 분석 객체를 반환합니다. public async Task ProcessAsync(string userInput, IEnumerable conversationHistory) { try diff --git a/ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs b/ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs index 092d75f..579e3fa 100644 --- a/ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs +++ b/ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs @@ -9,6 +9,9 @@ public class ChatLLMProcessor private readonly ILLMClient _llmClient; private readonly ILogger _logger; + /// + /// ChatLLMProcessor의 새 인스턴스를 생성하고 필요한 의존성(LLM 클라이언트 및 로거)을 주입합니다. + /// public ChatLLMProcessor( ILLMClient llmClient, ILogger logger) @@ -17,6 +20,15 @@ public ChatLLMProcessor( _logger = logger; } + /// + /// 지정된 처리 컨텍스트를 사용해 LLM에 질의를 보내고, 응답을 파싱·비용을 계산하여 컨텍스트에 결과를 저장합니다. + /// + /// + /// 이 메서드는 LLM 요청을 비동기적으로 실행하고, 반환된 텍스트 응답을 포맷에 따라 분할(segments)하며 + /// 입력/출력 토큰 수를 기반으로 비용을 계산한 후 컨텍스트에 원본 응답, 세그먼트 및 비용을 설정합니다. + /// 또한 내부 로깅을 통해 처리 세부 정보를 남깁니다. + /// + /// 처리에 필요한 대화 상태, 사용자 메시지, 세션 식별자 등을 포함하는 컨텍스트. 호출 후 응답·세그먼트·비용이 이 컨텍스트에 저장됩니다. public async Task ProcessAsync(ChatProcessContext context) { var format = LLMFormatFactory.CreateChatFormat(); diff --git a/ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs b/ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs index f650f3f..221c8e9 100644 --- a/ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs +++ b/ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs @@ -15,6 +15,9 @@ public class ChatResultProcessor private readonly IMemoryClient _memoryClient; private readonly IWebSocketManager _webSocketService; + /// + /// ChatResultProcessor의 새 인스턴스를 초기화합니다. + /// public ChatResultProcessor( ILogger logger, IConversationService conversationService, @@ -27,6 +30,11 @@ public ChatResultProcessor( _webSocketService = webSocketService; } + /// + /// 채팅 처리 컨텍스트의 사용자 메시지와 AI 응답을 대화 기록에 저장하고 메모리에도 영구화합니다. + /// + /// 저장할 세션 및 메시지 정보를 포함하는 처리 컨텍스트(사용자 ID, 캐릭터 ID, 사용자 메시지, AI 응답, 세그먼트 등). + /// 비동기 작업을 나타내는 Task. public async Task PersistResultsAsync(ChatProcessContext context) { await _conversationService.AddMessageAsync(context.UserId, context.CharacterId, ChatRole.User, context.UserMessage); @@ -36,6 +44,13 @@ public async Task PersistResultsAsync(ChatProcessContext context) _logger.LogDebug("채팅 결과 저장 완료: 세션 {UserId}, 사용자 {UserId}", context.SessionId, context.UserId); } + /// + /// Assistant의 응답(context.Response)을 메모리 저장소에 비동기으로 삽입합니다. + /// + /// 저장할 텍스트(응답), 사용자 ID 등을 포함한 처리 컨텍스트. Response가 메모리의 Text로, UserId가 UserId로 사용되며 Speaker는 "ai"로 설정됩니다. + /// + /// 내부에서 발생한 예외는 캡처되어 경고 로그로 기록되며 호출자에게 전파되지 않습니다. + /// private async Task PersistMemoryAsync(ChatProcessContext context) { var insert = new MemoryInsertRequest @@ -55,6 +70,11 @@ private async Task PersistMemoryAsync(ChatProcessContext context) } } + /// + /// 컨텍스트의 세그먼트들을 순서대로 클라이언트에 WebSocket으로 전송한다. + /// 빈 세그먼트는 건너뛰며, 각 전송 메시지는 세션 ID, 텍스트 및 오디오 메타데이터(형식, 길이, 타임스탬프)와 오디오 데이터를 포함한다. + /// + /// 전송할 세그먼트(순서, 텍스트, 오디오 데이터/메타데이터)와 대상 식별자(UserId, SessionId)를 포함하는 처리 컨텍스트. public async Task SendResultsAsync(ChatProcessContext context) { foreach (var segment in context.Segments.OrderBy(s => s.Order)) { diff --git a/ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs b/ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs index ae6778c..3c3dc7b 100644 --- a/ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs +++ b/ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs @@ -9,6 +9,12 @@ public class ChatTTSProcessor private readonly ITextToSpeechClient _ttsClient; private readonly ILogger _logger; + /// + /// ChatTTSProcessor 인스턴스를 생성합니다. + /// + /// + /// 외부 TTS 클라이언트와 로깅 서비스를 주입 받아 내부 필드에 저장합니다. + /// public ChatTTSProcessor( ITextToSpeechClient ttsClient, ILogger logger) @@ -17,6 +23,10 @@ public ChatTTSProcessor( _logger = logger; } + /// + /// 채팅 처리 컨텍스트의 세그먼트들에 대해 음성 합성(TTS)을 비동기적으로 생성하고 결과를 적용한다. + /// + /// TTS 사용 여부(UseTTS), 대상 캐릭터(VoiceId 포함), 세그먼트 목록 및 비용 누적을 포함하는 처리 컨텍스트. 성공한 TTS 오디오는 각 세그먼트에 설정되고(SetAudioData), 오디오 길이에 따라 비용이 계산되어 context.AddCost로 누적된다. public async Task ProcessAsync(ChatProcessContext context) { if (!context.UseTTS || string.IsNullOrWhiteSpace(context.Character?.VoiceId) || context.Segments?.Count == 0) { @@ -62,6 +72,12 @@ public async Task ProcessAsync(ChatProcessContext context) context.SessionId, processedCount, context.Cost); } + /// + /// 주어진 감정 문자열을 정규화하여 음성 프로필이 지원하는 스타일로 반환합니다. + /// + /// 정규화할 감정 문자열(널이면 "neutral"로 간주). + /// 대상 음성의 VoiceProfile(지원되는 스타일 목록을 검사함). + /// 프로필에서 지원하는 감정 스타일이거나, 지원하지 않으면 "neutral". private string NormalizeEmotion(string? emotion, VoiceProfile profile) { var normalizedEmotion = emotion ?? "neutral"; @@ -73,6 +89,18 @@ private string NormalizeEmotion(string? emotion, VoiceProfile profile) return normalizedEmotion; } + /// + /// 지정한 음성 프로파일과 감정으로 텍스트를 음성으로 변환해 TTS 응답을 반환합니다. + /// + /// + /// 입력 텍스트는 비어 있거나 공백일 수 없으며 최대 300자까지 허용됩니다. 검증 실패나 호출 중 예외가 발생하면 + /// Success = false 및 ErrorMessage가 설정된 TextToSpeechResponse를 반환합니다. + /// 외부 TTS 클라이언트에 요청을 보내며, 성공 시 서버가 반환한 오디오 메타데이터(길이 등)를 포함한 응답을 반환합니다. + /// + /// 사용할 음성 프로파일(기본 언어와 VoiceId가 요청에 사용됨). + /// 변환할 텍스트(공백 이외의 문자 필요, 최대 300자). + /// 요청할 감정 스타일(프로필의 지원 스타일에 맞추어 전달되어야 함). + /// 외부 TTS 호출 결과를 나타내는 TextToSpeechResponse 객체. 실패 시 Success=false와 ErrorMessage가 설정됩니다. private async Task GenerateTTSAsync(VoiceProfile profile, string text, string emotion) { var startTime = DateTime.UtcNow; diff --git a/ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs b/ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs index cfe3988..c95ce3e 100644 --- a/ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs +++ b/ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs @@ -25,6 +25,17 @@ public ChatRequestValidator( _logger = logger; } + /// + /// 지정된 채팅 명령의 식별자들을 검증합니다. + /// + /// + /// - command.SessionId가 null 또는 빈 문자열이 아니면 세션 존재 여부를 확인합니다. + /// - command.UserId와 command.CharacterId의 존재 여부를 확인합니다. + /// 검증에 실패하면 적절한 예외를 던집니다. + /// + /// 검증할 채팅 명령; 사용되는 필드: SessionId (선택적), UserId, CharacterId. + /// 제공된 세션 ID가 유효하지 않을 때 (ErrorCode.INVALID_SESSION_ID). + /// 사용자 또는 캐릭터가 존재하지 않을 때 각각 (ErrorCode.USER_NOT_FOUND, ErrorCode.CHARACTER_NOT_FOUND). public async Task ValidateAsync(ProcessChatCommand command) { if (!string.IsNullOrEmpty(command.SessionId)) { diff --git a/ProjectVG.Application/Services/Session/ConnectionRegistry.cs b/ProjectVG.Application/Services/Session/ConnectionRegistry.cs index 8255e26..3aea924 100644 --- a/ProjectVG.Application/Services/Session/ConnectionRegistry.cs +++ b/ProjectVG.Application/Services/Session/ConnectionRegistry.cs @@ -8,6 +8,12 @@ public class ConnectionRegistry : IConnectionRegistry private readonly ILogger _logger; private readonly ConcurrentDictionary _connections = new(); + /// + /// ConnectionRegistry의 새 인스턴스를 초기화합니다. + /// + /// + /// 내부의 스레드 안전한 연결 저장소를 초기화하고 로깅을 위한 인스턴스를 보관합니다. + /// public ConnectionRegistry(ILogger logger) { _logger = logger; @@ -15,7 +21,13 @@ public ConnectionRegistry(ILogger logger) /// /// 연결을 등록합니다 + /// + /// 지정된 사용자 ID에 대해 클라이언트 연결을 등록하거나 기존 연결을 교체합니다. /// + /// 연결을 식별하는 사용자 고유 ID(키). 기존 항목이 있으면 새 연결로 대체됩니다. + /// + /// 연결은 내부의 스레드 안전한 저장소(ConcurrentDictionary)에 저장됩니다. + /// public void Register(string userId, IClientConnection connection) { _connections[userId] = connection; @@ -24,7 +36,13 @@ public void Register(string userId, IClientConnection connection) /// /// 연결을 해제합니다 + /// + /// 지정한 사용자 ID에 연결된 클라이언트 연결을 레지스트리에서 제거합니다. /// + /// 제거할 연결이 등록된 사용자 식별자. + /// + /// 등록된 연결이 없으면 예외를 발생시키지 않고 조용히 무시합니다. + /// public void Unregister(string userId) { if (_connections.TryRemove(userId, out var removed)) @@ -39,7 +57,12 @@ public void Unregister(string userId) /// /// 연결을 조회합니다 + /// + /// 지정된 사용자 ID에 연관된 클라이언트 연결을 시도해 가져옵니다. /// + /// 조회할 사용자 식별자. + /// 찾은 경우 해당 사용자에 대한 IClientConnection 인스턴스(없는 경우 null)를 설정하는出力 매개변수. + /// 해당 사용자 ID의 연결이 존재하면 true, 그렇지 않으면 false. public bool TryGet(string userId, out IClientConnection? connection) { var ok = _connections.TryGetValue(userId, out var conn); @@ -49,7 +72,11 @@ public bool TryGet(string userId, out IClientConnection? connection) /// /// 연결 상태를 확인합니다 + /// + /// 주어진 세션 ID에 해당하는 연결이 등록되어 있는지 확인합니다. /// + /// 확인할 세션 ID. + /// 해당 세션 ID에 연결이 존재하면 true, 그렇지 않으면 false. public bool IsConnected(string sessionId) { return _connections.ContainsKey(sessionId); @@ -57,7 +84,10 @@ public bool IsConnected(string sessionId) /// /// 활성 연결 수를 반환합니다 + /// + /// 현재 레지스트리에 저장된 활성 연결의 수를 반환합니다. /// + /// 현재 활성 연결(저장된 항목)의 총 개수. public int GetActiveConnectionCount() { return _connections.Count; diff --git a/ProjectVG.Application/Services/Session/IConnectionRegistry.cs b/ProjectVG.Application/Services/Session/IConnectionRegistry.cs index 0ff4d3f..655d5c2 100644 --- a/ProjectVG.Application/Services/Session/IConnectionRegistry.cs +++ b/ProjectVG.Application/Services/Session/IConnectionRegistry.cs @@ -7,22 +7,37 @@ public interface IConnectionRegistry { /// /// 연결을 등록합니다 - /// + /// +/// 지정된 사용자 ID에 대해 클라이언트 연결을 등록한다. +/// +/// 연결을 등록할 사용자 식별자(빈 문자열이나 null은 허용되지 않아야 함). void Register(string userId, IClientConnection connection); /// /// 연결을 해제합니다 - /// + /// +/// 지정된 사용자 ID에 연결된 클라이언트 연결을 등록 해제(제거)합니다. +/// +/// 연결을 제거할 사용자 식별자. void Unregister(string userId); /// /// 연결을 조회합니다 - /// + /// +/// 지정된 사용자 ID에 연결된 클라이언트 연결을 조회하려 시도합니다. +/// +/// 조회할 사용자 식별자. +/// 사용자에 연결이 존재하면 해당 연결을 출력합니다. 연결이 없으면 null이 설정됩니다. +/// 연결이 존재하면 true, 없으면 false. bool TryGet(string userId, out IClientConnection? connection); /// /// 연결 상태를 확인합니다 - /// + /// +/// 지정된 사용자가 현재 활성 연결을 보유하고 있는지 확인합니다. +/// +/// 확인할 사용자의 고유 식별자. +/// 해당 사용자가 하나 이상의 활성 연결을 가지고 있으면 true, 그렇지 않으면 false. bool IsConnected(string userId); /// diff --git a/ProjectVG.Application/Services/User/IUserService.cs b/ProjectVG.Application/Services/User/IUserService.cs index 4dfec3e..23acac3 100644 --- a/ProjectVG.Application/Services/User/IUserService.cs +++ b/ProjectVG.Application/Services/User/IUserService.cs @@ -4,18 +4,75 @@ namespace ProjectVG.Application.Services.Users { public interface IUserService { - Task CreateUserAsync(UserCreateCommand command); - Task DeleteUserAsync(Guid userId); + /// +/// 지정된 생성 명령을 사용하여 새 사용자를 비동기적으로 생성하고 생성된 사용자의 정보를 반환합니다. +/// +/// 생성에 필요한 사용자 정보(Username, Email, ProviderId, Provider)를 포함하는 명령 객체. +/// 생성된 사용자를 나타내는 UserDto를 포함하는 비동기 작업. +Task CreateUserAsync(UserCreateCommand command); + /// +/// 지정된 ID의 사용자를 비동기적으로 삭제합니다. +/// +/// 삭제할 사용자의 고유 식별자(Guid). +/// 삭제에 성공하면 true, 사용자를 찾을 수 없거나 삭제에 실패하면 false를 반환하는 비동기 작업. +Task DeleteUserAsync(Guid userId); - Task TryGetByIdAsync(Guid userId); - Task TryGetByUidAsync(string uid); - Task TryGetByUsernameAsync(string username); - Task TryGetByProviderAsync(string provider, string providerId); + /// +/// 지정한 사용자 ID로 사용자를 비동기적으로 조회합니다. +/// +/// 조회할 사용자의 고유 식별자(Guid). +/// 해당 ID의 사용자 정보를 담은 객체 또는 존재하지 않으면 null을 반환합니다. +Task TryGetByIdAsync(Guid userId); + /// +/// 지정한 외부 UID로 사용자를 비동기 조회합니다. +/// +/// 외부 제공자에서 발급된 사용자 식별자(UID). +/// +/// 조회된 사용자의 UserDto 객체를 반환합니다. 해당 UID를 가진 사용자가 없으면 null을 반환합니다. +/// +Task TryGetByUidAsync(string uid); + /// +/// 주어진 사용자명(username)에 해당하는 사용자를 비동기적으로 조회합니다. +/// +/// 조회할 사용자의 사용자명(대소문자 처리 정책은 호출자에 따라 달라질 수 있음). +/// +/// 조회된 사용자의 UserDto 인스턴스 또는 존재하지 않으면 null을 반환합니다. +/// +Task TryGetByUsernameAsync(string username); + /// +— 지정된 외부 인증 제공자(provider)와 공급자 ID(providerId)에 연결된 사용자를 비동기적으로 조회합니다. + +/** 외부 인증 제공자 이름(예: "google", "github"). + 해당 제공자에서 발급된 사용자의 고유 식별자. + 해당 제공자·ID에 매칭되는 를 반환합니다. 없으면 null을 반환합니다. */ +Task TryGetByProviderAsync(string provider, string providerId); - Task ExistsByIdAsync(Guid userId); - Task ExistsByUidAsync(string uid); - Task ExistsByEmailAsync(string email); - Task ExistsByUsernameAsync(string username); + /// +/// 지정한 사용자 ID를 가진 사용자가 존재하는지 비동기적으로 확인합니다. +/// +/// 확인할 사용자의 고유 식별자(Guid). +/// +/// 사용자가 존재하면 true, 존재하지 않으면 false를 반환하는 비동기 작업. +/// +Task ExistsByIdAsync(Guid userId); + /// +/// 지정된 UID를 가진 사용자가 존재하는지 비동기적으로 확인합니다. +/// +/// 확인할 사용자의 고유 식별자(UID). +/// 해당 UID를 가진 사용자가 존재하면 true, 그렇지 않으면 false를 반환하는 작업. +Task ExistsByUidAsync(string uid); + /// +/// 주어진 이메일을 가진 사용자가 시스템에 존재하는지 비동기적으로 확인합니다. +/// +/// 확인할 이메일 주소. +/// 해당 이메일을 가진 사용자가 존재하면 true, 그렇지 않으면 false를 반환합니다. +Task ExistsByEmailAsync(string email); + /// +/// 주어진 사용자명(username)을 가진 사용자가 존재하는지 비동기적으로 확인합니다. +/// +/// 존재 여부를 확인할 대상 사용자명. +/// 사용자가 존재하면 true, 그렇지 않으면 false를 반환하는 Task. +Task ExistsByUsernameAsync(string username); } public record UserCreateCommand( diff --git a/ProjectVG.Application/Services/User/UserService.cs b/ProjectVG.Application/Services/User/UserService.cs index 4eedeed..ef4136d 100644 --- a/ProjectVG.Application/Services/User/UserService.cs +++ b/ProjectVG.Application/Services/User/UserService.cs @@ -10,12 +10,24 @@ public class UserService : IUserService private readonly IUserRepository _userRepository; private readonly ILogger _logger; + /// + /// UserService의 인스턴스를 초기화합니다. + /// + /// + /// DI로 전달된 IUserRepository와 ILogger를 내부 필드에 할당하여 서비스의 의존성을 설정합니다. + /// public UserService(IUserRepository userRepository, ILogger logger) { _userRepository = userRepository; _logger = logger; } + /// + /// 새 사용자 계정을 생성하고 생성된 사용자의 DTO를 반환합니다. + /// + /// 생성할 사용자의 정보(이메일, 사용자명, Provider 및 ProviderId 등)를 담은 명령 객체. + /// 생성된 사용자 정보를 담은 객체. + /// 이메일 또는 사용자명이 이미 존재할 경우 각각 또는 코드와 함께 throw됩니다. public async Task CreateUserAsync(UserCreateCommand command) { if (await ExistsByEmailAsync(command.Email)) @@ -42,6 +54,12 @@ public async Task CreateUserAsync(UserCreateCommand command) return new UserDto(created); } + /// + /// 사용자를 소프트 삭제(계정 상태를 Deleted로 변경)하고 변경사항을 저장합니다. + /// + /// 삭제할 사용자의 식별자(Guid). + /// 삭제 작업이 성공하면 true를 반환합니다. + /// 해당 ID의 사용자를 찾을 수 없는 경우 발생합니다. public async Task DeleteUserAsync(Guid userId) { var user = await _userRepository.GetByIdAsync(userId); @@ -55,24 +73,45 @@ public async Task DeleteUserAsync(Guid userId) return true; } + /// + /// 지정된 ID의 사용자를 조회하여 UserDto를 반환합니다. + /// + /// 조회할 사용자의 GUID 식별자. + /// 사용자를 찾으면 해당 사용자의 UserDto, 찾지 못하면 null을 반환합니다. public async Task TryGetByIdAsync(Guid userId) { var user = await _userRepository.GetByIdAsync(userId); return user is null ? null : new UserDto(user); } + /// + /// 주어진 UID로 사용자를 조회하여 UserDto를 반환합니다. UID에 해당하는 사용자가 없으면 null을 반환합니다. + /// + /// 조회할 사용자의 고유 식별자(UID). + /// 해당 UID의 사용자를 나타내는 UserDto 인스턴스 또는 사용자가 없으면 null. public async Task TryGetByUidAsync(string uid) { var user = await _userRepository.GetByUIDAsync(uid); return user is null ? null : new UserDto(user); } + /// + /// 사용자명(username)으로 사용자를 조회하여 UserDto를 반환합니다. + /// + /// 조회할 사용자의 사용자명. + /// 사용자가 존재하면 해당 UserDto, 존재하지 않으면 null. public async Task TryGetByUsernameAsync(string username) { var user = await _userRepository.GetByUsernameAsync(username); return user is null ? null : new UserDto(user); } + /// + /// 지정된 외부 인증 공급자(provider)와 공급자 식별자(providerId)에 대응하는 사용자를 조회하여 UserDto를 반환합니다. + /// + /// 외부 인증 공급자 이름(예: "google", "github"). + /// 해당 공급자에서 발급된 사용자의 고유 식별자. + /// 일치하는 사용자가 있으면 해당 사용자의 UserDto, 없으면 null. public async Task TryGetByProviderAsync(string provider, string providerId) { var users = await _userRepository.GetAllAsync(); @@ -80,14 +119,47 @@ public async Task DeleteUserAsync(Guid userId) return user is null ? null : new UserDto(user); } - public Task ExistsByEmailAsync(string email) => ExistsAsync(() => _userRepository.GetByEmailAsync(email)); - public Task ExistsByUsernameAsync(string username) => ExistsAsync(() => _userRepository.GetByUsernameAsync(username)); - public Task ExistsByIdAsync(Guid userId) => ExistsAsync(() => _userRepository.GetByIdAsync(userId)); - public Task ExistsByUidAsync(string uid) => ExistsAsync(() => _userRepository.GetByUIDAsync(uid)); - - private static async Task ExistsAsync(Func> getter) where T : class + /// +/// 주어진 이메일을 가진 사용자가 존재하는지 여부를 비동기적으로 확인합니다. +/// +/// 조회할 사용자의 이메일 주소. +/// 해당 이메일을 가진 사용자가 존재하면 true, 그렇지 않으면 false를 반환하는 비동기 작업. +public Task ExistsByEmailAsync(string email) => ExistsAsync(() => _userRepository.GetByEmailAsync(email)); + /// +/// 지정한 사용자 이름(username)을 가진 사용자가 존재하는지 비동기적으로 확인합니다. +/// +/// 해당 사용자 이름이 존재하면 true, 아니면 false를 반환하는 Task. +public Task ExistsByUsernameAsync(string username) => ExistsAsync(() => _userRepository.GetByUsernameAsync(username)); + /// +/// 지정한 사용자 ID를 가진 사용자가 존재하는지 비동기적으로 확인합니다. +/// +/// 확인할 사용자의 GUID 식별자. +/// 해당 ID를 가진 사용자가 존재하면 true, 없으면 false를 반환하는 Task. +public Task ExistsByIdAsync(Guid userId) => ExistsAsync(() => _userRepository.GetByIdAsync(userId)); + /// +/// 지정된 UID를 가진 사용자가 존재하는지 비동기적으로 검사합니다. +/// +/// 확인할 사용자의 고유 식별자(UID). +/// 해당 UID를 가진 사용자가 존재하면 true, 그렇지 않으면 false를 반환하는 Task. +public Task ExistsByUidAsync(string uid) => ExistsAsync(() => _userRepository.GetByUIDAsync(uid)); + + /// + /// 지정된 비동기 조회 함수를 실행하여 결과가 null이 아닌지 확인하고, null이 아니면 true를 반환합니다. + /// + /// 존재 여부를 판별할 객체를 비동기로 반환하는 함수(Task<T?>). + /// 조회 결과가 null이 아니면 true, null이면 false. + private static async Task ExistsAsync(Func> getter) where T : class => await getter() is not null; + /// + /// 새 사용자에 사용할 고유한 UID를 생성하여 반환합니다. + /// + /// + /// 랜덤 UID를 생성한 뒤 저장소에 이미 존재하는지 검사하여 중복이 없을 때까지 반복합니다. + /// 최대 10회 시도 후에도 고유한 UID를 찾지 못하면 예외를 던집니다. + /// + /// 중복되지 않는 UID 문자열. + /// 최대 허용 시도 횟수(10회)를 초과한 경우. private async Task GenerateUniqueUIDAsync() { string uid; diff --git a/ProjectVG.Application/Services/WebSocket/IWebSocketManager.cs b/ProjectVG.Application/Services/WebSocket/IWebSocketManager.cs index 1fea80f..85ce56b 100644 --- a/ProjectVG.Application/Services/WebSocket/IWebSocketManager.cs +++ b/ProjectVG.Application/Services/WebSocket/IWebSocketManager.cs @@ -6,22 +6,39 @@ public interface IWebSocketManager { /// /// WebSocket 연결을 생성하고 초기화합니다 - /// + /// +/// 지정한 사용자(userId)에 대한 WebSocket 연결을 비동기적으로 생성하고 초기화합니다. +/// +/// 연결을 생성할 대상 사용자 식별자(비어있거나 null일 수 없습니다). +/// 생성된 연결의 식별자(연결 ID). 이후 SendAsync, DisconnectAsync, IsSessionActive 등의 호출에 사용됩니다. Task ConnectAsync(string userId); /// /// WebSocket 메시지를 전송합니다 - /// + /// +/// 지정한 사용자의 WebSocket 연결로 WebSocketMessage를 비동기 전송합니다. +/// +/// 메시지를 수신할 대상 사용자의 식별자. +/// 전송할 WebSocketMessage 객체. +/// 전송 작업이 완료될 때까지 대기하는 비동기 작업. Task SendAsync(string userId, WebSocketMessage message); /// /// WebSocket 연결을 종료합니다 - /// + /// +/// 지정된 사용자(userId)에 대한 WebSocket 연결을 비동기적으로 종료합니다. +/// +/// 연결을 종료할 대상 사용자의 고유 식별자(널이 아닌 값). +/// 연결 해제가 완료될 때까지 대기하는 Task. Task DisconnectAsync(string userId); /// /// 세션이 활성 상태인지 확인합니다 - /// + /// +/// 지정한 사용자(userId)에 대한 WebSocket 세션이 현재 활성 상태인지 확인합니다. +/// +/// 활성 상태를 확인할 대상 사용자 식별자. +/// 세션이 활성화되어 있으면 true, 그렇지 않으면 false. bool IsSessionActive(string userId); } } diff --git a/ProjectVG.Application/Services/WebSocket/WebSocketManager.cs b/ProjectVG.Application/Services/WebSocket/WebSocketManager.cs index cada268..40bbf59 100644 --- a/ProjectVG.Application/Services/WebSocket/WebSocketManager.cs +++ b/ProjectVG.Application/Services/WebSocket/WebSocketManager.cs @@ -12,6 +12,12 @@ public class WebSocketManager : IWebSocketManager private readonly IConnectionRegistry _connectionRegistry; private readonly ISessionStorage _sessionStorage; + /// + /// WebSocketManager 인스턴스를 초기화합니다. + /// + /// + /// 로깅, 연결 레지스트리 및 세션 저장소에 대한 의존성을 주입하여 이 매니저를 사용 가능한 상태로 설정합니다. + /// public WebSocketManager( ILogger logger, IConnectionRegistry connectionRegistry, @@ -22,6 +28,11 @@ public WebSocketManager( _sessionStorage = sessionStorage; } + /// + /// 지정된 사용자 ID로 새로운 WebSocket 세션 정보를 저장하고 해당 사용자 ID를 반환합니다. + /// + /// 세션 식별 및 연결 대상으로 사용할 사용자 고유 ID. + /// 생성된 세션의 식별자(입력한 userId와 동일). public async Task ConnectAsync(string userId) { _logger.LogInformation("새 WebSocket 세션 생성: {UserId}", userId); @@ -35,6 +46,12 @@ await _sessionStorage.CreateAsync(new SessionInfo { return userId; } + /// + /// 지정된 사용자에게 WebSocket 메시지를 JSON 텍스트로 직렬화하여 비동기 전송합니다. + /// + /// 메시지 수신 대상 사용자 ID. + /// 전송할 WebSocket 메시지 객체. + /// 메시지 전송이 완료될 때까지 대기하는 비동기 작업. public async Task SendAsync(string userId, WebSocketMessage message) { var json = JsonSerializer.Serialize(message); @@ -42,6 +59,12 @@ public async Task SendAsync(string userId, WebSocketMessage message) _logger.LogDebug("WebSocket 메시지 전송: {UserId}, 타입: {MessageType}", userId, message.Type); } + /// + /// 지정한 사용자 ID에 대응하는 활성 WebSocket 연결으로 텍스트 메시지를 비동기 전송합니다. + /// + /// 메시지 수신 대상의 사용자 ID. + /// 전송할 텍스트 메시지 내용. + /// 메시지 전송이 완료될 때까지 대기하는 Task. 대상 연결이 없으면 메시지는 전송되지 않고 바로 완료됩니다. public async Task SendTextAsync(string userId, string text) { if (_connectionRegistry.TryGet(userId, out var connection) && connection != null) { @@ -53,6 +76,12 @@ public async Task SendTextAsync(string userId, string text) } } + /// + /// 지정된 사용자에게 바이너리 데이터를 비동기적으로 전송합니다. + /// + /// 수신자 식별자(연결 조회에 사용). 활성 연결이 없으면 전송되지 않습니다. + /// 전송할 바이너리 데이터. + /// 전송 작업이 완료될 때까지 대기하는 비동기 작업. public async Task SendBinaryAsync(string userId, byte[] data) { if (_connectionRegistry.TryGet(userId, out var connection) && connection != null) { @@ -64,6 +93,11 @@ public async Task SendBinaryAsync(string userId, byte[] data) } } + /// + /// 지정한 사용자(userId)에 대한 WebSocket 연결을 등록 해제합니다. + /// + /// 등록 해제할 사용자의 식별자. + /// 작업이 즉시 완료된 완료된 . public Task DisconnectAsync(string userId) { _connectionRegistry.Unregister(userId); @@ -71,6 +105,11 @@ public Task DisconnectAsync(string userId) return Task.CompletedTask; } + /// + /// 지정한 사용자 ID가 현재 활성 WebSocket 세션(연결)을 보유하고 있는지 여부를 반환합니다. + /// + /// 확인할 사용자의 고유 식별자. + /// 사용자가 연결되어 있으면 true, 그렇지 않으면 false. public bool IsSessionActive(string userId) { return _connectionRegistry.IsConnected(userId); diff --git a/ProjectVG.Common/Constants/ErrorCodes.cs b/ProjectVG.Common/Constants/ErrorCodes.cs index 499e620..bfd2da5 100644 --- a/ProjectVG.Common/Constants/ErrorCodes.cs +++ b/ProjectVG.Common/Constants/ErrorCodes.cs @@ -178,6 +178,11 @@ public static class ErrorCodeExtensions { ErrorCode.RESOURCE_QUOTA_EXCEEDED, "리소스 할당량을 초과했습니다" } }; + /// + /// 주어진 ErrorCode에 해당하는 사용자용 한국어 메시지를 반환합니다. + /// + /// 메시지를 조회할 에러 코드. + /// 해당 코드에 매핑된 한국어 메시지. 매핑이 없으면 INTERNAL_SERVER_ERROR에 대한 메시지를 반환합니다. public static string GetMessage(this ErrorCode errorCode) { return _errorMessages.TryGetValue(errorCode, out var message) ? message : ErrorCode.INTERNAL_SERVER_ERROR.GetMessage(); diff --git a/ProjectVG.Common/Constants/LLMModelInfo.cs b/ProjectVG.Common/Constants/LLMModelInfo.cs index 18f0b0f..bd0ccc3 100644 --- a/ProjectVG.Common/Constants/LLMModelInfo.cs +++ b/ProjectVG.Common/Constants/LLMModelInfo.cs @@ -225,6 +225,11 @@ public static class DefaultSettings public const float DefaultTemperature = 0.7f; } + /// + /// 지정한 모델 이름에 대응하는 입력 비용 상수(Input)를 반환합니다. + /// + /// 비용을 조회할 모델의 이름(예: 클래스에 정의된 각 모델의 Name 값). + /// 해당 모델의 입력 비용 상수 값을 반환합니다. 지정한 이름과 일치하는 모델이 없으면 기본값(GPT4oMini)의 입력 비용을 반환합니다. public static double GetInputCost(string model) { return model switch @@ -253,6 +258,11 @@ public static double GetInputCost(string model) }; } + /// + /// 지정한 모델의 출력(output) 비용(총 비용 단위)을 반환합니다. + /// + /// 비용을 조회할 모델 이름(예: "gpt-4o-mini"). + /// 해당 모델의 출력 비용(double). 알려지지 않은 모델 이름이 전달되면 기본값으로 GPT4oMini의 출력 비용을 반환합니다. public static double GetOutputCost(string model) { return model switch @@ -281,16 +291,34 @@ public static double GetOutputCost(string model) }; } + /// + /// 지정한 모델의 입력 토큰당 비용(달러 단위)을 반환합니다. + /// + /// 비용을 조회할 모델의 식별자(예: "gpt-4o-mini"). + /// 토큰 1개당 입력 비용(USD). public static double GetInputCostPerToken(string model) { return GetInputCost(model) / COST_CALCULATION_FACTOR; } + /// + /// 지정한 모델의 출력(output) 토큰당 비용을 반환합니다. + /// + /// 비용을 조회할 모델 이름(예: "gpt-4o-mini"). + /// 달러 기준 밀리센트 환산 후 토큰 하나당 비용을 나타내는 값. public static double GetOutputCostPerToken(string model) { return GetOutputCost(model) / COST_CALCULATION_FACTOR; } + /// + /// 지정한 모델의 캐시된 입력 비용을 반환합니다. + /// + /// + /// 모델이 캐시된 입력 비용을 명시적으로 정의한 경우 해당 값을 반환하고, 정의되어 있지 않으면 해당 모델의 입력 비용의 10%를 반환합니다. + /// + /// 조회할 모델의 이름(예: 클래스에 정의된 모델 이름 상수). + /// 모델의 캐시된 입력 비용을 나타내는 값. public static double GetCachedInputCost(string model) { return model switch @@ -312,6 +340,13 @@ public static double GetCachedInputCost(string model) }; } + /// + /// 지정한 모델과 토큰 수에 따라 입력(프롬프트) 비용과 출력(완성) 비용을 계산하여 합산한 총 비용을 반환합니다. + /// + /// 비용 계산에 사용할 모델 식별자(예: "gpt-4o-mini"). + /// 프롬프트(입력)에 사용된 토큰 수. + /// 완성(출력)에 사용된 토큰 수. + /// 프롬프트와 출력에 대해 각각 소수점 올림(ceil)한 값을 합산한 총 비용(함수의 단위로 표시된 비용 값). public static double CalculateCost(string model, int promptTokens, int completionTokens) { var inputCostPerToken = GetInputCostPerToken(model); diff --git a/ProjectVG.Common/Constants/TTSCostInfo.cs b/ProjectVG.Common/Constants/TTSCostInfo.cs index 0000e19..ca26369 100644 --- a/ProjectVG.Common/Constants/TTSCostInfo.cs +++ b/ProjectVG.Common/Constants/TTSCostInfo.cs @@ -7,11 +7,25 @@ public static class TTSCostInfo private const double MILLICENTS_PER_DOLLAR = 100_000.0; private const double TTS_COST_PER_SECOND = TTS_CREDITS_PER_SECOND / TTS_CREDITS_PER_DOLLAR * MILLICENTS_PER_DOLLAR; + /// + /// TTS(텍스트 음성 변환) 1초당 비용을 밀리센트 단위로 반환합니다. + /// + /// 클래스 내부 상수로 미리 계산된 1초당 비용(밀리센트 단위). public static double GetTTSCostPerSecond() { return TTS_COST_PER_SECOND; } + /// + /// 주어진 길이(초)에 대한 TTS 비용을 계산합니다. + /// + /// + /// 입력된 재생 시간은 소수점 첫째 자리(0.1초)에서 올림되어 반올림되지 않고 항상 올림됩니다(예: 1.01s → 1.1s). + /// 해당 반올림된 지속시간에 내부 정의된 초당 TTS 비용을 곱한 뒤 결과를 올림하여 반환합니다. + /// 반환값은 millicents(1 달러 = 100,000 millicents) 단위의 비용입니다. + /// + /// 비용을 계산할 재생 시간(초). + /// 계산된 총 비용(밀리센트 단위)의 올림값을 나타내는 double. public static double CalculateTTSCost(double durationInSeconds) { var roundedDuration = Math.Ceiling(durationInSeconds * 10) / 10.0; diff --git a/ProjectVG.Common/Exceptions/AuthenticationException.cs b/ProjectVG.Common/Exceptions/AuthenticationException.cs index e881423..2addb09 100644 --- a/ProjectVG.Common/Exceptions/AuthenticationException.cs +++ b/ProjectVG.Common/Exceptions/AuthenticationException.cs @@ -5,21 +5,44 @@ namespace ProjectVG.Common.Exceptions /// public class AuthenticationException : ProjectVGException { + /// + /// 인증 실패를 나타내는 AuthenticationException 인스턴스를 생성합니다. 이 생성자는 기본 HTTP 상태 코드 401을 사용합니다. + /// + /// 해당 예외를 식별하는 오류 코드. public AuthenticationException(Constants.ErrorCode errorCode) : base(errorCode, 401) { } + /// + /// 지정된 에러 코드와 사용자 지정 메시지로 인증 실패를 나타내는 예외를 생성합니다. 생성된 예외의 HTTP 상태 코드는 401(권한 없음)으로 설정됩니다. + /// + /// 예외에 대응되는 내부 에러 코드. + /// 사용자에게 전달할 상세 메시지. public AuthenticationException(Constants.ErrorCode errorCode, string customMessage) : base(errorCode, customMessage, 401) { } + /// + /// 인증 실패를 나타내는 예외를 생성합니다. 내부 예외를 포함하며 HTTP 상태 코드는 401(Unauthorized)으로 고정됩니다. + /// + /// 발생한 오류를 나타내는 상수형 오류 코드. + /// 원인이 되는 내부 예외 (있을 경우). public AuthenticationException(Constants.ErrorCode errorCode, Exception innerException) : base(errorCode, innerException, 401) { } + /// + /// 인증 실패를 나타내는 AuthenticationException 인스턴스를 생성합니다. + /// + /// + /// 지정한 오류 코드, 사용자 정의 메시지 및 내부 예외로 예외를 초기화하고 HTTP 상태 코드를 401(Unauthorized)로 설정합니다. + /// + /// 프로젝트 고유의 오류 코드. + /// 클라이언트에 표시하거나 로깅에 사용할 사용자 정의 메시지. + /// 원인이 되는 내부 예외(있을 경우). public AuthenticationException(Constants.ErrorCode errorCode, string customMessage, Exception innerException) : base(errorCode, customMessage, innerException, 401) { diff --git a/ProjectVG.Common/Exceptions/ValidationException.cs b/ProjectVG.Common/Exceptions/ValidationException.cs index ef205a1..fec574d 100644 --- a/ProjectVG.Common/Exceptions/ValidationException.cs +++ b/ProjectVG.Common/Exceptions/ValidationException.cs @@ -6,18 +6,37 @@ public class ValidationException : ProjectVGException { public List ValidationErrors { get; } + /// + /// 지정된 오류 코드를 사용해 새 ValidationException 인스턴스를 생성합니다. + /// + /// 예외의 원인이 되는 오류 코드; 예외 메시지는 해당 코드의 GetMessage() 결과로 설정됩니다. public ValidationException(ErrorCode errorCode) : base(errorCode, errorCode.GetMessage(), 400) { ValidationErrors = new List(); } + /// + /// 지정된 오류 코드와 검증 결과 목록으로 ValidationException 인스턴스를 생성합니다. + /// + /// 예외에 대응되는 ErrorCode. 이 코드의 메시지(errorCode.GetMessage())가 예외 메시지로 사용됩니다. + /// 발생한 검증 결과(ValidationResult)의 목록으로, 예외의 ValidationErrors 프로퍼티에 그대로 할당됩니다. + /// + /// 생성자는 상위(ProjectVGException) 생성자에 HTTP 상태 코드 400과 errorCode의 메시지를 전달합니다. + /// validationErrors는 null일 수 있으며(호출자가 전달한 값을 그대로 사용), 예외의 ValidationErrors에 설정됩니다. + /// public ValidationException(ErrorCode errorCode, List validationErrors) : base(errorCode, errorCode.GetMessage(), 400) { ValidationErrors = validationErrors; } + /// + /// 지정한 오류 코드와 메시지, 검증 결과 목록으로 새로운 ValidationException 인스턴스를 생성합니다. + /// + /// 예외에 대응되는 ErrorCode. + /// 예외 메시지로 사용될 문자열. + /// 검증 실패 정보를 담은 ValidationResult 목록. ValidationErrors 프로퍼티에 할당됩니다. public ValidationException(ErrorCode errorCode, string message, List validationErrors) : base(errorCode, message, 400) { diff --git a/ProjectVG.Common/Models/Session/IClientConnection.cs b/ProjectVG.Common/Models/Session/IClientConnection.cs index 88b4c3a..5ed66e2 100644 --- a/ProjectVG.Common/Models/Session/IClientConnection.cs +++ b/ProjectVG.Common/Models/Session/IClientConnection.cs @@ -4,8 +4,18 @@ public interface IClientConnection { string UserId { get; } DateTime ConnectedAt { get; } - Task SendTextAsync(string message); - Task SendBinaryAsync(byte[] data); + /// +/// 클라이언트에 텍스트 메시지를 비동기적으로 전송합니다. +/// +/// 전송할 텍스트 메시지(널이 아님). +/// 전송 작업이 완료될 때까지 대기할 수 있는 Task. +Task SendTextAsync(string message); + /// +/// 연결된 클라이언트로 바이너리 데이터를 비동기적으로 전송합니다. +/// +/// 전송할 바이트 배열 페이로드. +/// 전송 작업의 완료를 나타내는 Task. +Task SendBinaryAsync(byte[] data); } } diff --git a/ProjectVG.Common/Utils/UidGenerator.cs b/ProjectVG.Common/Utils/UidGenerator.cs index 28e57ae..6f5d1f4 100644 --- a/ProjectVG.Common/Utils/UidGenerator.cs +++ b/ProjectVG.Common/Utils/UidGenerator.cs @@ -15,7 +15,13 @@ public class UidGenerator /// /// 랜덤 UID 생성 (길이 보장) + /// + /// 길이 16의 대문자(A–Z)와 숫자(0–9)로 구성된 암호학적으로 안전한 무작위 UID를 생성합니다. /// + /// + /// 내부적으로 System.Security.Cryptography.RandomNumberGenerator로 난수를 생성하여 각 바이트를 문자 집합의 인덱스로 매핑합니다. + /// + /// 생성된 16자 UID 문자열. public static string GenerateRandomUID() { var randomBytes = new byte[UID_LENGTH]; diff --git a/ProjectVG.Infrastructure/Auth/IRefreshTokenStorage.cs b/ProjectVG.Infrastructure/Auth/IRefreshTokenStorage.cs index f57585b..c766429 100644 --- a/ProjectVG.Infrastructure/Auth/IRefreshTokenStorage.cs +++ b/ProjectVG.Infrastructure/Auth/IRefreshTokenStorage.cs @@ -8,21 +8,35 @@ public interface IRefreshTokenStorage /// 리프레시 토큰 /// 사용자 ID /// 토큰 만료 시간 - /// 저장 성공 여부 + /// +/// 리프레시 토큰을 지정된 사용자와 만료 시각과 함께 저장한다. +/// +/// 저장할 리프레시 토큰 문자열. +/// 토큰에 연관된 사용자 식별자. +/// 토큰의 만료 시각. +/// 저장에 성공하면 true, 실패하면 false. Task StoreRefreshTokenAsync(string refreshToken, Guid userId, DateTime expiresAt); /// /// 리프레시 토큰에서 사용자 ID 추출 /// /// 사용자 ID를 추출할 리프레시 토큰 - /// 추출된 사용자 ID + /// +/// 주어진 리프레시 토큰에 연결된 사용자 ID를 비동기적으로 조회합니다. +/// +/// 조회할 리프레시 토큰 +/// 토큰에 연결된 사용자 ID를 반환합니다. 토큰이 존재하지 않거나 유효하지 않은 경우 null을 반환합니다. Task GetUserIdFromRefreshTokenAsync(string refreshToken); /// /// 리프레시 토큰 삭제 /// /// 삭제할 리프레시 토큰 - /// 삭제 성공 여부 + /// +/// 지정된 리프레시 토큰을 저장소에서 제거합니다. +/// +/// 제거할 리프레시 토큰 문자열. +/// 토큰이 존재하여 성공적으로 제거되면 true, 그렇지 않으면 false를 반환합니다. Task RemoveRefreshTokenAsync(string refreshToken); @@ -30,14 +44,24 @@ public interface IRefreshTokenStorage /// 리프레시 토큰 유효성 검사 /// /// 검사할 리프레시 토큰 - /// 유효성 검사 결과 + /// +/// 저장소에 있는 지정된 리프레시 토큰이 유효한지 비동기적으로 확인합니다. +/// +/// 검사할 리프레시 토큰 문자열. +/// +/// 토큰이 저장되어 있고 만료되지 않은 경우에만 true를 반환합니다. 그렇지 않으면 false를返します. +/// Task IsRefreshTokenValidAsync(string refreshToken); /// /// 리프레시 토큰의 만료 시간 조회 /// /// 만료 시간을 조회할 리프레시 토큰 - /// 토큰의 만료 시간 + /// +/// 지정된 리프레시 토큰의 만료 시각을 비동기적으로 조회합니다. +/// +/// 만료 시각을 조회할 리프레시 토큰 문자열. +/// 토큰의 만료 시각(DateTime) 또는 토큰이 존재하지 않거나 만료 정보가 없으면 null을 반환합니다. Task GetRefreshTokenExpiresAtAsync(string refreshToken); } } diff --git a/ProjectVG.Infrastructure/Auth/ITokenService.cs b/ProjectVG.Infrastructure/Auth/ITokenService.cs index cd7eaa9..0c7b3c4 100644 --- a/ProjectVG.Infrastructure/Auth/ITokenService.cs +++ b/ProjectVG.Infrastructure/Auth/ITokenService.cs @@ -8,42 +8,66 @@ public interface ITokenService /// 토큰 생성 /// /// 사용자 ID - /// 생성된 토큰 정보 + /// +/// 지정된 사용자 ID에 대한 액세스 토큰과 리프레시 토큰을 생성합니다. +/// +/// 토큰을 생성할 대상 사용자의 식별자(Guid). +/// 생성된 액세스 토큰 및 리프레시 토큰을 포함한 TokenResponse. Task GenerateTokensAsync(Guid userId); /// /// 리프레시 토큰으로 액세스 토큰 갱신 /// /// 리프레시 토큰 - /// 갱신된 토큰 정보 + /// +/// 주어진 리프레시 토큰으로 새 액세스(및 리프레시) 토큰을 발급합니다. +/// +/// 갱신에 사용할 리프레시 토큰 문자열. +/// 성공하면 새로 발급된 TokenResponse 객체를 반환합니다. 리프레시 토큰이 유효하지 않거나 갱신할 수 없으면 null을 반환합니다. Task RefreshAccessTokenAsync(string refreshToken); /// /// 리프레시 토큰 무효화 /// /// 무효화할 리프레시 토큰 - /// 무효화 성공 여부 + /// +/// 지정한 리프레시 토큰을 무효화하여 이후 재사용을 방지합니다. +/// +/// 무효화할 리프레시 토큰 문자열. +/// 무효화에 성공하면 true, 토큰이 존재하지 않거나 무효화에 실패하면 false. Task RevokeRefreshTokenAsync(string refreshToken); /// /// 리프레시 토큰 유효성 검사 /// /// 검사할 리프레시 토큰 - /// 유효성 검사 결과 + /// +/// 주어진 리프레시 토큰이 현재 유효한지 확인합니다. +/// +/// 검증할 리프레시 토큰 문자열. +/// 토큰이 유효하면 true, 무효(만료·취소·변조 등)하면 false를 반환합니다. Task ValidateRefreshTokenAsync(string refreshToken); /// /// 액세스 토큰 유효성 검증 /// /// 검증할 액세스 토큰 - /// 토큰 유효성 + /// +/// 주어진 액세스 토큰의 유효성을 검증합니다. +/// +/// 검증할 액세스 토큰(JWT 등)의 문자열 표현. +/// 토큰이 유효하면 true, 유효하지 않거나 만료되었으면 false를 반환합니다. Task ValidateAccessTokenAsync(string accessToken); /// /// 토큰에서 사용자 ID 추출 /// /// 사용자 ID를 추출할 토큰 - /// 추출된 사용자 ID + /// +/// 토큰에서 사용자 식별자(Guid)를 추출합니다. +/// +/// 사용자 ID를 포함하고 있는 유효한 액세스 또는 리프레시 토큰 문자열. +/// 추출된 사용자 ID(Guid). 토큰이 유효하지 않거나 ID를 추출할 수 없으면 null을 반환합니다. Task GetUserIdFromTokenAsync(string token); } } diff --git a/ProjectVG.Infrastructure/Auth/InMemoryRefreshTokenStorage.cs b/ProjectVG.Infrastructure/Auth/InMemoryRefreshTokenStorage.cs index 1fd8b2d..0f33e2d 100644 --- a/ProjectVG.Infrastructure/Auth/InMemoryRefreshTokenStorage.cs +++ b/ProjectVG.Infrastructure/Auth/InMemoryRefreshTokenStorage.cs @@ -9,11 +9,24 @@ public class InMemoryRefreshTokenStorage : IRefreshTokenStorage private readonly ILogger _logger; private readonly object _lock = new object(); + /// + /// InMemoryRefreshTokenStorage의 새 인스턴스를 초기화합니다. + /// + /// + /// 내부에서 토큰 관리 중 발생하는 로그를 기록하기 위해 로거를 설정합니다. + /// public InMemoryRefreshTokenStorage(ILogger logger) { _logger = logger; } + /// + /// 지정된 리프레시 토큰을 주어진 사용자 ID 및 만료 시간으로 메모리 저장소에 저장하거나 갱신합니다. + /// + /// 저장할 리프레시 토큰 문자열. + /// 토큰에 연결할 사용자 식별자(Guid). + /// 토큰의 만료 시각(UTC 기준). UTC로 제공되는 것이 권장됩니다. + /// 저장이 성공하면 true, 예외 발생 등으로 실패하면 false를 반환합니다. public Task StoreRefreshTokenAsync(string refreshToken, Guid userId, DateTime expiresAt) { try @@ -33,6 +46,17 @@ public Task StoreRefreshTokenAsync(string refreshToken, Guid userId, DateT } } + /// + /// 주어진 리프레시 토큰에 연관된 사용자 ID를 반환합니다. + /// + /// + /// 토큰이 존재하고 만료되지 않은 경우 해당 사용자의 를 반환합니다. + /// 만료된 토큰은 내부 저장소에서 제거됩니다. 시간 비교는 를 사용합니다. + /// 오류 또는 토큰 미존재/만료 시에는 null을 반환합니다. + /// + /// + /// 토큰이 유효하면 사용자 ID(Guid), 그렇지 않으면 null을 반환합니다. + /// public Task GetUserIdFromRefreshTokenAsync(string refreshToken) { try @@ -66,6 +90,14 @@ public Task StoreRefreshTokenAsync(string refreshToken, Guid userId, DateT } } + /// + /// 지정한 리프레시 토큰을 메모리 저장소에서 제거합니다. + /// + /// 제거할 리프레시 토큰 문자열(키). + /// + /// 토큰이 존재하여 제거되면 true, 토큰이 없거나 제거에 실패하면 false를 반환하는 비동기 작업(Task). + /// 예외가 발생하면 내부에서 처리하고 false를 반환합니다. + /// public Task RemoveRefreshTokenAsync(string refreshToken) { try @@ -87,6 +119,17 @@ public Task RemoveRefreshTokenAsync(string refreshToken) } } + /// + /// 지정한 리프레시 토큰이 존재하고 만료되지 않았는지 비동기적으로 확인합니다. + /// + /// 검사할 리프레시 토큰 문자열. + /// + /// 토큰이 존재하고 현재 UTC 시간 기준으로 만료되지 않았으면 true, 그렇지 않거나 오류가 발생하면 false를 반환합니다. + /// + /// + /// 만료된 토큰은 내부 저장소에서 제거됩니다. 내부 동기화를 위해 잠금(_lock)을 사용하며 시간 비교는 을 기준으로 합니다. + /// 예외는 내부에서 처리되어 로그로 남기고 false를 반환합니다. + /// public Task IsRefreshTokenValidAsync(string refreshToken) { try @@ -115,6 +158,14 @@ public Task IsRefreshTokenValidAsync(string refreshToken) } } + /// + /// 지정한 리프레시 토큰의 만료 시각을 반환합니다. + /// + /// 조회할 리프레시 토큰 문자열. + /// + /// 토큰이 존재하고 만료되지 않았다면 해당 토큰의 만료 시각을 반환합니다(저장된 값, UTC 기준). + /// 토큰이 없거나 만료된 경우 null을 반환하며, 만료된 토큰은 내부 저장소에서 제거됩니다. + /// public Task GetRefreshTokenExpiresAtAsync(string refreshToken) { try diff --git a/ProjectVG.Infrastructure/Auth/JwtProvider.cs b/ProjectVG.Infrastructure/Auth/JwtProvider.cs index 713b1ee..d905344 100644 --- a/ProjectVG.Infrastructure/Auth/JwtProvider.cs +++ b/ProjectVG.Infrastructure/Auth/JwtProvider.cs @@ -7,10 +7,33 @@ namespace ProjectVG.Infrastructure.Auth { public interface IJwtProvider { - string GenerateAccessToken(Guid userId); - string GenerateRefreshToken(Guid userId); - ClaimsPrincipal? ValidateToken(string token); - string? GetUserIdFromToken(string token); + /// +/// 주어진 사용자 ID로 액세스용 JWT를 생성하여 직렬화된 토큰 문자열을 반환합니다. +/// +/// 토큰에 포함할 사용자 식별자(Guid). +/// 생성된 액세스 토큰의 직렬화된 문자열. 토큰에는 NameIdentifier 클레임(사용자 ID)과 `token_type="access"`가 포함됩니다. +string GenerateAccessToken(Guid userId); + /// +/// 지정된 사용자 ID에 대한 만료된 리프레시 토큰(서명된 JWT)을 생성합니다. +/// +/// 토큰에 포함할 사용자 식별자(Guid). +/// +/// 서명된 JWT 문자열. 토큰에는 NameIdentifier 클레임(userId)과 "refresh"인 token_type 클레임이 포함되며, +/// 제공된 설정에 따른 발급자·대상자·리프레시 토큰 만료 시간이 적용됩니다. +/// +string GenerateRefreshToken(Guid userId); + /// +/// 주어진 JWT 문자열의 서명, 발행자, 대상, 유효기간을 검증하고 검증에 성공하면 해당 토큰의 클레임을 담은 을 반환합니다. +/// +/// 검증할 JWT 문자열(액세스 토큰 또는 리프레시 토큰). +/// 토큰이 유효하면 해당 토큰의 ; 유효하지 않거나 검증 실패 시 null. +ClaimsPrincipal? ValidateToken(string token); + /// +/// JWT에서 사용자 ID(NameIdentifier 클레임)를 추출하여 반환합니다. +/// +/// 검증할 JWT 문자열. +/// 토큰이 유효하고 NameIdentifier 클레임이 존재하면 해당 사용자 ID 문자열을 반환하고, 그렇지 않으면 null을 반환합니다. +string? GetUserIdFromToken(string token); } /// @@ -24,6 +47,14 @@ public class JwtProvider : IJwtProvider private readonly int _accessTokenExpirationMinutes; private readonly int _refreshTokenExpirationMinutes; + /// + /// JwtProvider 인스턴스를 초기화합니다. + /// + /// 토큰 서명에 사용할 비밀 키(대칭 키 문자열). + /// 발급자(issuer)로 사용될 식별자. + /// 토큰의 대상(audience)으로 사용할 식별자. + /// 액세스 토큰 만료 시간(분 단위). + /// 리프레시 토큰 만료 시간(분 단위). public JwtProvider(string jwtKey, string issuer, string audience, int accessTokenExpirationMinutes, int refreshTokenExpirationMinutes) { _jwtKey = jwtKey; @@ -37,7 +68,11 @@ public JwtProvider(string jwtKey, string issuer, string audience, int accessToke /// 액세스 토큰 생성 /// /// 사용자 ID - /// 생성된 토큰 + /// + /// 지정한 사용자 ID로 만료 시간과 발급자/대상자를 포함한 액세스 JWT를 생성하여 직렬화된 문자열로 반환합니다. + /// + /// 토큰에 포함될 사용자 식별자(ClaimTypes.NameIdentifier로 저장됨). + /// 생성된 액세스 토큰의 직렬화된 JWT 문자열(내부적으로 `token_type` 클레임은 "access"로 설정되고 설정된 만료 시간이 적용됨). public string GenerateAccessToken(Guid userId) { var tokenHandler = new JwtSecurityTokenHandler(); @@ -70,7 +105,11 @@ public string GenerateAccessToken(Guid userId) /// 리프레시 토큰 생성 /// /// 사용자 ID - /// 생성된 토큰 + /// + /// 지정한 사용자 ID로 만료 시간이 설정된 refresh JWT를 생성합니다. + /// + /// 토큰이 발급될 사용자의 GUID (토큰의 NameIdentifier 클레임에 저장됨). + /// 생성된 refresh 토큰 문자열 (토큰에는 `token_type` 클레임이 "refresh"로 설정되어 있음). public string GenerateRefreshToken(Guid userId) { var tokenHandler = new JwtSecurityTokenHandler(); @@ -99,7 +138,11 @@ public string GenerateRefreshToken(Guid userId) /// 토큰 검증 /// /// 검증할 토큰 - /// 검증된 토큰의 클레임 정보 + /// + /// 지정된 JWT를 검증하고 해당 토큰의 클레임을 포함한 ClaimsPrincipal을 반환합니다. + /// + /// 검증할 JWT 문자열. + /// 토큰이 유효하면 토큰의 클레임을 포함한 ; 유효하지 않거나 검증에 실패하면 null. public ClaimsPrincipal? ValidateToken(string token) { var tokenHandler = new JwtSecurityTokenHandler(); @@ -133,7 +176,11 @@ public string GenerateRefreshToken(Guid userId) /// 토큰에서 사용자 ID 추출 /// /// 사용자 ID를 추출할 토큰 - /// 추출된 사용자 ID + /// + /// JWT에서 사용자 ID(NameIdentifier 클레임)를 추출하여 반환합니다. + /// + /// 검증할 JWT 문자열(액세스 또는 리프레시 토큰). + /// 토큰이 유효하고 NameIdentifier 클레임이 있으면 해당 사용자 ID 문자열, 그렇지 않으면 null. public string? GetUserIdFromToken(string token) { var principal = ValidateToken(token); diff --git a/ProjectVG.Infrastructure/Auth/JwtService.cs b/ProjectVG.Infrastructure/Auth/JwtService.cs index 913bb3d..e96f842 100644 --- a/ProjectVG.Infrastructure/Auth/JwtService.cs +++ b/ProjectVG.Infrastructure/Auth/JwtService.cs @@ -9,11 +9,20 @@ public class JwtService { private readonly string _secretKey; + /// + /// JwtService 인스턴스를 초기화합니다. + /// + /// JWT 서명에 사용할 대칭 비밀키(UTF-8로 인코딩하여 HMAC-SHA256 서명에 사용). 유효한 비밀키 문자열이어야 합니다. public JwtService(string secretKey) { _secretKey = secretKey; } + /// + /// 지정된 사용자 ID를 기반으로 서명된 JWT(JSON Web Token)를 생성하여 반환합니다. + /// + /// 토큰의 NameIdentifier 클레임으로 포함할 사용자 식별자(예: 사용자 ID). + /// HMAC-SHA256로 서명되고 발행 시점으로부터 30분간 유효한 JWT 문자열. public string GenerateJwt(string userId) { var tokenHandler = new JwtSecurityTokenHandler(); diff --git a/ProjectVG.Infrastructure/Auth/RedisRefreshTokenStorage.cs b/ProjectVG.Infrastructure/Auth/RedisRefreshTokenStorage.cs index 47c6af2..18ca250 100644 --- a/ProjectVG.Infrastructure/Auth/RedisRefreshTokenStorage.cs +++ b/ProjectVG.Infrastructure/Auth/RedisRefreshTokenStorage.cs @@ -9,12 +9,22 @@ public class RedisRefreshTokenStorage : IRefreshTokenStorage private readonly ILogger _logger; private const string KeyPrefix = "refresh_token:"; + /// + /// Redis 기반으로 리프레시 토큰을 저장·관리하는 RedisRefreshTokenStorage 인스턴스를 생성합니다. + /// public RedisRefreshTokenStorage(IConnectionMultiplexer redis, ILogger logger) { _redis = redis; _logger = logger; } + /// + /// 주어진 리프레시 토큰을 Redis에 사용자 ID와 만료 시간(TTL)으로 저장합니다. + /// + /// 저장할 리프레시 토큰 문자열(키의 일부로 사용됨). + /// 토큰에 연관된 사용자 식별자(Guid)로, Redis 값으로 저장됩니다. + /// 토큰의 만료 시각(UTC). 현재 UTC 시각과의 차이를 TTL로 설정합니다. + /// 저장이 성공하면 true, 실패하거나 예외가 발생하면 false를 반환합니다. public async Task StoreRefreshTokenAsync(string refreshToken, Guid userId, DateTime expiresAt) { try @@ -32,6 +42,14 @@ public async Task StoreRefreshTokenAsync(string refreshToken, Guid userId, } } + /// + /// 주어진 리프레시 토큰에 연관된 사용자 ID를 Redis에서 조회하여 반환합니다. + /// + /// 조회할 원시 리프레시 토큰 문자열(키에는 내부적으로 "refresh_token:" 접두사가 붙습니다). + /// + /// 토큰이 존재하고 저장된 값이 유효한 GUID인 경우 해당 사용자 ID를 반환합니다. + /// 토큰이 없거나 값이 GUID로 파싱되지 않거나 조회 중 오류가 발생하면 null을 반환합니다. + /// public async Task GetUserIdFromRefreshTokenAsync(string refreshToken) { try @@ -54,6 +72,13 @@ public async Task StoreRefreshTokenAsync(string refreshToken, Guid userId, } } + /// + /// 지정된 리프레시 토큰에 대응하는 Redis 키를 삭제합니다. + /// + /// 삭제할 리프레시 토큰(키는 내부적으로 "refresh_token:{refreshToken}" 형태로 사용됩니다). + /// + /// 삭제가 성공적으로 수행되어 키가 제거되면 true를 반환합니다. 키가 존재하지 않거나(또는 내부 오류 발생 시) false를 반환합니다. + /// public async Task RemoveRefreshTokenAsync(string refreshToken) { try @@ -69,6 +94,13 @@ public async Task RemoveRefreshTokenAsync(string refreshToken) } } + /// + /// 지정된 리프레시 토큰이 저장소에 존재하는지(유효한지) 확인합니다. + /// + /// 접두사 없이 저장된 리프레시 토큰 문자열. + /// + /// 토큰이 존재하면 true, 존재하지 않거나(만료 포함) 확인 중 오류가 발생하면 false를 반환합니다. + /// public async Task IsRefreshTokenValidAsync(string refreshToken) { try @@ -84,6 +116,14 @@ public async Task IsRefreshTokenValidAsync(string refreshToken) } } + /// + /// 지정된 리프레시 토큰의 만료 시각을 조회합니다. + /// + /// 조회할 리프레시 토큰 문자열(저장된 키는 "refresh_token:{refreshToken}" 형식입니다). + /// + /// 토큰이 Redis에 존재하고 TTL이 설정되어 있으면 해당 TTL을 현재 UTC 시간에 더한 만료 시각(DateTime)을 반환합니다. + /// 존재하지 않거나 TTL이 없으면 null을 반환합니다. + /// public async Task GetRefreshTokenExpiresAtAsync(string refreshToken) { try diff --git a/ProjectVG.Infrastructure/Auth/TokenService.cs b/ProjectVG.Infrastructure/Auth/TokenService.cs index 1f88a78..2865f86 100644 --- a/ProjectVG.Infrastructure/Auth/TokenService.cs +++ b/ProjectVG.Infrastructure/Auth/TokenService.cs @@ -9,6 +9,9 @@ public class TokenService : ITokenService private readonly IRefreshTokenStorage _refreshTokenStorage; private readonly ILogger _logger; + /// + /// TokenService의 새 인스턴스를 생성하고 내부에서 사용할 JwtProvider, RefreshTokenStorage 및 Logger를 주입하여 초기화합니다. + /// public TokenService(IJwtProvider jwtProvider, IRefreshTokenStorage refreshTokenStorage, ILogger logger) { _jwtProvider = jwtProvider; @@ -16,6 +19,15 @@ public TokenService(IJwtProvider jwtProvider, IRefreshTokenStorage refreshTokenS _logger = logger; } + /// + /// 지정된 사용자 ID에 대해 액세스 토큰과 리프레시 토큰을 생성하고 리프레시 토큰을 저장한 뒤 토큰과 만료 시각을 반환합니다. + /// + /// 토큰을 발급할 대상 사용자의 고유 식별자(Guid). + /// + /// 생성된 액세스 토큰과 리프레시 토큰, 각각의 만료 시각을 포함한 . + /// 액세스 토큰 만료 시각은 생성 시점 기준 UTC +15분, 리프레시 토큰 만료 시각은 UTC +24시간으로 설정됩니다. + /// + /// 리프레시 토큰을 저장하지 못했을 경우 발생합니다. public async Task GenerateTokensAsync(Guid userId) { var accessToken = _jwtProvider.GenerateAccessToken(userId); @@ -40,6 +52,14 @@ public async Task GenerateTokensAsync(Guid userId) }; } + /// + /// 주어진 리프레시 토큰을 검증하고 유효하면 새로운 액세스 토큰을 발급하여 토큰 응답을 반환합니다. + /// + /// 클라이언트가 제공한 리프레시 토큰 문자열. + /// + /// 유효한 경우 새 액세스 토큰과 기존 리프레시 토큰 및 각각의 만료 시간을 담은 를 반환합니다. + /// 토큰이 형식상 유효하지 않거나 타입이 "refresh"가 아니거나 저장소 검증에 실패하면 null을 반환합니다. + /// public async Task RefreshAccessTokenAsync(string refreshToken) { var principal = _jwtProvider.ValidateToken(refreshToken); @@ -91,11 +111,21 @@ public async Task GenerateTokensAsync(Guid userId) }; } + /// + /// 지정한 리프레시 토큰을 저장소에서 제거(해제)합니다. + /// + /// 제거할 리프레시 토큰 문자열. + /// 토큰이 존재하여 성공적으로 제거되면 true, 그렇지 않으면 false. public async Task RevokeRefreshTokenAsync(string refreshToken) { return await _refreshTokenStorage.RemoveRefreshTokenAsync(refreshToken); } + /// + /// 주어진 문자열형 리프레시 토큰이 유효한 리프레시 토큰인지 검사합니다. + /// + /// 검사할 JWT 리프레시 토큰 문자열. + /// 토큰이 서명 및 클레임 검증에 통과하고 저장소에 유효한 토큰으로 존재하면 true, 그렇지 않으면 false를 반환합니다. public async Task ValidateRefreshTokenAsync(string refreshToken) { var principal = _jwtProvider.ValidateToken(refreshToken); @@ -113,6 +143,11 @@ public async Task ValidateRefreshTokenAsync(string refreshToken) return await _refreshTokenStorage.IsRefreshTokenValidAsync(refreshToken); } + /// + /// 주어진 JWT가 유효한 액세스 토큰인지 검증합니다. + /// + /// 검사할 JWT 문자열(액세스 토큰). + /// 토큰이 유효하고 내부 클레임 `token_type`이 `"access"`이면 true, 그렇지 않으면 false를 반환하는 비동기 작업. public Task ValidateAccessTokenAsync(string accessToken) { var principal = _jwtProvider.ValidateToken(accessToken); @@ -125,6 +160,13 @@ public Task ValidateAccessTokenAsync(string accessToken) return Task.FromResult(tokenType == "access"); } + /// + /// 전달된 JWT에서 사용자 ID를 추출하여 Guid로 반환합니다. + /// + /// 확인할 JWT 문자열. + /// + /// 토큰에서 추출된 사용자 ID(Guid). 토큰에 사용자 ID가 없거나 Guid로 변환할 수 없으면 null을 반환합니다. + /// public Task GetUserIdFromTokenAsync(string token) { var userId = _jwtProvider.GetUserIdFromToken(token); diff --git a/ProjectVG.Infrastructure/InfrastructureServiceCollectionExtensions.cs b/ProjectVG.Infrastructure/InfrastructureServiceCollectionExtensions.cs index 3b67602..37ea4d5 100644 --- a/ProjectVG.Infrastructure/InfrastructureServiceCollectionExtensions.cs +++ b/ProjectVG.Infrastructure/InfrastructureServiceCollectionExtensions.cs @@ -20,7 +20,13 @@ public static class InfrastructureServiceCollectionExtensions { /// /// Infrastructure 모듈 서비스 등록 + /// + /// 인프라스트럭처 관련 서비스를 DI 컨테이너에 등록합니다. /// + /// + /// 데이터베이스, 외부 API 클라이언트, 퍼시스턴스, 인증(JWT), Redis 및 OAuth2 관련 서비스를 순차적으로 등록합니다. + /// + /// 구성된 IServiceCollection 인스턴스 public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) { AddDatabaseServices(services, configuration); @@ -86,7 +92,16 @@ private static void AddExternalApiClients(IServiceCollection services, IConfigur /// /// 저장소 서비스 + /// + /// 인프라스트럭처의 영속성 관련 서비스들을 DI 컨테이너에 등록합니다. /// + /// + /// 등록 항목 및 수명주기: + /// - ICharacterRepository -> SqlServerCharacterRepository (Scoped) + /// - IConversationRepository -> SqlServerConversationRepository (Scoped) + /// - IUserRepository -> SqlServerUserRepository (Scoped) + /// - ISessionStorage -> InMemorySessionStorage (Singleton) + /// private static void AddPersistenceServices(IServiceCollection services) { services.AddScoped(); @@ -97,7 +112,20 @@ private static void AddPersistenceServices(IServiceCollection services) /// /// 인증 서비스 + /// + /// JWT 서명 키와 관련 인증 서비스를 DI 컨테이너에 등록합니다. /// + /// + /// 다음 순서로 JWT 설정을 구성하고 서비스들을 등록합니다: + /// - 서명 키를 우선순위로 환경변수 및 구성에서 조회: + /// Environment: JWT_SECRET_KEY, JWT_KEY 또는 구성값: JWT_SECRET_KEY, Jwt:SecretKey 등을 확인하며 기본값은 + /// "your-super-secret-jwt-key-here-minimum-32-characters"입니다. + /// - 값이 "${VAR_NAME}" 형태인 경우 내부 이름(VAR_NAME)으로 환경변수를 재조회하여 치환합니다. + /// - Jwt 설정(JwtSettings)을 구성에서 바인드하거나, 값이 없으면 조회한 키와 기본 Issuer/Audience("ProjectVG") 및 + /// 만료값(액세스 기본 15분, 리프레시 기본 1440분)으로 생성합니다. + /// - 구성된 JwtSettings를 싱글턴으로 등록하고, IJwtProvider(Scoped)와 ITokenService(Scoped)를 등록합니다. + /// - 동일한 서명 키로 JwtService를 싱글턴으로 등록합니다. + /// private static void AddAuthServices(IServiceCollection services, IConfiguration configuration) { // JWT 키를 여러 소스에서 찾기 @@ -136,7 +164,12 @@ private static void AddAuthServices(IServiceCollection services, IConfiguration /// /// OAuth2 서비스 + /// + /// "OAuth2" 구성 섹션을 읽어 OAuth2ProviderSettings에 바인딩하고 DI에 등록합니다. /// + /// + /// 앱 설정의 "OAuth2" 섹션을 OAuth2ProviderSettings 타입으로 구성 바인딩하여 서비스에서 해당 설정을 주입받아 사용할 수 있게 합니다. + /// private static void AddOAuth2Services(IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection("OAuth2")); @@ -144,7 +177,16 @@ private static void AddOAuth2Services(IServiceCollection services, IConfiguratio /// /// Redis 서비스 (개발 환경에서는 In-Memory 사용) + /// + /// Redis 관련 서비스를 DI 컨테이너에 등록합니다. /// + /// + /// - ASPNETCORE_ENVIRONMENT 환경 변수가 "Production"(대소문자 구분 없음)일 경우: + /// - 설정의 ConnectionStrings:Redis 또는 환경 변수 REDIS_CONNECTION_STRING 또는 기본값 "localhost:6379" 순으로 Redis 연결 문자열을 결정합니다. + /// - IConnectionMultiplexer를 싱글턴으로 등록하고, AbortOnConnectFail=false, ConnectRetry=5, ReconnectRetryPolicy=ExponentialRetry(5000) 옵션으로 연결합니다. + /// - IRefreshTokenStorage는 RedisRefreshTokenStorage로 스코프 등록합니다. + /// - Production이 아닐 경우 IRefreshTokenStorage는 InMemoryRefreshTokenStorage로 스코프 등록합니다. + /// private static void AddRedisServices(IServiceCollection services, IConfiguration configuration) { var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; diff --git a/ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs b/ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs index e71e281..016a0fd 100644 --- a/ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs +++ b/ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs @@ -32,6 +32,18 @@ public LLMClient(HttpClient httpClient, ILogger logger, IConfiguratio _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); } + /// + /// 지정된 LLM 요청을 원격 LLM 서비스로 전송하고 그 결과를 LLMResponse로 반환합니다. + /// + /// 전송할 채팅 요청(시스템 메시지, 사용자 메시지, 모델, 토큰/온도 설정 등)을 포함하는 LLMRequest 객체. + /// + /// 서비스의 응답을 파싱한 LLMResponse를 반환합니다. + /// - HTTP 응답이 실패일 경우: Success = false, ErrorMessage에 상태 코드가 설정된 응답을 반환합니다. + /// - 응답 파싱에 실패하면: Success = false, ErrorMessage = "응답을 파싱할 수 없습니다."를 반환합니다. + /// - 네트워크 연결 오류(HttpRequestException) 발생 시: 개발용 mock 성공 응답을 반환합니다 (Success = true, mock Id와 샘플 응답 포함). + /// - 요청 시간 초과(TaskCanceledException) 시: Success = false, ErrorMessage = "요청 시간이 초과되었습니다."를 반환합니다. + /// - 기타 예외 발생 시: Success = false, ErrorMessage = "요청 처리 중 오류가 발생했습니다."를 반환합니다. + /// public async Task SendRequestAsync(LLMRequest request) { try diff --git a/ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs b/ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs index 3d3c9ed..f9df066 100644 --- a/ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs +++ b/ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs @@ -6,32 +6,59 @@ public interface IMemoryClient { /// /// 텍스트를 자동 분류(Episodic/Semantic)하여 삽입한다. - /// + /// +/// 입력된 텍스트를 받아 자동으로 에피소드(Episodic) 또는 시맨틱(Semantic)으로 분류해 비동기적으로 저장합니다. +/// +/// 저장할 메모리의 내용과 메타데이터(텍스트, 사용자 ID, 화자, 감정, 컨텍스트 등)를 포함한 요청 객체. +/// 삽입된 메모리의 식별자·타입·컬렉션명·타임스탬프 등 정보를 담은 를 비동기로 반환합니다. Task InsertAutoAsync(MemoryInsertRequest request); /// /// Episodic 메모리를 수동 타입으로 삽입한다. - /// + /// +/// 사용자의 에피소드성 메모리를 비동기적으로 저장합니다. +/// +/// 저장할 에피소드 메모리 정보(텍스트, UserId 및 선택적 메타데이터 포함)를 가진 요청 객체. +/// 삽입된 메모리의 식별자, 컬렉션명, 타임스탬프 및 분류 관련 메타데이터를 포함하는 를 반환하는 비동기 작업. Task InsertEpisodicAsync(EpisodicInsertRequest request); /// /// Semantic 메모리를 수동 타입으로 삽입한다. - /// + /// +/// 주어진 SemanticInsertRequest를 사용하여 수동으로 의미적(semantic) 메모리를 비동기적으로 저장합니다. +/// +/// 저장할 의미적 메모리의 내용과 메타데이터(예: Text, UserId, FactType, ConfidenceScore, LastUpdated 등)를 포함한 요청 객체. +/// 생성된 메모리의 정보(식별자, 컬렉션명, 타임스탬프 등)를 담은 MemoryInsertResponse를 비동기적으로 반환합니다. Task InsertSemanticAsync(SemanticInsertRequest request); /// /// 특정 타입(Episodic/Semantic)의 메모리에서 검색한다. - /// + /// +/// 지정한 사용자에 대해 특정 메모 타입에서 쿼리와 유사한 메모를 비동기적으로 검색합니다. +/// +/// 검색할 메모의 분류(예: Episodic 또는 Semantic). +/// 검색할 텍스트 쿼리. +/// 검색 대상 메모가 속한 사용자 식별자. +/// 반환할 최대 결과 수(기본값 10). +/// 결과로 포함할 최소 유사도 임계값(0.0–1.0, 기본값 0.0 — 필터 없음). +/// 조건에 맞는 MemorySearchResult 객체 리스트를 비동기적으로 반환합니다. Task> SearchAsync(MemoryType memoryType, string query, string userId, int limit = 10, double similarityThreshold = 0.0); /// /// 사용자 메모리 통계를 조회한다. - /// + /// +/// 지정한 사용자의 메모 통계를 비동기로 조회합니다. +/// +/// 통계를 조회할 대상 사용자의 고유 식별자. +/// 해당 사용자의 통계 정보를 담은 를 반환하는 작업. Task GetUserStatsAsync(string userId); /// /// 시스템 전체 통계를 조회한다. - /// + /// +/// 시스템 전체 통계(총 사용자 수, 총 메모리 수, 가동 시각 등)를 비동기적으로 조회합니다. +/// +/// 시스템 통계 정보를 담은 객체. Task GetSystemStatsAsync(); } } diff --git a/ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs b/ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs index 0eb2cb7..037421f 100644 --- a/ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs +++ b/ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs @@ -10,6 +10,13 @@ public class VectorMemoryClient : IMemoryClient private readonly HttpClient _httpClient; private readonly ILogger _logger; + /// + /// VectorMemoryClient의 새 인스턴스를 생성합니다. + /// + /// + /// 생성된 인스턴스는 주입된 HttpClient로 메모리 서비스와의 HTTP 통신을 수행하고, ILogger로 로깅을 처리합니다. + /// HttpClient와 ILogger는 DI 컨테이너에서 제공되어야 합니다. + /// public VectorMemoryClient(HttpClient httpClient, ILogger logger) { _httpClient = httpClient; @@ -18,7 +25,12 @@ public VectorMemoryClient(HttpClient httpClient, ILogger log /// /// 텍스트를 자동 분류(Episodic/Semantic)하여 삽입한다. + /// + /// 자동 분류(episodic/semantic)로 메모리를 삽입하기 위해 메모리 API에 JSON 페이로드를 POST하고 응답을 MemoryInsertResponse로 매핑합니다. /// + /// 삽입할 메모리의 내용(text, userId, speaker, 선택적 emotion, context, importanceScore)을 포함하는 요청 객체. + /// 서버가 반환한 삽입 결과를 포함하는 MemoryInsertResponse. + /// HTTP 응답이 성공(2xx)이 아닌 경우, 응답 상태와 본문 상세를 포함하여 발생합니다. public async Task InsertAutoAsync(MemoryInsertRequest request) { var payload = new Dictionary @@ -64,7 +76,12 @@ public async Task InsertAutoAsync(MemoryInsertRequest requ /// /// Episodic 메모리를 수동 타입으로 삽입한다. + /// + /// 에피소드(episodic) 메모리를 서버에 삽입합니다. /// + /// 삽입할 에피소드 메모리의 내용(텍스트, 사용자 ID, 발화자, 선택적 감정, 컨텍스트 및 중요도 점수)을 포함한 요청 객체. + /// 서버 응답을 파싱한 MemoryInsertResponse 객체. + /// HTTP 응답이 성공(2xx)이 아닐 때, 서버 응답 내용과 상태 코드를 함께 포함하여 발생합니다. public async Task InsertEpisodicAsync(EpisodicInsertRequest request) { var payload = new Dictionary @@ -110,7 +127,16 @@ public async Task InsertEpisodicAsync(EpisodicInsertReques /// /// Semantic 메모리를 수동 타입으로 삽입한다. + /// + /// 지정된 의미(semantic) 메모리를 원격 메모리 서비스의 `/api/memory/semantic` 엔드포인트에 삽입합니다. /// + /// 삽입할 의미 메모리의 내용(텍스트, 사용자 ID, fact 타입, 신뢰도·중요도 점수, 선택적 마지막 수정 시각)을 포함하는 요청 객체입니다. + /// 서버 응답을 파싱하여 구성한 를 반환합니다. + /// + /// - 요청 페이로드는 JSON으로 직렬화되어 `text`, `user_id`, `fact_type`, `confidence_score`, `importance_score`, `last_updated`(있을 경우 ISO 8601 문자열) 필드를 전송합니다. + /// - HTTP 응답의 본문을 파싱하여 내부적으로 MapInsertResponse를 통해 결과를 매핑합니다. + /// + /// 서버가 성공(2xx) 응답을 반환하지 않을 경우 상태 코드와 응답 본문을 포함한 예외를 던집니다. public async Task InsertSemanticAsync(SemanticInsertRequest request) { var payload = new Dictionary @@ -149,7 +175,15 @@ public async Task InsertSemanticAsync(SemanticInsertReques /// /// 특정 타입(Episodic/Semantic)의 메모리에서 검색한다. + /// + /// 지정한 메모리 유형(episodic 또는 semantic)에 대해 쿼리를 실행하여 유사한 메모리들을 반환합니다. /// + /// 검색 대상 메모리 유형(episodic 또는 semantic). + /// 검색할 텍스트 쿼리. + /// 요청하는 사용자 ID — 내부 요청에 `X-User-ID` 헤더로 전달됩니다. + /// 최대 반환 항목 수(기본값: 10). + /// 0보다 클 경우 쿼리 문자열에 추가되어 유사도 기준으로 필터링합니다(기본값: 0.0). + /// 검색 결과의 리스트. HTTP 오류나 예외 발생 시 빈 리스트를 반환합니다. public async Task> SearchAsync(MemoryType memoryType, string query, string userId, int limit = 10, double similarityThreshold = 0.0) { var typeSegment = memoryType == MemoryType.Episodic ? "episodic" : "semantic"; @@ -187,7 +221,12 @@ public async Task> SearchAsync(MemoryType memoryType, s /// /// 사용자 메모리 통계를 조회한다. + /// + /// 지정된 사용자에 대한 메모리 통계 정보를 원격 API에서 조회합니다. + /// 실패하거나 예외가 발생하면 UserId만 설정된 빈 UserStatsResponse를 반환합니다. /// + /// 조회할 사용자의 식별자(요청 시 URL에 안전하게 이스케이프됨). + /// 사용자 통계가 채워된 UserStatsResponse; 오류 시 UserId만 설정된 기본 응답. public async Task GetUserStatsAsync(string userId) { try @@ -212,7 +251,12 @@ public async Task GetUserStatsAsync(string userId) /// /// 시스템 전체 통계를 조회한다. + /// + /// 시스템 전체 메모리 통계를 비동기로 조회합니다. /// + /// + /// 성공 시 API에서 반환된 값을 매핑한 를 반환합니다. 요청 실패나 예외가 발생하면 빈 필드가 채워진 새 인스턴스를 반환합니다. + /// public async Task GetSystemStatsAsync() { try @@ -235,6 +279,18 @@ public async Task GetSystemStatsAsync() } } + /// + /// JSON 엘리먼트에서 MemoryInsertResponse로 매핑합니다. + /// + /// + /// 입력 JsonElement의 필드들을 안전하게 읽어 다음 프로퍼티로 변환합니다: + /// - id, memory_type, collection_name, user_id: 문자열로 읽어 비어있지 않으면 설정(없거나 null이면 빈 문자열). + /// - timestamp: 문자열일 경우 DateTimeOffset으로 파싱 시도(파싱 실패 시 설정하지 않음). + /// - classification_confidence: 숫자일 경우 double로 설정. + /// - classification_explanation: 문자열로 설정. + /// 누락된 필드는 기본값(문자열은 빈 문자열, nullable 타입은 미설정)으로 남습니다. + /// + /// 맵핑된 MemoryInsertResponse 인스턴스. private static MemoryInsertResponse MapInsertResponse(JsonElement root) { var res = new MemoryInsertResponse(); @@ -260,6 +316,11 @@ private static MemoryInsertResponse MapInsertResponse(JsonElement root) return res; } + /// + /// JSON 응답(JsonElement)을 UserStatsResponse로 변환합니다. + /// + /// 사용자 통계 정보를 담은 JSON 루트 객체(예: API 응답의 최상위 요소). + /// JSON에서 추출한 값으로 채워진 UserStatsResponse(존재하지 않거나 형식이 맞지 않는 필드는 기본값 유지). private static UserStatsResponse MapUserStats(JsonElement root) { var res = new UserStatsResponse(); @@ -274,6 +335,11 @@ private static UserStatsResponse MapUserStats(JsonElement root) return res; } + /// + /// JSON 루트에서 시스템 통계 값을 추출하여 SystemStatsResponse 객체로 변환합니다. + /// + /// API 응답의 JSON 루트 요소. 예상되는 필드는 `total_users`(정수), `total_memories`(정수), `uptime_since`(문자열, ISO 8601 또는 파싱 가능한 날짜/시간)입니다. + /// 추출된 값으로 채워진 SystemStatsResponse. JSON에 필드가 없거나 타입이 일치하지 않으면 해당 속성은 기본값을 유지합니다. private static SystemStatsResponse MapSystemStats(JsonElement root) { var res = new SystemStatsResponse(); diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Data/ProjectVGDbContext.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Data/ProjectVGDbContext.cs index 5b1e01c..028cad8 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Data/ProjectVGDbContext.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Data/ProjectVGDbContext.cs @@ -16,6 +16,16 @@ public ProjectVGDbContext(DbContextOptions options) : base(o public DbSet Characters { get; set; } public DbSet ConversationHistories { get; set; } + /// + /// EF Core 모델을 구성합니다. + /// + /// + /// 엔티티별 스키마 및 제약을 구성합니다: + /// - User: 기본 키(Id), UID(필수, 최대 16), Email(필수, 최대 255), Username(필수, 최대 50), ProviderId(필수, 최대 255), Provider(필수, 최대 50), Status(필수) 및 UID/Email에 대한 고유 인덱스와 ProviderId 인덱스. + /// - Character: 기본 키(Id), Name(필수, 최대 100), Description(최대 1000), Role(필수, 최대 500), Personality(최대 1000), Background(최대 2000). + /// - ConversationHistory: 기본 키(Id), Content(필수, 최대 4000), MetadataJson(최대 4000), User 및 Character에 대한 필수 외래키(삭제 시 Cascade), 그리고 UserId+CharacterId+Timestamp 복합 인덱스 및 개별 인덱스들. + /// 메서드 마지막에 SeedData(modelBuilder)를 호출하여 초기 데이터를 주입합니다. + /// protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -82,6 +92,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) SeedData(modelBuilder); } + /// + /// 데이터베이스 모델에 초기 시드 데이터를 추가합니다. + /// + /// 엔터티 구성을 위한 ModelBuilder. DatabaseSeedData의 기본 캐릭터 풀과 사용자 풀을 사용하여 Character와 User 엔터티에 초기 데이터를 등록합니다. + /// + /// - Character 엔터티: DatabaseSeedData.DefaultCharacterPool에서 복사하여 Background는 빈 문자열로 설정하고 CreatedAt/UpdatedAt는 UTC 현재 시각으로 설정합니다. + /// - User 엔터티: DatabaseSeedData.DefaultUserPool에서 복사하여 UID, Status 등을 포함하고 CreatedAt/UpdatedAt는 UTC 현재 시각으로 설정합니다. + /// - ConversationHistory에 대한 시드 데이터는 추가하지 않습니다. + /// private void SeedData(ModelBuilder modelBuilder) { var defaultCharacters = DatabaseSeedData.DefaultCharacterPool.Select(p => new Character diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.Designer.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.Designer.cs index 44d26fd..da7512c 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.Designer.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -15,7 +15,10 @@ namespace ProjectVG.Infrastructure.Persistence.EfCore [Migration("20250825023623_AddUIDToUser")] partial class AddUIDToUser { - /// + /// + /// 이 마이그레이션의 대상 EF Core 모델을 구성합니다. + /// + /// 이 마이그레이션(AddUIDToUser)에서 생성될 데이터베이스 스키마(엔티티, 속성, 인덱스, 관계 및 시드 데이터)를 구성하는 ModelBuilder 인스턴스입니다. protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.cs index e943490..4d4ed62 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -8,7 +8,14 @@ namespace ProjectVG.Infrastructure.Persistence.EfCore /// public partial class AddUIDToUser : Migration { - /// + /// + /// 데이터베이스 스키마와 시드 데이터를 적용한다. + /// + /// + /// - Users 테이블에 non-null string 열 `UID`(기본값: 빈 문자열)를 추가합니다. + /// - Characters 테이블의 네 개 행(Ids: 11111111-..., 22222222-..., 33333333-..., 44444444-...)에 대해 CreatedAt 및 UpdatedAt 타임스탬프를 갱신합니다. + /// - Users 테이블의 두 행(Ids: aaaaaaaa-..., bbbbbbbb-...)에 대해 CreatedAt, UpdatedAt, Name을 갱신하고 `UID`를 빈 문자열로 설정합니다. + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn( @@ -61,7 +68,14 @@ protected override void Up(MigrationBuilder migrationBuilder) values: new object[] { new DateTime(2025, 8, 25, 2, 36, 22, 883, DateTimeKind.Utc).AddTicks(5786), "Zero User", "", new DateTime(2025, 8, 25, 2, 36, 22, 883, DateTimeKind.Utc).AddTicks(5786) }); } - /// + /// + /// 마이그레이션을 롤백합니다: Users 테이블에서 'UID' 열을 제거하고 변경된 시드 데이터를 이전 상태로 복원합니다. + /// + /// + /// 다음 변경을 되돌립니다: + /// - Users 테이블: 'UID' 열 삭제 및 두 사용자 레코드(IDs aaaaaaaa-... 및 bbbbbbbb-...)의 Name·CreatedAt·UpdatedAt 값을 이전 값으로 복원. + /// - Characters 테이블: 네 개의 특정 행(IDs 1111..., 2222..., 3333..., 4444...)에 대한 CreatedAt·UpdatedAt 값을 이전 타임스탬프로 복원. + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.Designer.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.Designer.cs index c601e18..8ef7d80 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.Designer.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -15,7 +15,14 @@ namespace ProjectVG.Infrastructure.Persistence.EfCore [Migration("20250825023833_AddUIDToUserTable")] partial class AddUIDToUserTable { - /// + /// + /// 마이그레이션의 대상 모델을 구성합니다. + /// + /// + /// 모델 빌더에 대해 SQL Server 식별자 길이 및 제품 버전을 설정하고(identity columns 포함), + /// 엔티티 매핑(Characters, ConversationHistories, Users), 열 속성, 키, 인덱스, 외래키 관계(필수 관계 및 캐스케이드 삭제 포함), + /// 및 초기 시드 데이터를 구성합니다. 이 구성은 해당 마이그레이션(AddUIDToUserTable)의 타깃 데이터베이스 스키마를 정의합니다. + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.cs index a02d9b1..1a9f3d9 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -54,7 +54,13 @@ protected override void Up(MigrationBuilder migrationBuilder) values: new object[] { new DateTime(2025, 8, 25, 2, 38, 33, 60, DateTimeKind.Utc).AddTicks(3954), new DateTime(2025, 8, 25, 2, 38, 33, 60, DateTimeKind.Utc).AddTicks(3954) }); } - /// + /// + /// 마이그레이션 적용을 되돌립니다. + /// + /// + /// Up 메서드에서 변경한 시드 데이터의 CreatedAt 및 UpdatedAt 값을 원래 타임스탬프(2025-08-25 02:36:22.883 UTC에 특정 ticks 추가된 값)로 복원합니다. + /// 복원 대상: Characters 테이블의 4개 레코드(IDs: 11111111-..., 22222222-..., 33333333-..., 44444444-...)와 Users 테이블의 2개 레코드(IDs: aaaaaaaa-..., bbbbbbbb-...). + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.UpdateData( diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.cs index 6bd7f8a..d178bc6 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -54,7 +54,14 @@ protected override void Up(MigrationBuilder migrationBuilder) values: new object[] { new DateTime(2025, 8, 25, 2, 39, 55, 748, DateTimeKind.Utc).AddTicks(8944), new DateTime(2025, 8, 25, 2, 39, 55, 748, DateTimeKind.Utc).AddTicks(8945) }); } - /// + /// + /// 이 마이그레이션의 적용을 되돌립니다. + /// + /// + /// Seed 데이터의 CreatedAt 및 UpdatedAt 값을 이전 타임스탬프로 복원합니다. + /// 복원 대상: Characters 테이블(IDs: 11111111-1111-1111-1111-111111111111, 22222222-2222-2222-2222-222222222222, 33333333-3333-3333-3333-333333333333, 44444444-4444-4444-4444-444444444444) 및 + /// Users 테이블(IDs: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb). + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.UpdateData( diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.Designer.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.Designer.cs index f1765f2..673d13a 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.Designer.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -15,7 +15,13 @@ namespace ProjectVG.Infrastructure.Persistence.EfCore [Migration("20250825051022_UpdateUserEntityWithUIDAndStatus")] partial class UpdateUserEntityWithUIDAndStatus { - /// + /// + /// 마이그레이션 대상(20250825051022_UpdateUserEntityWithUIDAndStatus)의 EF Core 모델을 구성합니다. + /// + /// + /// 모델 빌더에 대해 SQL Server 아이덴티티 컬럼 설정을 적용하고 Characters, ConversationHistories, Users 엔티티의 테이블/컬럼 제약, 인덱스, 외래키 관계(캐스케이드 삭제 포함) 및 시드 데이터를 설정합니다. + /// 이 구현은 마이그레이션 이후의 대상 모델(스키마 및 시드 레코드)을 정확히 재현하도록 설계되어 있습니다. + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.Designer.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.Designer.cs index 5183ab5..79ee473 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.Designer.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -15,7 +15,10 @@ namespace ProjectVG.Infrastructure.Persistence.EfCore [Migration("20250825135004_IncreaseUIDLength")] partial class IncreaseUIDLength { - /// + /// + /// 마이그레이션의 대상 EF Core 모델을 구성한다. + /// + /// 이 마이그레이션에서 구성할 엔터티, 속성, 인덱스, 외래키 및 시드 데이터를 설정하는 ModelBuilder 인스턴스. protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.cs index d4a2c4c..533ecb0 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -8,7 +8,13 @@ namespace ProjectVG.Infrastructure.Persistence.EfCore /// public partial class IncreaseUIDLength : Migration { - /// + /// + /// 사용자 테이블의 UID 열 길이를 12자에서 16자로 확장하고 관련 시드 데이터의 타임스탬프 및 일부 사용자 상태를 갱신하는 마이그레이션을 적용합니다. + /// + /// + /// - 스키마 변경: Users 테이블의 `UID` 열을 `nvarchar(12)`(maxLength:12)에서 `nvarchar(16)`(maxLength:16, not null)로 변경합니다. + /// - 시드 데이터 변경: 특정 Characters 행들의 `CreatedAt` 및 `UpdatedAt` 값을 갱신하고, 두 Users 행의 `CreatedAt`, `UpdatedAt` 값을 갱신하며 해당 Users의 `Status`를 0으로 설정합니다. + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AlterColumn( diff --git a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/ProjectVGDbContextModelSnapshot.cs b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/ProjectVGDbContextModelSnapshot.cs index 2d142d5..39dce00 100644 --- a/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/ProjectVGDbContextModelSnapshot.cs +++ b/ProjectVG.Infrastructure/Persistence/EfCore/Migrations/ProjectVGDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -13,6 +13,13 @@ namespace ProjectVG.Infrastructure.Persistence.EfCore [DbContext(typeof(ProjectVGDbContext))] partial class ProjectVGDbContextModelSnapshot : ModelSnapshot { + /// + /// EF Core 마이그레이션용 모델 스냅샷을 구성합니다. + /// + /// + /// DbContext 모델의 현재 상태를 ModelBuilder에 적용합니다. 이 구현은 엔티티 매핑(Characters, ConversationHistories, Users), 속성 타입과 제약, 키 및 인덱스, 관계(외래키 및 삭제 동작), 시드 데이터, 그리고 SQL Server용 identity 칼럼 설정과 관련 어노테이션을 정의합니다. + /// 이 메서드는 마이그레이션 생성·적용 시 사용되는 스냅샷의 일부분이며 런타임에서 직접 호출할 목적으로 설계되지 않았습니다. + /// protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/ProjectVG.Infrastructure/Persistence/Repositories/Character/SqlServerCharacterRepository.cs b/ProjectVG.Infrastructure/Persistence/Repositories/Character/SqlServerCharacterRepository.cs index 52750a4..70764cc 100644 --- a/ProjectVG.Infrastructure/Persistence/Repositories/Character/SqlServerCharacterRepository.cs +++ b/ProjectVG.Infrastructure/Persistence/Repositories/Character/SqlServerCharacterRepository.cs @@ -42,6 +42,12 @@ public async Task CreateAsync(Character character) return character; } + /// + /// 주어진 의 변경 내용을 데이터베이스의 활성 캐릭터 엔티티에 적용하고 저장합니다. + /// + /// 업데이트할 값이 들어있는 캐릭터 엔티티(식별자(Id) 포함). + /// 저장된 후의 기존 캐릭터 엔티티(데이터베이스에 있는 인스턴스). + /// 지정한 Id의 활성 캐릭터를 찾을 수 없을 경우 발생합니다. public async Task UpdateAsync(Character character) { var existingCharacter = await _context.Characters diff --git a/ProjectVG.Infrastructure/Persistence/Repositories/User/IUserRepository.cs b/ProjectVG.Infrastructure/Persistence/Repositories/User/IUserRepository.cs index 03b26bb..59db1ab 100644 --- a/ProjectVG.Infrastructure/Persistence/Repositories/User/IUserRepository.cs +++ b/ProjectVG.Infrastructure/Persistence/Repositories/User/IUserRepository.cs @@ -6,12 +6,46 @@ public interface IUserRepository { Task> GetAllAsync(); Task GetByIdAsync(Guid id); - Task GetByUsernameAsync(string username); - Task GetByEmailAsync(string email); - Task GetByProviderIdAsync(string providerId); - Task GetByUIDAsync(string uid); - Task CreateAsync(User user); - Task UpdateAsync(User user); - Task DeleteAsync(Guid id); + /// +/// 지정한 사용자 이름에 해당하는 사용자를 비동기적으로 조회합니다. +/// +/// 조회할 사용자의 사용자 이름(대소문자 처리 규칙은 구현체에 따름). +/// 조회된 User 객체를 담은 Task. 사용자가 존재하지 않으면 null을 반환할 수 있습니다. +Task GetByUsernameAsync(string username); + /// +/// 지정한 이메일과 일치하는 사용자 엔터티를 비동기적으로 조회합니다. +/// +/// 조회할 사용자의 이메일(정규화/대소문자 처리 규칙은 저장소 구현에 따름). +/// 일치하는 User를 포함한 Task; 사용자가 없으면 null을 반환합니다. +Task GetByEmailAsync(string email); + /// +/// 지정된 외부 인증 공급자 식별자(providerId)에 해당하는 사용자를 비동기적으로 조회합니다. +/// +/// 외부 인증 공급자에서 발급된 사용자 식별자(예: OAuth 제공자 ID). +/// 조회된 User 객체를 포함한 Task; 사용자가 없으면 null을 반환합니다. +Task GetByProviderIdAsync(string providerId); + /// +/// 지정된 UID로 사용자를 비동기 조회합니다. +/// +/// 조회할 사용자의 고유 식별자(UID). +/// 일치하는 사용자를 반환합니다. 없으면 null을 반환합니다. +Task GetByUIDAsync(string uid); + /// +/// 새 사용자(User) 엔티티를 비동기적으로 생성하여 저장소에 추가합니다. +/// +/// 생성할 사용자 엔티티(저장 후 식별자나 생성 시점 등 영속화된 필드가 채워질 수 있음). +/// 저장소에 생성되어 반환된 User 인스턴스(영속화된 필드가 반영됨). +Task CreateAsync(User user); + /// +/// 기존 사용자 정보를 비동기적으로 갱신하고 갱신된 사용자 엔터티를 반환합니다. +/// +/// 갱신할 사용자 엔터티(식별자(Id)가 존재하는 상태여야 함). +/// 갱신이 완료된 User 엔터티. +Task UpdateAsync(User user); + /// +/// 지정한 Id를 가진 사용자 엔티티를 비동기적으로 삭제합니다. +/// +/// 삭제할 사용자의 고유 식별자(Guid). +Task DeleteAsync(Guid id); } } \ No newline at end of file diff --git a/ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs b/ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs index 76406ad..88e5250 100644 --- a/ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs +++ b/ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs @@ -18,6 +18,10 @@ public SqlServerUserRepository(ProjectVGDbContext context, ILogger + /// 활성 상태(AccountStatus.Active)인 사용자들을 사용자명(Username) 오름차순으로 정렬하여 비동기적으로 조회합니다. + /// + /// 활성 사용자 목록을 비동기적으로 반환합니다 (IEnumerable<User>). public async Task> GetAllAsync() { return await _context.Users @@ -26,31 +30,61 @@ public async Task> GetAllAsync() .ToListAsync(); } + /// + /// 지정된 식별자에 해당하는 삭제되지 않은 사용자 엔터티를 비동기적으로 조회합니다. + /// + /// 조회할 사용자의 고유 식별자(Guid). + /// 조회된 User 객체(Task 결과). 해당 사용자를 찾지 못했거나 삭제된 경우 null. public async Task GetByIdAsync(Guid id) { return await _context.Users.FirstOrDefaultAsync(u => u.Id == id && u.Status != AccountStatus.Deleted); } + /// + /// 지정한 사용자 이름(username)에 해당하는 활성(삭제되지 않은) 사용자를 비동기적으로 조회합니다. + /// + /// 조회할 사용자의 사용자 이름(Username). + /// 일치하는 사용자가 있으면 해당 User 객체를, 없으면 null을 반환합니다. 삭제된(Status == AccountStatus.Deleted) 사용자는 조회 대상에서 제외됩니다. public async Task GetByUsernameAsync(string username) { return await _context.Users.FirstOrDefaultAsync(u => u.Username == username && u.Status != AccountStatus.Deleted); } + /// + /// 지정한 이메일과 일치하는 삭제되지 않은 사용자 엔터티를 비동기적으로 조회합니다. + /// + /// 조회할 이메일 문자열(정확 일치, 대소문자 비교는 DB의 설정에 따름). + /// 일치하는 사용자가 있으면 해당 객체, 없으면 null. public async Task GetByEmailAsync(string email) { return await _context.Users.FirstOrDefaultAsync(u => u.Email == email && u.Status != AccountStatus.Deleted); } + /// + /// 지정된 외부 제공자 ID에 해당하는 활성(삭제되지 않은) 사용자를 비동기적으로 검색합니다. + /// + /// 검색할 사용자의 외부 제공자 식별자(provider ID). + /// 일치하는 사용자가 있으면 해당 User 객체를, 없으면 null을 반환합니다. public async Task GetByProviderIdAsync(string providerId) { return await _context.Users.FirstOrDefaultAsync(u => u.ProviderId == providerId && u.Status != AccountStatus.Deleted); } + /// + — 지정된 UID(사용자 고유 식별자)를 가진, 삭제 상태가 아닌 사용자 엔터티를 비동기적으로 조회합니다. + /// + /// 조회할 사용자의 UID(고유 식별자). + /// 일치하는 사용자가 있으면 해당 객체, 없으면 null을 반환합니다. public async Task GetByUIDAsync(string uid) { return await _context.Users.FirstOrDefaultAsync(u => u.UID == uid && u.Status != AccountStatus.Deleted); } + /// + /// 새 사용자 엔티티를 생성하여 영구 저장소에 저장하고 생성된 사용자 객체를 반환합니다. + /// + /// 생성할 사용자 엔티티(메서드에서 Id, CreatedAt, UpdatedAt, Status가 설정됩니다). + /// 저장된 사용자 엔티티(데이터베이스에 반영된 상태). public async Task CreateAsync(User user) { user.Id = Guid.NewGuid(); @@ -64,6 +98,12 @@ public async Task CreateAsync(User user) return user; } + /// + /// 지정한 사용자 엔티티의 정보를 갱신하고 갱신된 엔티티를 반환합니다. + /// + /// 갱신할 값을 가진 사용자 엔티티(유효한 Id를 포함해야 함). + /// 데이터베이스에 저장된 갱신된 사용자 엔티티. + /// 요청된 Id의 사용자가 존재하지 않거나 삭제된 상태일 경우 발생합니다. public async Task UpdateAsync(User user) { var existingUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == user.Id && u.Status != AccountStatus.Deleted); @@ -84,6 +124,11 @@ public async Task UpdateAsync(User user) return existingUser; } + /// + /// 지정한 사용자를 소프트 삭제(상태를 AccountStatus.Deleted로 설정)하고 변경 사항을 영속화합니다. + /// + /// 삭제할 사용자의 식별자(Guid). + /// 지정한 Id에 해당하는 활성(삭제되지 않은) 사용자가 존재하지 않을 경우 발생합니다. public async Task DeleteAsync(Guid id) { var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id && u.Status != AccountStatus.Deleted); diff --git a/ProjectVG.Infrastructure/Persistence/Session/InMemorySessionStorage.cs b/ProjectVG.Infrastructure/Persistence/Session/InMemorySessionStorage.cs index 7d128ea..662b264 100644 --- a/ProjectVG.Infrastructure/Persistence/Session/InMemorySessionStorage.cs +++ b/ProjectVG.Infrastructure/Persistence/Session/InMemorySessionStorage.cs @@ -31,6 +31,11 @@ public Task> GetAllAsync() } } + /// + /// 메모리 내에 주어진 세션을 저장하거나 동일한 ID의 기존 세션을 교체하고 저장된 세션을 반환합니다. + /// + /// 저장할 SessionInfo 객체 (SessionId를 키로 사용). + /// 저장된 SessionInfo를 감싼 Task. public Task CreateAsync(SessionInfo session) { lock (_lock) @@ -47,6 +52,15 @@ public Task CreateAsync(SessionInfo session) } } + /// + /// 지정한 세션 정보를 저장소의 동일한 SessionId 항목으로 교체하여 업데이트합니다. + /// + /// + /// 호출은 내부 잠금을 사용해 스레드 안전하게 수행됩니다. + /// + /// 교체할 세션 정보(유효한 SessionId 필드 포함). + /// 업데이트된 세션을 포함하는 완료된 . + /// 지정한 session.SessionId에 해당하는 세션이 존재하지 않을 경우 발생합니다. public Task UpdateAsync(SessionInfo session) { lock (_lock) @@ -62,6 +76,15 @@ public Task UpdateAsync(SessionInfo session) } } + /// + /// 지정된 세션 ID로 메모리 저장소에서 세션을 제거합니다. + /// + /// 제거할 세션의 식별자. + /// + /// 존재하지 않는 세션 ID를 지정해도 예외는 발생하지 않으며, 해당 경우 경고가 기록됩니다. + /// 이 메서드는 내부 동기화를 사용해 스레드 안전하게 동작합니다. + /// + /// 작업 완료를 나타내는 완료된 . public Task DeleteAsync(string sessionId) { lock (_lock) diff --git a/ProjectVG.Infrastructure/Realtime/WebSocketConnection/WebSocketClientConnection.cs b/ProjectVG.Infrastructure/Realtime/WebSocketConnection/WebSocketClientConnection.cs index 95c4b6a..3cec77b 100644 --- a/ProjectVG.Infrastructure/Realtime/WebSocketConnection/WebSocketClientConnection.cs +++ b/ProjectVG.Infrastructure/Realtime/WebSocketConnection/WebSocketClientConnection.cs @@ -13,6 +13,10 @@ public class WebSocketClientConnection : IClientConnection public DateTime ConnectedAt { get; set; } = DateTime.UtcNow; public System.Net.WebSockets.WebSocket WebSocket { get; set; } = null!; + /// + /// 지정된 사용자 ID와 WebSocket을 사용하여 클라이언트 연결 인스턴스를 초기화하고 연결 시각을 UTC로 기록합니다. + /// + /// 연결에 연관된 사용자 식별자. public WebSocketClientConnection(string userId, WebSocket socket) { UserId = userId; diff --git a/test-clients/start-oauth2-client.py b/test-clients/start-oauth2-client.py index 4c116cd..90dcd6c 100644 --- a/test-clients/start-oauth2-client.py +++ b/test-clients/start-oauth2-client.py @@ -16,6 +16,14 @@ class OAuth2TestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): + """ + 루트 경로('/')를 OAuth2 테스트 클라이언트 페이지('/oauth2-test-client.html')로 302 리디렉트하고, + 요청이 '/oauth2-test-client.html'일 경우 파일(HTML_FILE)의 존재를 확인하여 없으면 404 응답을 보낸다. + 그 밖의 경로에 대해서는 기본 SimpleHTTPRequestHandler의 처리로 위임한다. + + 부작용: + - HTTP 응답(리디렉션 또는 오류)을 직접 전송한다. + """ if self.path == '/': # Redirect root path to OAuth2 test client self.send_response(302) @@ -33,6 +41,16 @@ def do_GET(self): super().do_GET() def main(): + """ + 로컬 HTTP 서버를 실행하여 같은 디렉터리의 oauth2-test-client.html을 제공하고 OAuth2 테스트 안내를 출력합니다. + + 서버를 포트 3000에서 시작하며, 시작 전에 HTML 파일 존재를 확인합니다. HTML 파일이 없으면 에러 메시지를 출력하고 프로세스를 종료합니다. 작업 디렉터리를 스크립트 디렉터리로 변경한 뒤 SimpleHTTPRequestHandler를 기반으로 하는 서버(OAuth2TestHandler)를 생성하고 serve_forever()로 요청을 처리합니다. 실행 중 Ctrl+C(KeyboardInterrupt)로 중지하면 정상 종료 메시지를 출력하고, 포트가 이미 사용 중인 경우에는 해당 사실을 안내하며 다른 포트 사용 또는 프로세스 종료를 제안합니다. 기타 예외는 "Unexpected error" 메시지와 함께 출력됩니다. + + 부수 효과: + - 현재 작업 디렉터리를 SCRIPT_DIR로 변경합니다. + - HTML 파일이 없을 경우 sys.exit(1)로 프로세스를 종료합니다. + - 표준 출력에 서버 상태와 사용 안내를 출력합니다. + """ PORT = 3000 # Check if HTML file exists