Skip to content

Conversation

ImGdevel
Copy link
Member

@ImGdevel ImGdevel commented Aug 28, 2025

Summary by CodeRabbit

  • New Features

    • 채팅 응답이 감정/액션 기반 세그먼트와 음성(TTS) 포함으로 스트리밍 전송됩니다.
    • 캐릭터 프로필에 요약, 사용자 호칭, 이미지가 추가되었습니다.
    • 웹 기반 AI 채팅 데모 클라이언트가 추가되었습니다.
  • Improvements

    • 채팅 요청에 요청 시각과 TTS 사용 여부 필드가 추가되었습니다.
    • 대화 생성 로직과 파싱이 향상되어 더 자연스러운 응답을 제공합니다.
    • 초기 데이터(캐릭터)가 업데이트되었습니다.
  • Documentation

    • 메모리 서버 API 사용 가이드가 추가되었습니다.
  • Chores

    • DB/Redis 초기화 스크립트가 추가되었습니다.

Copy link
Contributor

coderabbitai bot commented Aug 28, 2025

Walkthrough

채팅 요청 파이프라인을 전면 개편: ProcessChatCommand/ChatMessageSegment 제거, ChatRequestCommand/ChatSegment 도입, UserIntentType 기반 입력 분석으로 전환, LLM/메모리/웹소켓 연계 API 변경, 성공/실패 핸들러 분리, DTO/도메인/시드/마이그레이션 및 테스트·도구·문서 전반 반영.

Changes

Cohort / File(s) Summary
• API 입력/컨트롤러 변경
ProjectVG.Api/Controllers/ChatController.cs, ProjectVG.Api/Models/Chat/Request/ChatRequest.cs, ProjectVG.Api/Middleware/GlobalExceptionHandler.cs
컨트롤러가 ProcessChatCommandChatRequestCommand 생성으로 교체. 요청 모델에 RequestAt 추가, SessionId 제거. 예외 로그 템플릿 플레이스홀더 교체({Message}{UserPrompt}).
• DI 및 등록 수정
ProjectVG.Application/ApplicationServiceCollectionExtensions.cs
IOAuth2Service Singleton→Scoped. ChatSuccessHandler Scoped 등록.
• 도메인/DTO 확장
ProjectVG.Domain/.../Character.cs, ProjectVG.Application/Models/Character/CharacterDto.cs
캐릭터에 Summary, UserAlias, ImageUrl 추가. DTO가 신규 필드 매핑, 기본 생성자 제거.
• 채팅 모델 교체(명령/세그먼트/컨텍스트)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs, .../ChatSegment.cs, .../ChatProcessContext.cs, .../ChatRequestResult.cs
새 명령/세그먼트/결과 타입 도입. 컨텍스트가 ChatSegment 사용, UserRequestAt 추가, 디버그 문자열 메서드 추가, 일부 속성 제거.
• 구 타입 제거
ProjectVG.Application/Models/Chat/ProcessChatCommand.cs, .../ChatMessageSegment.cs, .../UserInputAction.cs, .../UserInputAnalysis.cs
이전 명령/세그먼트/분석 관련 공개 타입 삭제.
• 사용자 의도 타입 추가
ProjectVG.Application/Models/Chat/UserIntentType.cs
Chat/Ignore/Reject/Undefined 열거형 추가.
• 사용자 DTO 기본값
ProjectVG.Application/Models/User/UserDto.cs
기본 생성자에서 필드 초기화(새 Guid, UtcNow 등).
• 인증 서비스 게스트 UUID
ProjectVG.Application/Services/Auth/AuthService.cs
게스트 UUID를 SHA256 기반 16hex로 생성하는 헬퍼 추가, 로그인 경로 적용.
• 채팅 서비스 인터페이스/구현/검증
ProjectVG.Application/Services/Chat/IChatService.cs, .../ChatService.cs, .../Validators/ChatRequestValidator.cs, .../Validators/UserInputActionProcessor.cs
메서드 시그니처를 새 명령/결과로 변경. 세션 검증 제거(TODO). 액션 프로세서가 UserIntentType 사용. 성공/실패 핸들러 연계.
• 전처리/코스트 트래킹/입력 분석
.../Preprocessors/UserInputAnalysisProcessor.cs, .../Preprocessors/MemoryContextPreprocessor.cs, .../CostTracking/ICostTrackingDecorator.cs, .../CostTracking/CostTrackingDecorator.cs
분석이 요청 객체를 변이(튜플 반환)하도록 변경. 메모리 컨텍스트 수집이 요청 기반으로 단순화. 코스트 트래커가 요청을 받고 반환값 제거, 코스트는 요청에서 취득.
• LLM 포맷/클라이언트 대대적 변경
.../Factories/ILLMFormat.cs, .../Factories/ChatLLMFormat.cs, .../Factories/UserInputAnalysisLLMFormat.cs, ProjectVG.Infrastructure/Integrations/LLMClient/*
입력 nullable 허용. 시스템/지시문·파서 재작성(ChatSegment 반환). LLMClient가 요청/응답 스키마 교체(필드명 변경, 메모리컨텍스트 제거, History 모델 추가).
• 처리기/프로세서
.../Handlers/ChatSuccessHandler.cs, .../Handlers/ChatFailureHandler.cs, .../Processors/ChatLLMProcessor.cs, .../Processors/ChatTTSProcessor.cs, .../Processors/ChatResultProcessor.cs
성공 시 세그먼트별 웹소켓 전송 추가. 실패 핸들러 단순화(fail 전송). LLM 응답 처리 OutputText 기반. TTS 불변 업데이트 패턴. 메모리 저장이 에피소드형 두 건으로 분리.
• 공통 상수
ProjectVG.Common/Constants/CharacterConstants.cs, .../EmotionConstants.cs(삭제)
감정/액션 상수 추가, 구 감정 상수 삭제.
• 사용자 서비스/저장소
ProjectVG.Application/Services/User/IUserService.cs, .../UserService.cs, ProjectVG.Infrastructure/.../User/IUserRepository.cs, .../SqlServerUserRepository.cs
이메일 조회 메서드 추가. 프로바이더 조회가 (provider, providerId)로 변경.
• 마이그레이션/시드/디자인타임
ProjectVG.Infrastructure/Migrations/*, .../Persistence/Data/DatabaseSeedData.cs, .../DesignTimeDbContextFactory.cs
마이그레이션 재구성/네임스페이스 이동 및 스키마 변경(캐릭터 신규 필드, Users UID/Status 등). 시드 데이터 업데이트. 디자인타임이 .env 로드 및 연결문자열 우선순위 조정.
• 테스트 추가/삭제
ProjectVG.Tests/Services/Chat/Factories/*, ProjectVG.Tests/Auth/JwtAuthenticationFilterTests.cs
새 LLM 포맷 테스트 2종 추가. JWT 필터 테스트 제거.
• 문서/스크립트/테스트 클라이언트
docs/memory_server_api.md, scripts/*.ps1, test-clients/ai-chat-client/*, test-clients/test-client.html(삭제)
메모리 서버 API 문서 추가. DB 리셋/마이그레이션 스크립트 조정. 새 웹 클라이언트(HTML/CSS/JS) 추가, 구 테스트 페이지 삭제.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client as Client (HTTP)
  participant API as ChatController
  participant Svc as ChatService
  participant Val as ChatRequestValidator
  participant Pre1 as UserInputAnalysisProcessor
  participant Pre2 as MemoryContextPreprocessor
  participant LLM as ILLMClient
  participant TTS as ChatTTSProcessor
  participant Res as ChatResultProcessor
  participant WS as IWebSocketManager
  note over Client,API: POST /api/v1/chat

  Client->>API: ChatRequest(message, character_id, request_at, use_tts)
  API->>Svc: EnqueueChatRequestAsync(ChatRequestCommand)
  Svc->>Val: ValidateAsync(command)
  Val-->>Svc: ok
  Svc->>Pre1: ProcessAsync(command)
  Pre1-->>Svc: command.ProcessType/Intent set
  Svc->>Pre2: CollectMemoryContextAsync(command)
  Pre2-->>Svc: memoryContext (list)
  Svc->>LLM: CreateTextResponseAsync(system, prompt, instructions, history)
  LLM-->>Svc: LLMResponse(OutputText, tokens, cost)
  Svc->>Svc: Parse → List<ChatSegment>
  Svc->>TTS: GenerateTTS for segments (if UseTTS)
  TTS-->>Svc: segments with audio
  Svc->>Res: PersistResultsAsync(context)
  Svc->>WS: ChatSuccessHandler.HandleAsync(context)
  WS-->>Client: WebSocket messages (segments/audio)
  Svc-->>API: ChatRequestResult.Accepted(...)
  API-->>Client: 202 Accepted
  rect rgba(255,230,230,0.4)
    alt On error
      Svc->>WS: ChatFailureHandler.HandleAsync(context)
      Svc-->>API: ChatRequestResult.Rejected(...)
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

  • Feature: chat result hanlder #4 — 동일한 채팅 파이프라인 타입 교체(명령/세그먼트/포맷)와 핸들러 흐름 수정이 겹치는 대규모 리팩터링.

Poem

귀가 쫑긋 토끼 한 마리, 버전에 깡총 점프해
명령은 새 옷 갈아입고, 세그먼트 줄 맞춰 서네 🎶
의도는 또렷, 메모리 살짝—LLM에 속삭이고
웨브소켓 바람 타고, 목소리로 인사하네 💬🔊
“Accepted!” 깃발 휘날리니, 캐롯 로그 반짝하도다 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/memory

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 49

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (12)
scripts/start-oauth2-client-node.ps1 (2)

25-28: 오류 메시지의 디렉터리 경로가 변경사항과 불일치

실제 경로는 test-clients\oauth2-client인데 메시지는 상위 test-clients를 가리킵니다. 사용자 혼란 유발.

-    Write-Host "Please check if start-oauth2-client.js exists in test-clients directory." -ForegroundColor Red
+    Write-Host ("Please check if start-oauth2-client.js exists at: " +
+                (Join-Path $TestClientsDir 'start-oauth2-client.js')) -ForegroundColor Red

100-111: 노드 서버의 포트 설정 지원 추가 필요
현재 test-clients/oauth2-client/start-oauth2-client.js는 const PORT = 3000으로 하드코딩되어 있어 process.env.PORT--port CLI 인자를 전혀 읽지 않습니다. 이로 인해 PowerShell 스크립트에서 설정한 $Port 값과 서버가 실제로 바인딩하는 포트가 불일치합니다. Node 스크립트가 환경 변수(process.env.PORT) 또는 CLI 인자를 통해 포트를 입력받도록 수정해야 합니다.

ProjectVG.Application/Models/User/UserDto.cs (1)

30-39: 엔터티 → DTO 매핑에서 CreatedAt/UpdatedAt 누락

DTO에 필드가 존재하지만 복사되지 않아 조회 응답 시 타임스탬프가 빠집니다.

적용 diff:

         public UserDto(Domain.Entities.Users.User user)
         {
             Id = user.Id;
             UID = user.UID;
             Username = user.Username;
             Email = user.Email;
             ProviderId = user.ProviderId;
             Provider = user.Provider;
             Status = user.Status;
+            CreatedAt = user.CreatedAt;
+            UpdatedAt = user.UpdatedAt;
         }
ProjectVG.Api/Middleware/GlobalExceptionHandler.cs (1)

36-47: Response.HasStarted 이후 오류 본문을 추가로 쓰지 마세요

헤더 전송 이후에도 JSON 본문을 쓰고 있어, 이미 스트리밍/부분 응답이 시작된 경우 본문이 뒤섞일 수 있습니다. 시작 여부를 확인해 조기 반환하세요.

-            if (!context.Response.HasStarted) {
-                context.Response.ContentType = "application/json";
-                context.Response.StatusCode = errorResponse.StatusCode;
-            }
-
-            var jsonResponse = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions {
-                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
-                WriteIndented = _environment.IsDevelopment()
-            });
-
-            await context.Response.WriteAsync(jsonResponse);
+            if (context.Response.HasStarted) {
+                _logger.LogWarning("응답 헤더가 이미 전송되었습니다. 오류 본문을 생략합니다. TraceId: {TraceId}", context.TraceIdentifier);
+                return;
+            }
+
+            context.Response.ContentType = "application/json";
+            context.Response.StatusCode = errorResponse.StatusCode;
+
+            var jsonResponse = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions {
+                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+                WriteIndented = _environment.IsDevelopment()
+            });
+
+            await context.Response.WriteAsync(jsonResponse);
ProjectVG.Application/Models/Chat/ChatRequestResult.cs (1)

3-13: Status 문자열 상수화/열거형화

"ACCEPTED"/"REJECTED"의 오탈자 리스크를 없애기 위해 enum 또는 상수 클래스로 고정하세요.

- public string Status { get; private set; } = string.Empty;
+ public string Status { get; private set; } = string.Empty; // TODO: enum 전환 권장

(예: enum ChatRequestStatus { Accepted, Rejected })

ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (2)

69-76: 감정 스타일 비교의 대소문자 민감도

profile.SupportedStyles.Contains(normalizedEmotion)는 기본적으로 대/소문자를 구분합니다. 프롬프트/파서가 소문자 보장하지 않으면 미지원 오탐이 납니다. OrdinalIgnoreCase 비교를 사용하세요.

- if (!profile.SupportedStyles.Contains(normalizedEmotion)) {
+ if (!profile.SupportedStyles.Contains(normalizedEmotion, StringComparer.OrdinalIgnoreCase)) {

100-102: AudioLength 로그 포맷의 null 안전성

{AudioLength:F2}는 null일 경우 포맷 예외가 납니다. null 병합 또는 조건부 포맷을 적용하세요.

- _logger.LogInformation("[TTS] 응답 생성 완료: 오디오 길이 ({AudioLength:F2}초), 요청 시간({ProcessingTimeMs:F2}ms)",
-     response.AudioLength, processingTime);
+ var audioLen = response.AudioLength ?? 0f;
+ _logger.LogInformation("[TTS] 응답 생성 완료: 오디오 길이 ({AudioLength:F2}초), 요청 시간({ProcessingTimeMs:F2}ms)",
+     audioLen, processingTime);
ProjectVG.Application/ApplicationServiceCollectionExtensions.cs (1)

30-31: 중복 서비스 등록 제거 필요(ICharacterService, IUserService).
같은 서비스가 두 번 등록되어 혼란을 유발합니다(마지막 등록이 유효). 중복 제거를 권장합니다.

             // Character Services
-            services.AddScoped<ICharacterService, CharacterService>();
+            services.AddScoped<ICharacterService, CharacterService>();

             // Chat Services - Core
             services.AddScoped<IChatService, ChatService>();
             services.AddScoped<IChatMetricsService, ChatMetricsService>();

-            services.AddScoped<ICharacterService, CharacterService>();
-            services.AddScoped<IUserService, UserService>();
+            // (중복 제거됨)

             // User Services
-            services.AddScoped<IUserService, UserService>();
+            services.AddScoped<IUserService, UserService>();

Also applies to: 36-38

ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (1)

36-37: 로그 플레이스홀더 오타: 세션/사용자 값이 동일 키로 출력

"{UserId}, {UserId}"로 되어 있어 세션 ID가 사용자 ID로 표시됩니다.

-            _logger.LogDebug("채팅 결과 저장 완료: 세션 {UserId}, 사용자 {UserId}", context.SessionId, context.UserId);
+            _logger.LogDebug("채팅 결과 저장 완료: 세션 {SessionId}, 사용자 {UserId}", context.SessionId, context.UserId);
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (1)

63-69: 대화 이력 최신 n개 선별 및 시간순 정렬

최근 n개를 뽑아 시간 오름차순으로 정렬하는 편이 일반적인 LLM 컨텍스트 구성에 적합합니다.

-            return ConversationHistory.Take(count).Select(h => $"{h.Role}: {h.Content}");
+            return ConversationHistory
+                .OrderByDescending(h => h.Timestamp)
+                .Take(count)
+                .OrderBy(h => h.Timestamp)
+                .Select(h => $"{h.Role}: {h.Content}");
ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (2)

76-99: LLMClient: 네트워크 오류 시 Mock 응답을 설정 플래그로만 활성화

네트워크 예외 발생 시 Success = true Mock 응답을 무조건 반환하면 프로덕션 데이터 오염 및 장애 은폐 위험이 큽니다. 아래처럼 _enableMockOnNetworkError 플래그로 분기 처리하고, 플래그가 false인 경우 Success = false 오류 응답을 반환하도록 수정하세요. 또한 appsettings.json(및 환경별 설정)에 기본값을 명시해야 합니다.

– 파일: ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs
• catch 블록에 _enableMockOnNetworkError 조건 분기 및 오류 응답 반환 로직 추가
• 클래스 필드(private readonly bool _enableMockOnNetworkError;) 및 생성자에서 플래그 초기화

– 설정 (appsettings.json 등)에 다음 항목 추가:

"LLM": {
  "EnableMockOnNetworkError": false
}

120-128: CreateTextResponseAsync: conversationHistory를 List<History>?로 변경하고 API 키 강제 설정 제거

  • ILLMClient.CreateTextResponseAsync 시그니처에서 List<string>? conversationHistory = defaultList<History>? conversationHistory = default로 교체하고, 구현부 매핑에서 Select 변환 대신 conversationHistory를 그대로 전달하세요.
  • OpenAiApiKey = ""UseUserApiKey = false 할당을 제거하여 기본 설정/서버 측 설정에 위임하세요.
  • 변경 후 호출부가 여전히 호환되는지(List<string> 대신 List<History> 전달 여부 등) 검증하세요.
🧹 Nitpick comments (59)
scripts/start-oauth2-client-node.ps1 (3)

16-18: 경로 결합에서 역슬래시 하드코딩 제거

Windows 전용이라도 Join-Path에 경로 구분자를 문자열로 넣지 말고 세그먼트로 결합하는 편이 안전합니다.

-$TestClientsDir = Join-Path $ProjectRoot "test-clients\oauth2-client"
+$TestClientsDir = Join-Path (Join-Path $ProjectRoot 'test-clients') 'oauth2-client'

35-56: 포트 점유 프로세스 중복 종료 방지

같은 PID가 다수 연결을 보유하면 Stop-Process가 중복 호출됩니다. 고유 PID로 한 번만 종료하세요.

-    Write-Host "Processes using the port:" -ForegroundColor Yellow
-    $PortInUse | ForEach-Object {
-        $Process = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
-        if ($Process) {
-            Write-Host "  - PID: $($_.OwningProcess), Process: $($Process.ProcessName)" -ForegroundColor Gray
-        }
-    }
+    Write-Host "Processes using the port:" -ForegroundColor Yellow
+    $pids = $PortInUse | Select-Object -ExpandProperty OwningProcess | Sort-Object -Unique
+    foreach ($pid in $pids) {
+        $Process = Get-Process -Id $pid -ErrorAction SilentlyContinue
+        if ($Process) {
+            Write-Host "  - PID: $pid, Process: $($Process.ProcessName)" -ForegroundColor Gray
+        }
+    }
@@
-        $PortInUse | ForEach-Object {
-            try {
-                Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue
-                Write-Host "  - PID $($_.OwningProcess) terminated" -ForegroundColor Green
-            } catch {
-                Write-Host "  - Failed to terminate PID $($_.OwningProcess): $($_.Exception.Message)" -ForegroundColor Red
-            }
-        }
+        foreach ($pid in $pids) {
+            try {
+                Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
+                Write-Host "  - PID $pid terminated" -ForegroundColor Green
+            } catch {
+                Write-Host "  - Failed to terminate PID $pid: $($_.Exception.Message)" -ForegroundColor Red
+            }
+        }

79-91: Start-Browser 함수가 사용되지 않음

동일 기능이 Start-Job 블록 내에서 직접 호출됩니다. 함수 제거 또는 재사용 중 하나로 정리하세요.

ProjectVG.Application/Models/User/UserDto.cs (1)

14-15: DateTime 대신 DateTimeOffset 고려

크로스 타임존/서머타임 안전성을 위해 감사용 타임스탬프는 DateTimeOffset이 더 적합합니다. 전역 정책에 따라 단계적 전환을 검토하세요.

test-clients/ai-chat-client/index.html (2)

5-8: 모바일 대응을 위한 viewport 메타 추가 제안
반응형 안내 없어서 모바일에서 레이아웃이 깨질 수 있습니다.

적용 diff:

   <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>AI 채팅 + 음성 데모</title>

42-42: 접근성: 채팅 로그에 ARIA 속성 부여
스크린리더가 실시간 로그를 읽을 수 있도록 role/aria 속성 추가 권장.

적용 diff:

-    <div id="chat-log"></div>
+    <div id="chat-log" role="log" aria-live="polite" aria-atomic="false"></div>
test-clients/ai-chat-client/styles.css (1)

102-112: 접근성: 버튼 포커스 가시성 추가
키보드 사용자를 위해 :focus-visible 스타일 추가 권장.

적용 diff:

 #send-btn {
   background: #4caf50;
   color: white;
   border: none;
   padding: 0.75em 1.5em;
   border-radius: 8px;
   font-size: 1em;
   cursor: pointer;
   transition: all 0.3s ease;
   font-weight: 500;
 }
+
+#send-btn:focus-visible,
+#login-btn:focus-visible {
+  outline: 3px solid #4caf50;
+  outline-offset: 2px;
+}

Also applies to: 125-141

test-clients/ai-chat-client/script.js (1)

441-451: 재연결 정책 개선(옵션)
선형 backoff보다 지터 포함 지수 backoff가 안정적입니다. 또한 MAX_RECONNECT 노출 설정화 권장.

원하시면 간단한 지터 backoff 패치를 드리겠습니다.

ProjectVG.Infrastructure/Persistence/Data/DatabaseSeedData.cs (1)

45-47: UserAlias 기본값 중복 지정 최소화(가독성/Nitpick)

UserAlias의 클래스 기본값이 "마스터"로 설정되어 있어 시드에서 동일 값을 반복 지정할 필요는 없습니다. 유지보수성을 위해 제거를 권장합니다.

예시 diff:

-                UserAlias = "마스터",
+                // 기본값 "마스터" 사용

위 변경을 각 캐릭터 항목(하루/미야/소피아/제로)에 적용하세요.

Also applies to: 57-59, 69-71, 81-83

scripts/reset_db.ps1 (2)

5-5: docker-compose(v1)와 docker compose(v2) 호환 처리 제안

일부 환경은 v2만 설치되어 있습니다. 공용 래퍼 함수를 두어 둘 다 지원하면 실패율이 줄어듭니다.

아래 함수와 에러 중단 설정을 스크립트 상단(라인 1 직후)에 추가하고, 호출부를 교체하세요.

추가(상단):

$ErrorActionPreference = "Stop"

function Invoke-Compose {
  param([Parameter(Mandatory=$true)][string[]]$Args)
  if (Get-Command docker-compose -ErrorAction SilentlyContinue) {
    & docker-compose @Args
  } elseif (Get-Command docker -ErrorAction SilentlyContinue) {
    & docker compose @Args
  } else {
    throw "Docker Compose가 설치되어 있지 않습니다."
  }
}

호출부 교체(diff):

- docker-compose -f docker-compose.db.yml down -v
+ Invoke-Compose -Args @('-f','docker-compose.db.yml','down','-v')

- docker-compose -f docker-compose.db.yml up -d
+ Invoke-Compose -Args @('-f','docker-compose.db.yml','up','-d')

- docker-compose -f docker-compose.db.yml ps
+ Invoke-Compose -Args @('-f','docker-compose.db.yml','ps')

Also applies to: 18-18, 23-23


9-10: 멱등성·안정성 소폭 개선 제안

  • 볼륨 제거는 존재 여부 확인 후 강제 제거로 간결화 가능(docker volume rm -f).
  • 네트워크는 존재 시 재생성 생략(docker network inspect → 조건부 생성).

현재도 에러 리다이렉션으로 큰 문제는 없으나, 아래와 같은 형태를 추천합니다.

# 볼륨
'docker volume rm -f projectvg-db-data','docker volume rm -f projectvg-redis-data' | ForEach-Object { iex $_ }

# 네트워크
if (-not (docker network inspect projectvg-external-db 2>$null)) {
  docker network create projectvg-external-db | Out-Null
}

Also applies to: 14-14

ProjectVG.Infrastructure/Integrations/LLMClient/Models/History.cs (1)

5-18: 메시지 역할을 enum으로 한정(검증·오타 방지)

Role 문자열은 오타/불일치 위험이 있으므로 enum+JsonStringEnumConverter 사용을 권장합니다.

예시:

public enum RoleType { System, User, Assistant }

public class History {
  [JsonPropertyName("role")]
  [JsonConverter(typeof(JsonStringEnumConverter))]
  public RoleType Role { get; set; }

  [JsonPropertyName("content")]
  public string Content { get; set; } = "";
}

필요 시 서버 내부 맵핑에서 기존 문자열("system"/"user"/"assistant")과의 호환 계층을 두세요.

ProjectVG.Common/Constants/CharacterConstants.cs (2)

5-17: 케이스 무시 멤버십 체크를 위한 HashSet 노출 제안

외부에서 지원 여부를 빠르게/일관되게 검사할 수 있도록 OrdinalIgnoreCase HashSet을 함께 노출하세요. 중복 방지도 됩니다.

 public static class CharacterConstants
 {
     public static readonly string[] SupportedEmotions = new[]
     {
         "neutral",       // 중립
         "happy",         // 행복
         ...
         "proud"          // 자부심
     };
+
+    public static readonly ISet<string> SupportedEmotionSet =
+        new HashSet<string>(SupportedEmotions, StringComparer.OrdinalIgnoreCase);

20-37: 액션도 동일한 형태로 제공

액션도 대소문자 무시 포함검사를 위해 Set을 노출하는 편이 좋습니다.

     public static readonly string[] SupportedActions = new[]
     {
         "blushing",       // 얼굴 붉히기
         ...
         "clapping"        // 박수
     };
+
+    public static readonly ISet<string> SupportedActionSet =
+        new HashSet<string>(SupportedActions, StringComparer.OrdinalIgnoreCase);
ProjectVG.Application/Models/Chat/ChatRequestResult.cs (2)

14-26: Accepted 팩토리의 시간 주입 가능성

테스트 결정성을 위해 DateTime.UtcNow를 직접 호출하기보다 Clock/IClock 주입 또는 팩토리 인자로 받을 수 있게 고려하세요.


28-41: Rejected 기본 ErrorCode의 타입 안전성

에러코드도 enum/상수로 관리하십시오. 또한 message가 비어있을 경우의 가드(ArgumentException) 추가를 고려하세요.

ProjectVG.Application/Models/Chat/ChatSegment.cs (1)

25-31: IsEmpty 의미 명확화

현재 IsEmpty는 Content 유무만 봅니다. 향후 오디오만 있는 세그먼트가 허용된다면 정의 재고가 필요합니다(예: IsEmpty => !HasContent && !HasAudio).

ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (2)

55-58: Console.WriteLine 대신 로거 사용

콘솔 출력은 운영 환경에서 잡음이 됩니다. 로거 Debug로 치환하세요.

- Console.WriteLine($"[TTS_DEBUG] 오디오 길이: {ttsResult.AudioLength.Value:F2}초, TTS 비용: {ttsCost:F0} Cost");
+ _logger.LogDebug("[TTS] 오디오 길이: {AudioLength:F2}초, TTS 비용: {Cost:F0}", ttsResult.AudioLength.Value, ttsCost);

78-96: 텍스트 길이 제한 상수화 및 메시지 일관화

매직넘버 300을 상수로 승격하고 Validation 메시지를 상수/리소스로 통일하세요. 다국어 대응에도 유리합니다.

docs/memory_server_api.md (1)

7-8: 기본 URL 스킴(HTTP) 주의

가이드 상단의 기본 URL이 HTTP입니다. 운영 권고는 HTTPS로 명시하세요.

- - **기본 URL**: `http://memory-server`
+ - **기본 URL**: `https://memory-server` (운영 권장) / `http://memory-server` (로컬/개발)
ProjectVG.Infrastructure/Integrations/LLMClient/Models/LLMResponse.cs (2)

19-22: CreatedAt 단위 명확화 필요(권장: DateTimeOffset 사용 또는 Unix epoch 주석 고정).
외부 스키마가 epoch seconds/ms 중 무엇인지 명확하지 않습니다. 주석에 단위를 못 박거나 DateTimeOffset로 전환을 검토하세요.


55-58: TextFormatType는 enum으로 제한해 입력 오류를 예방하세요.
허용 값이 한정적이라면 enum → 문자열 변환으로 밸리데이션을 확보하는 편이 안전합니다.

ProjectVG.Application/Models/Chat/UserIntentType.cs (1)

5-8: LLM 지시문과 enum 값 매핑을 일치시키세요.
현재 프롬프트는 0/1만 안내하지만 enum은 Reject(2), Undefined(3)을 포함합니다. 지시문 예시/규칙에 2,3을 추가하거나 파서의 기본값을 Undefined로 조정하세요.

ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs (1)

10-10: ICostTrackingDecorator 구현체 검증 완료 및 오버로드 분리 검토
CostTrackingDecorator에서 ProcessAsync(ChatRequestCommand)와 기존 ProcessAsync(ChatProcessContext)가 모두 구현되어 누락된 구현체는 없습니다. 동일한 이름의 오버로드로 인해 DI 프록시/동적 디스패치 시 혼동 가능성이 있으므로, 필요 시 명시적 메서드 명 분리(예: ProcessRequestAsync, ProcessContextAsync)를 권장합니다.

ProjectVG.Application/Models/Character/CharacterDto.cs (1)

19-31: 널 가드 및 ImageUrl 매핑 추가

생성자 인자에 대한 널 가드와 ImageUrl 매핑을 추가해 NRE와 누락을 방지하세요.

-        public CharacterDto(Domain.Entities.Characters.Character character)
+        public CharacterDto(Domain.Entities.Characters.Character character)
         {
-            Id = character.Id;
+            if (character is null) throw new ArgumentNullException(nameof(character));
+            Id = character.Id;
             Name = character.Name;
             Description = character.Description;
             Role = character.Role;            
             IsActive = character.IsActive;
             Personality = character.Personality;
             SpeechStyle = character.SpeechStyle;
             UserAlias = character.UserAlias;
             Summary = character.Summary;
+            ImageUrl = character.ImageUrl;
             VoiceId = character.VoiceId;
         }
ProjectVG.Domain/Entities/Character/Character.cs (2)

37-39: 기본값 표기 일관성

다른 문자열 기본값은 string.Empty를 사용하고 있는데 ImageUrl만 리터럴 ""를 사용합니다. 일관성 유지를 권장합니다.

-        public string ImageUrl { get; set; } = "";
+        public string ImageUrl { get; set; } = string.Empty;

16-18: 도메인 필드에 대한 길이/유효성 제약 검토

Summary, UserAlias, ImageUrl에 대한 최대 길이 및 URL 유효성은 인프라(EF 설정/엔티티 구성)에서 강제되는지 확인 필요합니다. 미설정 시 데이터 품질 저하/인덱스 비대화 가능.

Also applies to: 31-33, 37-39

ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs (1)

28-43: 널 가드 및 병렬 검증으로 간단한 견고성/성능 향상

입력 널 가드 추가, 사용자/캐릭터 존재 확인을 병렬화해 왕복을 줄일 수 있습니다.

-        public async Task ValidateAsync(ChatRequestCommand command)
+        public async Task ValidateAsync(ChatRequestCommand command)
         {
-            // TODO : 세션 검증
+            if (command is null) throw new ArgumentNullException(nameof(command));
+            // TODO : 세션 검증
 
-            var userExists = await _userService.ExistsByIdAsync(command.UserId);
+            var userExistsTask = _userService.ExistsByIdAsync(command.UserId);
-            if (!userExists) {
+            var characterExistsTask = _characterService.CharacterExistsAsync(command.CharacterId);
+            await Task.WhenAll(userExistsTask, characterExistsTask);
+            if (!userExistsTask.Result) {
                 _logger.LogWarning("사용자 ID 검증 실패: {UserId}", command.UserId);
                 throw new NotFoundException(ErrorCode.USER_NOT_FOUND, command.UserId);
             }
 
-            var characterExists = await _characterService.CharacterExistsAsync(command.CharacterId);
-            if (!characterExists) {
+            if (!characterExistsTask.Result) {
                 _logger.LogWarning("캐릭터 ID 검증 실패: {CharacterId}", command.CharacterId);
                 throw new NotFoundException(ErrorCode.CHARACTER_NOT_FOUND, command.CharacterId);
             }
         }
ProjectVG.Application/Services/Chat/Validators/UserInputActionProcessor.cs (1)

37-41: Undefined → Chat 처리 정책 확인 필요

Undefined를 일괄적으로 Chat으로 통과시키는 것은 안전장치 부족입니다. 의도 판별 실패 상황에서 부적절한 입력이 그대로 처리될 수 있습니다. 제품 요구사항에 맞는지 확인하고, 최소한 메트릭/알람을 추가하거나 별도 게이트로 라우팅하는 방안을 고려해 주세요.

ProjectVG.Infrastructure/Persistence/EfCore/Migrations/ProjectVGDbContextModelSnapshot.cs (2)

44-80: 새 필드의 스키마 제약 강화 제안(ImageUrl, Summary, UserAlias)

세 컬럼이 nvarchar(max) Required로 추가되었습니다. 실서비스에서는 길이 상한/인덱스가 필요할 수 있습니다. 예:

  • ImageUrl: nvarchar(1024)
  • UserAlias: nvarchar(100) + Unique Index(검색/표시용 별칭이라면)
  • Summary: nvarchar(2000)

스냅샷은 자동 생성물이므로 도메인 엔티티/Fluent API에서 제약을 정의한 후 마이그레이션을 재생성해 주세요.


89-157: 시드 데이터: Required 컬럼에 빈 문자열 사용

필수 컬럼(ImageUrl, Summary, UserAlias)에 빈 문자열을 시드하면, 도메인 검증과 UI 가정(미표시, 플레이스홀더 등)과 충돌할 수 있습니다. 최소한 기본값(예: “N/A”, 샘플 URL)을 넣거나 null 허용 + 기본 처리로 설계를 명확히 해 주세요.

ProjectVG.Application/Services/Chat/Handlers/ChatSuccessHandler.cs (1)

40-41: 로그 템플릿 파라미터 불일치

세션 {UserId}로 템플릿을 선언하고 context.SessionId를 전달합니다. 플레이스홀더를 {SessionId}로 수정하세요.

-            _logger.LogDebug("채팅 결과 전송 완료: 세션 {UserId}, 세그먼트 {SegmentCount}개",
-                context.SessionId, context.Segments.Count(s => !s.IsEmpty));
+            _logger.LogDebug("채팅 결과 전송 완료: 세션 {SessionId}, 세그먼트 {SegmentCount}개",
+                context.SessionId, context.Segments.Count(s => !s.IsEmpty));
ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (1)

40-41: 로그 민감도: 전체 응답 본문 노출

전체 응답을 로그에 남기면 개인정보/민감정보 유출 위험이 있습니다. 길이 제한 또는 마스킹을 권장합니다.

-            _logger.LogInformation("채팅 처리 결과: {Response}\n 세그먼트 생성 개수: {SegmentCount}\n 입력 토큰: {InputTokens}\n 출력 토큰: {OutputTokens}\n 비용: {Cost}",
-                llmResponse.OutputText, segments.Count, llmResponse.InputTokens, llmResponse.OutputTokens, cost);
+            var preview = llmResponse.OutputText.Length > 300 ? llmResponse.OutputText.Substring(0, 300) + "..." : llmResponse.OutputText;
+            _logger.LogInformation("채팅 처리 결과: {ResponsePreview}\n 세그먼트 생성 개수: {SegmentCount}\n 입력 토큰: {InputTokens}\n 출력 토큰: {OutputTokens}\n 비용: {Cost}",
+                preview, segments.Count, llmResponse.InputTokens, llmResponse.OutputTokens, cost);
ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs (1)

111-117: MultiSearchResponse 불변/의도치 않은 수정 방지

외부에서 리스트가 변경될 수 있어 결과 일관성이 깨질 수 있습니다. init 접근자와 IReadOnlyList 노출로 안전성을 높이세요.

-    public class MultiSearchResponse
+    public class MultiSearchResponse
     {
-        public List<MemorySearchResult> EpisodicResults { get; set; } = new();
-        public List<MemorySearchResult> SemanticResults { get; set; } = new();
-        public int TotalResults { get; set; }
-        public string Query { get; set; } = string.Empty;
+        public List<MemorySearchResult> EpisodicResults { get; init; } = new();
+        public List<MemorySearchResult> SemanticResults { get; init; } = new();
+        public int TotalResults { get; init; }
+        public string Query { get; init; } = string.Empty;
     }

필요 시 별도 파일(Models/MultiSearchResponse.cs)로 분리하여 모듈성도 개선해 주세요.

ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs (1)

42-53: 의도 키워드 매칭의 문화권/대소문자 민감도 보완

string.Contains 기본 비교는 문화권/대소문자에 민감합니다. 오탐/실패를 줄이기 위해 OrdinalIgnoreCase 사용을 권장합니다.

-        private MemoryType ChooseMemoryType(string userIntent)
+        private MemoryType ChooseMemoryType(string userIntent)
         {
             // 질문, 회상, 기억 관련 의도는 Episodic 메모리에서 검색
             var episodicKeywords = new[] { "질문", "회상", "기억", "과거", "경험", "언제", "어떻게", "무엇" };
             
-            if (episodicKeywords.Any(keyword => userIntent.Contains(keyword)))
+            if (episodicKeywords.Any(keyword => userIntent.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
             {
                 return MemoryType.Episodic;
             }
             
             return MemoryType.Semantic;
         }
ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (1)

79-110: 콘솔 출력 제거 및 메트릭/로깅 일관성 유지

프로덕션에서 Console.WriteLine은 로깅/메트릭 파이프라인과 분리되어 관리가 어렵습니다. 메트릭만 기록하거나(현 구현처럼) 별도 ILogger를 주입해 일관된 로깅으로 전환하세요.

-                // Cost는 request 객체에서 직접 가져올 수 있음
-                var cost = request.Cost;
-                Console.WriteLine($"[COST_TRACKING] {_processName} - 추출된 비용: {cost:F0} Cost");
-                _metricsService.EndProcessMetrics(_processName, (decimal)cost);
+                // Cost는 request 객체에서 직접 가져올 수 있음
+                var cost = request.Cost;
+                _metricsService.EndProcessMetrics(_processName, (decimal)cost);

필요 시 ILogger 주입은 별도 PR로 반영해도 됩니다.

ProjectVG.Tests/Services/Chat/Factories/ChatLLMFormatTests.cs (1)

428-435: 허용 감정/액션 검증이 순서/포맷에 취약 — 안정적인 단위 검증으로 교체 권장

고정된 콤마 연결 문자열 비교는 상수 목록 변경/순서 변경 시 취약합니다. 목록의 각 항목 포함 여부로 검증하세요.

-            result.Should().Contain("MANDATORY OUTPUT FORMAT SPECIFICATION");
-            result.Should().Contain("[emotion:emotion_name]");
-            result.Should().Contain("(action:action_name)");
-            result.Should().Contain("dialogue");
-            result.Should().Contain("neutral, happy, sad, angry, shy, surprised, embarrassed, sleepy, confused, proud");
-            result.Should().Contain("blushing, nodding, shaking_head, waving, smiling");
+            result.Should().Contain("MANDATORY OUTPUT FORMAT SPECIFICATION");
+            result.Should().Contain("[emotion:emotion_name]");
+            result.Should().Contain("(action:action_name)");
+            result.Should().Contain("dialogue");
+            foreach (var e in CharacterConstants.SupportedEmotions)
+                result.Should().Contain(e);
+            foreach (var a in CharacterConstants.SupportedActions)
+                result.Should().Contain(a);
ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250828110518_AddMissingCharacterColumns.Designer.cs (2)

73-76: Summary 컬럼도 nvarchar(max) 대신 상한을 지정하세요

요약 텍스트는 보통 수백~수천 자 범위입니다(예: 1000). nvarchar(max)는 인덱싱/통계에 불리합니다. 빈 문자열 시드 대신 Nullable 또는 최소 길이 검증 정책을 정합적으로 가져가세요.


80-83: UserAlias에 길이/유니크 제약 검토

별칭이라면 최대 길이(예: 100) 및 사용자 범위 유니크 인덱스가 필요할 수 있습니다. 현재 nvarchar(max) Required는 과합니다.

ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250828110518_AddMissingCharacterColumns.cs (1)

26-32: Summary 스키마: nvarchar(max) 지양 및 기본값 정책 정합화

요약은 제한 길이를 두는 편이 좋습니다(예: nvarchar(1000)). 또한 비어있을 수 있다면 Nullable, 필수라면 빈 문자열 대신 도메인에 맞는 기본값 또는 마이그레이션 단계에서 생성 규칙을 적용하세요.

-            migrationBuilder.AddColumn<string>(
-                name: "Summary",
-                table: "Characters",
-                type: "nvarchar(max)",
-                nullable: false,
-                defaultValue: "");
+            migrationBuilder.AddColumn<string>(
+                name: "Summary",
+                table: "Characters",
+                type: "nvarchar(1000)",
+                nullable: true);
ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (1)

90-98: 이중 쓰기 부분 실패 처리

두 번째 삽입 실패 시 사용자 메시지만 기록되는 “반쪽 성공” 상태가 생깁니다. 일괄 API가 없다면 병렬 처리 + 개별 예외 로깅으로 처리 상태를 분리하거나, 보상 로직을 마련하세요.

예시(간단 병렬화):

-                await _memoryClient.InsertEpisodicAsync(userMemoryRequest);
-                await _memoryClient.InsertEpisodicAsync(episodicRequest);
+                var insertUser = _memoryClient.InsertEpisodicAsync(userMemoryRequest);
+                var insertAssistant = _memoryClient.InsertEpisodicAsync(episodicRequest);
+                await Task.WhenAll(insertUser, insertAssistant);
ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (3)

26-27: 로컬 변수 네이밍: Instructions → instructions (camelCase)

C# 컨벤션에 맞지 않고 속성명과 혼동을 유발합니다.

-            var Instructions = format.GetInstructions(null);
+            var instructions = format.GetInstructions(null);
...
-                    Instructions,
+                    instructions,

Also applies to: 31-34, 46-47, 50-51


22-22: CancellationToken 미수용

LLM 호출 경로에 취소 토큰을 전파하지 않아 타임아웃/취소 전략을 적용하기 어렵습니다. 메서드 시그니처와 LLMClient 호출에 Token을 전달하세요.

-        public async Task ProcessAsync(ChatRequestCommand request)
+        public async Task ProcessAsync(ChatRequestCommand request, CancellationToken ct = default)
...
-                var llmResponse = await _llmClient.CreateTextResponseAsync(
+                var llmResponse = await _llmClient.CreateTextResponseAsync(
                     systemPrompt,
                     userPrompt,
-                    instructions,
-                    null,
+                    instructions,
+                    null,
                     model: format.Model,
                     maxTokens: format.MaxTokens,
-                    temperature: format.Temperature
+                    temperature: format.Temperature
                 );

(LLMClient/인터페이스도 토큰 지원으로 확장 필요)


30-38: 대화 맥락 미사용 의도 확인

의도 분석에서 history를 완전히 배제했습니다. 애매한 입력(오타/맥락 의존)에서 분류 정확도가 저하될 수 있습니다. 포맷이 맥락 비의존 설계를 전제하는지 확인하고, 필요 시 최근 N개만 전달하도록 옵션화하세요.

ProjectVG.Application/Models/Chat/ChatProcessContext.cs (2)

13-13: 시간대 일관성: DateTime.Now → UtcNow로 교체 권장

서버/로그 전반이 UTC를 사용합니다. 지역 시간 혼입을 피하기 위해 기본값을 UtcNow로 맞추세요.

-        public DateTime UserRequestAt { get; private set; } = DateTime.Now;
+        public DateTime UserRequestAt { get; private set; } = DateTime.UtcNow;

51-56: 널 방어 및 음수 비용 방지

Segments에 null이 들어오거나 음수 비용이 설정될 수 있습니다. 방어 로직을 추가하세요.

-        public void SetResponse(string response, List<ChatSegment> segments, double cost)
+        public void SetResponse(string response, List<ChatSegment> segments, double cost)
         {
             Response = response;
-            Segments = segments;
-            Cost = cost;
+            Segments = segments ?? new List<ChatSegment>();
+            Cost = Math.Max(0, cost);
         }
ProjectVG.Application/Services/Chat/ChatService.cs (3)

10-10: 불필요한 using 제거

using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;는 사용되지 않습니다. 제거해 주세요.

-using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;

69-84: 엔큐 API는 OK, 단 취소 토큰 지원 고려

즉시 응답/백그라운드 처리 전략은 적절합니다. 다만 외부 호출 취소 연동을 위해 CancellationToken 지원을 추가하는 것을 권장합니다.


79-82: 백그라운드 실행 전략 개선(선택): 작업 큐/호스티드 서비스 사용

Task.Run 대신 큐 기반 백그라운드 워커를 사용하면 스로틀링/관찰/취소가 용이합니다.

ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)

52-55: 비용 누적 시 음수 입력 방지

예상치 못한 음수 입력을 차단하세요.

-        public void AddCost(double value)
+        public void AddCost(double value)
         {
-            Cost += value;
+            if (value <= 0) return;
+            Cost += value;
         }

56-61: 디버그 문자열의 PII 노출 가능성

운영 기본 레벨에서는 사용하지 않거나(Info↓), 프롬프트를 마스킹/절단하는 것을 권장합니다. ChatService 코멘트와 함께 적용 검토 바랍니다.


41-44: 대화 이력 보관 프로퍼티는 현재 미사용

현 파이프라인에서는 컨텍스트로 직접 전달합니다. 향후 사용 계획이 없으면 제거하여 표면적을 줄이는 것도 고려해 주세요.

ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (2)

1-1: 불필요한 using 제거

이 파일에서 ILogger는 사용되지 않습니다. 정리하세요.

-using Microsoft.Extensions.Logging;

65-71: 요일/시간 표기 일관성

UserRequestAt:O(UTC/오프셋 포함) 서식을 고려해 주세요. 시스템 메시지의 시간대 혼동을 줄입니다.

ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (4)

51-56: 에러 본문 일부를 포함해 진단 가능성 향상

현재 StatusCode만 반환되어 원인 분석이 어렵습니다. 본문 일부(최대 200자)를 에러 메시지에 포함하세요.

적용 diff:

-                        Error = $"서비스 오류: {response.StatusCode}"
+                        Error = $"서비스 오류: {response.StatusCode} - {errorContent[..Math.Min(200, errorContent.Length)]}"

68-72: JSON 파싱 실패 시 원문 스니펫 포함

파싱 실패 시 원문 200자를 포함하면 트러블슈팅이 쉬워집니다.

적용 diff:

-                    Error = "응답을 파싱할 수 없습니다."
+                    Error = $"응답 파싱 실패. 원본(200자): {responseContent[..Math.Min(200, responseContent.Length)]}"

23-27: 선택 필드 null 직렬화 무시 권장

선택 필드를 null로 두고 JSON에 포함하지 않으려면 옵션을 추가하세요.

외부 라인 변경(옵션) — JSON 옵션:

_jsonOptions = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = false,
    DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};

100-108: 타임아웃 vs 취소 구분 및 호출 취소 지원

TaskCanceledException은 사용자 취소와 타임아웃이 혼재합니다. CancellationToken을 메서드 시그니처에 추가해 PostAsync에 전달하고, 취소와 타임아웃을 구분해 메시지를 분리하세요.

ProjectVG.Tests/Services/Chat/Factories/UserInputAnalysisLLMFormatTests.cs (1)

181-194: 테스트명 의도와 단언 불일치 — 명확한 네이밍으로 수정

Out-of-range 값을 기본값으로 돌리지 않고 enum 캐스팅값을 유지합니다. 메서드명을 의도에 맞게 변경하세요.

적용 diff:

-        public void Parse_NumberOutOfRange_ShouldReturnDefault()
+        public void Parse_NumberOutOfRange_ShouldPreserveEnumValue()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 22acc5f and 38656e1.

📒 Files selected for processing (56)
  • ProjectVG.Api/Controllers/ChatController.cs (1 hunks)
  • ProjectVG.Api/Middleware/GlobalExceptionHandler.cs (12 hunks)
  • ProjectVG.Api/Models/Chat/Request/ChatRequest.cs (1 hunks)
  • ProjectVG.Application/ApplicationServiceCollectionExtensions.cs (1 hunks)
  • ProjectVG.Application/Models/Character/CharacterDto.cs (2 hunks)
  • ProjectVG.Application/Models/Chat/ChatMessageSegment.cs (0 hunks)
  • ProjectVG.Application/Models/Chat/ChatProcessContext.cs (4 hunks)
  • ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (1 hunks)
  • ProjectVG.Application/Models/Chat/ChatRequestResult.cs (3 hunks)
  • ProjectVG.Application/Models/Chat/ChatSegment.cs (1 hunks)
  • ProjectVG.Application/Models/Chat/ProcessChatCommand.cs (0 hunks)
  • ProjectVG.Application/Models/Chat/UserInputAction.cs (0 hunks)
  • ProjectVG.Application/Models/Chat/UserInputAnalysis.cs (0 hunks)
  • ProjectVG.Application/Models/Chat/UserIntentType.cs (1 hunks)
  • ProjectVG.Application/Models/User/UserDto.cs (1 hunks)
  • ProjectVG.Application/Services/Auth/AuthService.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/ChatService.cs (4 hunks)
  • ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Handlers/ChatSuccessHandler.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/IChatService.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs (2 hunks)
  • ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (2 hunks)
  • ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs (1 hunks)
  • ProjectVG.Application/Services/Chat/Validators/UserInputActionProcessor.cs (1 hunks)
  • ProjectVG.Common/Constants/CharacterConstants.cs (1 hunks)
  • ProjectVG.Common/Constants/EmotionConstants.cs (0 hunks)
  • ProjectVG.Domain/Entities/Character/Character.cs (1 hunks)
  • ProjectVG.Infrastructure/Integrations/LLMClient/ILLMClient.cs (0 hunks)
  • ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (7 hunks)
  • ProjectVG.Infrastructure/Integrations/LLMClient/Models/History.cs (1 hunks)
  • ProjectVG.Infrastructure/Integrations/LLMClient/Models/LLMRequest.cs (2 hunks)
  • ProjectVG.Infrastructure/Integrations/LLMClient/Models/LLMResponse.cs (1 hunks)
  • ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs (2 hunks)
  • ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs (2 hunks)
  • ProjectVG.Infrastructure/Persistence/Data/DatabaseSeedData.cs (5 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250828110518_AddMissingCharacterColumns.Designer.cs (1 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250828110518_AddMissingCharacterColumns.cs (1 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/ProjectVGDbContextModelSnapshot.cs (4 hunks)
  • ProjectVG.Tests/Auth/JwtAuthenticationFilterTests.cs (0 hunks)
  • ProjectVG.Tests/Services/Chat/Factories/ChatLLMFormatTests.cs (1 hunks)
  • ProjectVG.Tests/Services/Chat/Factories/UserInputAnalysisLLMFormatTests.cs (1 hunks)
  • docs/memory_server_api.md (1 hunks)
  • scripts/reset_db.ps1 (1 hunks)
  • scripts/start-oauth2-client-node.ps1 (1 hunks)
  • test-clients/ai-chat-client/index.html (1 hunks)
  • test-clients/ai-chat-client/script.js (1 hunks)
  • test-clients/ai-chat-client/styles.css (1 hunks)
  • test-clients/test-client.html (0 hunks)
💤 Files with no reviewable changes (8)
  • ProjectVG.Application/Models/Chat/ChatMessageSegment.cs
  • ProjectVG.Tests/Auth/JwtAuthenticationFilterTests.cs
  • ProjectVG.Common/Constants/EmotionConstants.cs
  • ProjectVG.Application/Models/Chat/UserInputAction.cs
  • test-clients/test-client.html
  • ProjectVG.Application/Models/Chat/ProcessChatCommand.cs
  • ProjectVG.Infrastructure/Integrations/LLMClient/ILLMClient.cs
  • ProjectVG.Application/Models/Chat/UserInputAnalysis.cs
🧰 Additional context used
🧬 Code graph analysis (30)
ProjectVG.Application/Services/Chat/Handlers/ChatSuccessHandler.cs (5)
ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (2)
  • Task (20-65)
  • Task (78-113)
ProjectVG.Application/Services/Chat/ChatService.cs (3)
  • Task (69-84)
  • Task (90-107)
  • Task (112-132)
ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (1)
  • Task (22-31)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
  • ChatProcessContext (6-138)
  • ChatProcessContext (24-32)
  • ChatProcessContext (34-49)
ProjectVG.Application/Models/Chat/IntegratedChatMessage.cs (2)
  • IntegratedChatMessage (5-45)
  • SetAudioData (34-44)
ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (2)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (2)
  • GetSystemMessage (15-84)
  • GetInstructions (86-132)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (2)
  • GetSystemMessage (11-14)
  • GetInstructions (16-32)
ProjectVG.Application/Models/Chat/UserIntentType.cs (1)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1)
  • UserIntentType (35-70)
ProjectVG.Domain/Entities/Character/Character.cs (1)
ProjectVG.Domain/Common/BaseEntity.cs (2)
  • BaseEntity (3-20)
  • BaseEntity (10-14)
ProjectVG.Application/Models/Chat/ChatSegment.cs (1)
test-clients/ai-chat-client/script.js (2)
  • audioData (137-137)
  • audioLength (135-135)
ProjectVG.Application/Services/Chat/IChatService.cs (2)
ProjectVG.Application/Models/Chat/ChatRequestResult.cs (3)
  • ChatRequestResult (3-42)
  • ChatRequestResult (14-26)
  • ChatRequestResult (28-41)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs (1)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs (3)
ProjectVG.Application/Services/Chat/ChatService.cs (3)
  • Task (69-84)
  • Task (90-107)
  • Task (112-132)
ProjectVG.Application/Services/User/UserService.cs (10)
  • Task (19-43)
  • Task (45-56)
  • Task (58-62)
  • Task (64-68)
  • Task (70-74)
  • Task (76-81)
  • Task (83-83)
  • Task (84-84)
  • Task (85-85)
  • Task (86-86)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs (4)
ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (1)
  • Task (22-52)
ProjectVG.Application/Services/Chat/ChatService.cs (3)
  • Task (69-84)
  • Task (90-107)
  • Task (112-132)
ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs (7)
  • Task (10-10)
  • Task (15-15)
  • Task (20-20)
  • Task (25-25)
  • Task (30-30)
  • Task (35-35)
  • Task (40-40)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
ProjectVG.Tests/Services/Chat/Factories/ChatLLMFormatTests.cs (3)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (7)
  • ChatLLMFormat (9-220)
  • List (134-140)
  • List (142-205)
  • List (207-214)
  • GetSystemMessage (15-84)
  • GetInstructions (86-132)
  • CalculateCost (216-219)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
  • ChatProcessContext (6-138)
  • ChatProcessContext (24-32)
  • ChatProcessContext (34-49)
ProjectVG.Application/Models/Character/CharacterDto.cs (2)
  • CharacterDto (5-32)
  • CharacterDto (19-31)
ProjectVG.Infrastructure/Integrations/LLMClient/Models/History.cs (1)
ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (2)
  • LLMClient (11-145)
  • LLMClient (17-33)
ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (5)
ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (2)
  • Task (35-118)
  • Task (120-144)
ProjectVG.Infrastructure/Integrations/LLMClient/ILLMClient.cs (2)
  • Task (12-12)
  • Task (25-32)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (5)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
  • AddCost (52-55)
  • SetAnalysisResult (46-50)
ProjectVG.Application/Services/Chat/Factories/LLMFormatFactory.cs (1)
  • LLMFormatFactory (5-16)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (2)
  • GetSystemMessage (11-14)
  • GetInstructions (16-32)
ProjectVG.Application/Services/Chat/Validators/UserInputActionProcessor.cs (5)
ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (1)
  • Task (22-52)
ProjectVG.Application/Services/Chat/ChatService.cs (3)
  • Task (69-84)
  • Task (90-107)
  • Task (112-132)
ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs (1)
  • Task (22-40)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1)
  • UserIntentType (35-70)
ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (3)
ProjectVG.Application/Services/Chat/Handlers/ChatSuccessHandler.cs (1)
  • Task (21-42)
ProjectVG.Application/Services/Chat/ChatService.cs (3)
  • Task (69-84)
  • Task (90-107)
  • Task (112-132)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
  • ChatProcessContext (6-138)
  • ChatProcessContext (24-32)
  • ChatProcessContext (34-49)
ProjectVG.Api/Controllers/ChatController.cs (1)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs (2)
ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs (8)
  • Task (10-10)
  • Task (15-15)
  • Task (20-20)
  • Task (25-25)
  • Task (30-30)
  • Task (35-35)
  • Task (40-40)
  • MultiSearchResponse (111-117)
ProjectVG.Infrastructure/Integrations/MemoryClient/Models/MemorySearchResult.cs (1)
  • MemorySearchResult (5-12)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (3)
ProjectVG.Application/Services/Chat/Factories/LLMFormatFactory.cs (1)
  • UserInputAnalysisLLMFormat (12-15)
ProjectVG.Common/Constants/LLMModelInfo.cs (2)
  • LLMModelInfo (3-325)
  • GPT4oMini (127-136)
ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (2)
  • GetSystemMessage (5-5)
  • GetInstructions (6-6)
ProjectVG.Application/Services/Chat/ChatService.cs (10)
ProjectVG.Application/Services/Chat/Handlers/ChatSuccessHandler.cs (2)
  • ChatSuccessHandler (8-43)
  • ChatSuccessHandler (13-19)
ProjectVG.Application/Services/Chat/Handlers/ChatFailureHandler.cs (3)
  • ChatFailureHandler (9-32)
  • ChatFailureHandler (14-20)
  • Task (22-31)
ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (1)
  • Task (22-52)
ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (4)
  • Task (30-37)
  • Task (39-99)
  • ChatResultProcessor (11-100)
  • ChatResultProcessor (18-28)
ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (2)
  • Task (43-75)
  • Task (79-110)
ProjectVG.Application/Services/Chat/Preprocessors/MemoryContextPreprocessor.cs (1)
  • Task (22-40)
ProjectVG.Application/Models/Chat/ChatRequestResult.cs (3)
  • ChatRequestResult (3-42)
  • ChatRequestResult (14-26)
  • ChatRequestResult (28-41)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (4)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
  • ToDebugString (56-61)
ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (3)
  • StartChatMetrics (16-26)
  • EndChatMetrics (59-66)
  • LogChatMetrics (73-92)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (4)
  • ChatProcessContext (6-138)
  • ChatProcessContext (24-32)
  • ChatProcessContext (34-49)
  • ToDebugString (70-137)
ProjectVG.Tests/Services/Chat/Factories/UserInputAnalysisLLMFormatTests.cs (2)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (4)
  • UserInputAnalysisLLMFormat (5-77)
  • UserIntentType (35-70)
  • GetSystemMessage (11-14)
  • GetInstructions (16-32)
ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (2)
  • GetSystemMessage (5-5)
  • GetInstructions (6-6)
ProjectVG.Infrastructure/Integrations/LLMClient/Models/LLMRequest.cs (1)
ProjectVG.Infrastructure/Integrations/LLMClient/Models/History.cs (1)
  • History (5-18)
ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs (2)
ProjectVG.Infrastructure/Integrations/MemoryClient/VectorMemoryClient.cs (1)
  • MultiSearchResponse (320-352)
ProjectVG.Infrastructure/Integrations/MemoryClient/Models/MemorySearchResult.cs (1)
  • MemorySearchResult (5-12)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
  • IEnumerable (63-68)
  • AddCost (58-61)
  • ToDebugString (70-137)
ProjectVG.Domain/Entities/ConversationHistory/ConversationHistory.cs (1)
  • ConversationHistory (14-55)
ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs (1)
  • UserIntentType (35-70)
ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (3)
ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (2)
  • LLMClient (11-145)
  • LLMClient (17-33)
ProjectVG.Common/Constants/LLMModelInfo.cs (1)
  • CalculateCost (315-324)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (1)
  • SetResponse (51-56)
ProjectVG.Application/Services/Chat/CostTracking/CostTrackingDecorator.cs (4)
ProjectVG.Application/Services/Chat/Preprocessors/UserInputAnalysisProcessor.cs (1)
  • Task (22-52)
ProjectVG.Application/Services/Chat/CostTracking/ICostTrackingDecorator.cs (2)
  • Task (9-9)
  • Task (10-10)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (3)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
ProjectVG.Application/Services/Chat/CostTracking/ChatMetricsService.cs (1)
  • EndProcessMetrics (42-57)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (6)
ProjectVG.Application/Services/Chat/Factories/LLMFormatFactory.cs (1)
  • ChatLLMFormat (7-10)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
  • ChatProcessContext (6-138)
  • ChatProcessContext (24-32)
  • ChatProcessContext (34-49)
ProjectVG.Application/Models/Chat/ChatSegment.cs (3)
  • ChatSegment (34-43)
  • ChatSegment (45-53)
  • ChatSegment (56-64)
ProjectVG.Common/Constants/LLMModelInfo.cs (3)
  • LLMModelInfo (3-325)
  • GPT4oMini (127-136)
  • CalculateCost (315-324)
ProjectVG.Application/Services/Chat/Factories/ILLMFormat.cs (3)
  • GetSystemMessage (5-5)
  • GetInstructions (6-6)
  • CalculateCost (11-11)
ProjectVG.Common/Constants/CharacterConstants.cs (1)
  • CharacterConstants (3-38)
ProjectVG.Application/Services/Chat/Processors/ChatTTSProcessor.cs (3)
ProjectVG.Application/Services/Chat/Processors/ChatLLMProcessor.cs (1)
  • Task (22-42)
ProjectVG.Application/Services/Chat/ChatService.cs (3)
  • Task (69-84)
  • Task (90-107)
  • Task (112-132)
ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (2)
  • Task (30-37)
  • Task (39-99)
ProjectVG.Application/Services/Chat/Processors/ChatResultProcessor.cs (1)
ProjectVG.Infrastructure/Integrations/MemoryClient/IMemoryClient.cs (2)
  • EpisodicInsertRequest (62-64)
  • EmotionInfo (73-79)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (4)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (3)
  • List (134-140)
  • List (142-205)
  • List (207-214)
ProjectVG.Application/Models/Chat/ChatSegment.cs (3)
  • ChatSegment (34-43)
  • ChatSegment (45-53)
  • ChatSegment (56-64)
ProjectVG.Application/Models/Chat/ChatRequestCommand.cs (4)
  • ChatRequestCommand (5-62)
  • ChatRequestCommand (24-28)
  • ChatRequestCommand (30-39)
  • ToDebugString (56-61)
ProjectVG.Domain/Entities/ConversationHistory/ConversationHistory.cs (1)
  • ConversationHistory (14-55)
ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250828110518_AddMissingCharacterColumns.Designer.cs (2)
ProjectVG.Infrastructure/Persistence/EfCore/Migrations/ProjectVGDbContextModelSnapshot.cs (1)
  • DbContext (13-305)
ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250828110518_AddMissingCharacterColumns.cs (1)
  • AddMissingCharacterColumns (9-134)
ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs (2)
ProjectVG.Infrastructure/Integrations/LLMClient/Models/LLMResponse.cs (1)
  • LLMResponse (5-79)
ProjectVG.Infrastructure/Integrations/LLMClient/Models/History.cs (1)
  • History (5-18)
🪛 markdownlint-cli2 (0.17.2)
docs/memory_server_api.md

634-634: Bare URL used

(MD034, no-bare-urls)


635-635: Bare URL used

(MD034, no-bare-urls)

Comment on lines +7 to +10
- **기본 URL**: `http://memory-server`
- **API 버전**: v2.0
- **인증**: User ID 헤더 기반

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

인증 헤더(X-User-ID)만으로의 인증은 보안상 위험

문서에 최소한의 보안 권고(토큰 기반 인증, HTTPS, 레이트리밋, 감사로그) 주석을 추가하세요. 운영/개발 환경 분리 표기도 권장합니다.

🤖 Prompt for AI Agents
In docs/memory_server_api.md around lines 7 to 10, the doc currently lists only
header-based X-User-ID authentication which is insecure; update the
documentation to add a short security recommendations section: state that
X-User-ID alone is insufficient and recommend token-based authentication (e.g.,
bearer tokens or JWTs with expiry and signature validation), require HTTPS for
all traffic, enforce rate limiting and IP throttling, record audit logs for
authentication and sensitive operations, and describe separate configurations
and examples for development vs production (e.g., local/dev may allow relaxed
settings with clear warnings, production must use tokens/HTTPS/ratelimit/audit).
Ensure each recommendation is a concise bullet or short paragraph and include
explicit notes about rotating credentials and validating tokens server-side.

Comment on lines +634 to +636
- **Swagger UI**: http://memory-server/docs
- **ReDoc**: http://memory-server/redoc
- **GitHub 이슈**: [프로젝트 저장소 이슈 페이지]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

markdownlint(MD034) 해결: Bare URL 링크 포맷팅

두 URL을 마크다운 링크로 감싸세요.

- - **Swagger UI**: http://memory-server/docs
- - **ReDoc**: http://memory-server/redoc
+ - **Swagger UI**: [http://memory-server/docs](http://memory-server/docs)
+ - **ReDoc**: [http://memory-server/redoc](http://memory-server/redoc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Swagger UI**: http://memory-server/docs
- **ReDoc**: http://memory-server/redoc
- **GitHub 이슈**: [프로젝트 저장소 이슈 페이지]
- **Swagger UI**: [http://memory-server/docs](http://memory-server/docs)
- **ReDoc**: [http://memory-server/redoc](http://memory-server/redoc)
- **GitHub 이슈**: [프로젝트 저장소 이슈 페이지]
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

634-634: Bare URL used

(MD034, no-bare-urls)


635-635: Bare URL used

(MD034, no-bare-urls)

🤖 Prompt for AI Agents
In docs/memory_server_api.md around lines 634 to 636, the two bare URLs are
flagged by markdownlint MD034; update them to use Markdown link syntax by
replacing the plain URLs with bracketed link text followed by the URL in
parentheses (e.g., [Swagger UI](http://memory-server/docs) and
[ReDoc](http://memory-server/redoc)) so both links are properly formatted.

Comment on lines +30 to +35
var command = new ChatRequestCommand(
userGuid,
request.CharacterId,
request.Message,
request.RequestAt,
request.UseTTS);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

클라이언트 제공 시각의 타임존 정규화 및 메시지 정제

request.RequestAt을 그대로 전달하면 타임존 혼선이 생깁니다. UTC 정규화 후 전달을 권장합니다. 메시지도 트리밍하여 노이즈를 줄이세요.

-            var command = new ChatRequestCommand(
-                userGuid, 
-                request.CharacterId, 
-                request.Message,
-                request.RequestAt,
-                request.UseTTS);
+            var requestedAtUtc = request.RequestAt.Kind == DateTimeKind.Utc
+                ? request.RequestAt
+                : DateTime.SpecifyKind(request.RequestAt, DateTimeKind.Utc);
+            var message = request.Message?.Trim() ?? string.Empty;
+            var command = new ChatRequestCommand(
+                userGuid,
+                request.CharacterId,
+                message,
+                requestedAtUtc,
+                request.UseTTS);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In ProjectVG.Api/Controllers/ChatController.cs around lines 30 to 35, the code
passes request.RequestAt and request.Message directly into ChatRequestCommand
which can cause timezone confusion and noisy messages; convert request.RequestAt
to UTC (e.g., call ToUniversalTime() or ensure DateTimeKind.Utc) before passing
it, and trim request.Message (e.g., call .Trim() and handle null safely) so the
command receives a normalized UTC timestamp and a trimmed message.

Comment on lines +20 to +21
[JsonPropertyName("request_at")]
public DateTime RequestAt { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

요청 시간 타입을 DateTimeOffset으로 변경 권장(타임존/서머타임 안전)

DateTime은 Kind/타임존 불명 이슈로 순서 판단·보관 시 오류가 납니다. DateTimeOffset 사용과 서버 UTC 기본값을 권장합니다.

-        [JsonPropertyName("request_at")]
-        public DateTime RequestAt { get; set; }
+        [JsonPropertyName("request_at")]
+        public DateTimeOffset RequestAt { get; set; } = DateTimeOffset.UtcNow;

클라이언트에는 ISO 8601(예: 2025-08-28T16:20:00+09:00)로 전송하도록 문서화해 주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[JsonPropertyName("request_at")]
public DateTime RequestAt { get; set; }
[JsonPropertyName("request_at")]
public DateTimeOffset RequestAt { get; set; } = DateTimeOffset.UtcNow;
🤖 Prompt for AI Agents
In ProjectVG.Api/Models/Chat/Request/ChatRequest.cs around lines 20-21, the
RequestAt property uses System.DateTime which can lead to timezone/Kind
ambiguity; change its type to System.DateTimeOffset, initialize or validate it
to use UTC offsets (e.g., default to DateTimeOffset.UtcNow where appropriate),
update any serializers/converters if needed to preserve offsets, and run/adjust
unit tests and model bindings; also document that clients must send the
timestamp in ISO 8601 with offset (e.g., 2025-08-28T16:20:00+09:00).

Comment on lines +14 to 16
public string Summary { get; set; } = string.Empty;
public string UserAlias { get; set; } = string.Empty;
public string VoiceId { get; set; } = string.Empty;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

DTO에 ImageUrl 누락 — 도메인과 매핑 정합성 확보 필요

도메인에 ImageUrl이 추가되었는데 DTO에 노출되지 않습니다. API/클라이언트에서 이미지가 필요하다면 DTO에 포함하세요.

         public string SpeechStyle { get; set; } = string.Empty;
         public string Summary { get; set; } = string.Empty;
         public string UserAlias { get; set; } = string.Empty;
+        public string ImageUrl { get; set; } = string.Empty;
         public string VoiceId { get; set; } = string.Empty;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public string Summary { get; set; } = string.Empty;
public string UserAlias { get; set; } = string.Empty;
public string VoiceId { get; set; } = string.Empty;
public string SpeechStyle { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public string UserAlias { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty;
public string VoiceId { get; set; } = string.Empty;
🤖 Prompt for AI Agents
In ProjectVG.Application/Models/Character/CharacterDto.cs around lines 14 to 16,
the DTO is missing the ImageUrl property added to the domain model; add a public
string ImageUrl { get; set; } = string.Empty; to the DTO so API responses
include the character image and mapping between domain and DTO remains
consistent (also update any mapping/profile code to map the domain ImageUrl to
this new DTO property).

Comment on lines +27 to +33
const audioPlayer = document.getElementById('audio-player');
const characterSelect = document.getElementById('character-select');
const guestIdInput = document.getElementById('guest-id');
const loginBtn = document.getElementById('login-btn');
const loginSection = document.getElementById('login-section');
const includeAudioCheckbox = document.getElementById('include-audio');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

오디오 표시 토글 대상 잘못 지정
현재 audio-player만 토글하고 부모 #audio-section은 숨김 상태여서 UI가 드러나지 않습니다. 부모를 hidden 속성으로 토글하세요.

적용 diff:

-const audioPlayer = document.getElementById('audio-player');
+const audioPlayer = document.getElementById('audio-player');
+const audioSection = document.getElementById('audio-section');
@@
   if (audioQueue.length === 0) {
     isPlayingAudio = false;
-    audioPlayer.style.display = "none";
+    if (audioSection) audioSection.hidden = true;
     console.log("오디오 큐가 비어있음");
     return;
   }
   const blob = audioQueue.shift();
@@
-  audioPlayer.src = URL.createObjectURL(blob);
-  audioPlayer.style.display = "";
+  audioPlayer.src = URL.createObjectURL(blob);
+  if (audioSection) audioSection.hidden = false;
   isPlayingAudio = true;

Also applies to: 186-207

🤖 Prompt for AI Agents
In test-clients/ai-chat-client/script.js around lines 27 to 33 (and also apply
the same change to lines 186 to 207), the toggle currently targets only the
audio-player element but its parent #audio-section remains hidden so the UI
doesn't appear; update the toggle logic to set the parent element
(document.getElementById('audio-section')) hidden property instead of (or in
addition to) audio-player so the section is shown/hidden correctly, i.e., query
and store the audioSection element and toggle audioSection.hidden where the code
previously toggled audioPlayer.hidden.

Comment on lines +110 to +156
// 바이너리 메시지 파싱 함수
function parseBinaryMessage(arrayBuffer) {
try {
const dataView = new DataView(arrayBuffer);
let offset = 0;

// 메시지 타입 확인 (1바이트)
const messageType = dataView.getUint8(offset);
offset += 1;

if (messageType !== 0x03) {
return null;
}

// 텍스트 읽기
const textLength = dataView.getUint32(offset, true);
offset += 4;
let text = null;
if (textLength > 0) {
const textBytes = new Uint8Array(arrayBuffer, offset, textLength);
text = new TextDecoder().decode(textBytes);
offset += textLength;
}

// 오디오 데이터 읽기
const audioLength = dataView.getUint32(offset, true);
offset += 4;
let audioData = null;
if (audioLength > 0) {
audioData = new Uint8Array(arrayBuffer, offset, audioLength);
offset += audioLength;
}

// 오디오 길이 읽기 (float)
const audioDuration = dataView.getFloat32(offset, true);

return {
sessionId,
text,
audioData,
audioLength: audioDuration
};
} catch (e) {
console.error("바이너리 메시지 파싱 오류:", e);
return null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

바이너리 파서의 참조 오류(sessionId 미정의) 및 필드명 불일치
parseBinaryMessage가 존재하지 않는 sessionId를 반환하고, onmessage에서 이를 참조해 런타임 오류 가능. audioLength 필드에 duration을 대입하는 혼동도 존재.

적용 diff:

 function parseBinaryMessage(arrayBuffer) {
   try {
     const dataView = new DataView(arrayBuffer);
     let offset = 0;

     // 메시지 타입 확인 (1바이트)
     const messageType = dataView.getUint8(offset);
     offset += 1;

     if (messageType !== 0x03) {
       return null; 
     }

     // 텍스트 읽기
     const textLength = dataView.getUint32(offset, true);
     offset += 4;
     let text = null;
     if (textLength > 0) {
       const textBytes = new Uint8Array(arrayBuffer, offset, textLength);
       text = new TextDecoder().decode(textBytes);
       offset += textLength;
     }

     // 오디오 데이터 읽기
     const audioLength = dataView.getUint32(offset, true);
     offset += 4;
     let audioData = null;
     if (audioLength > 0) {
       audioData = new Uint8Array(arrayBuffer, offset, audioLength);
       offset += audioLength;
     }

     // 오디오 길이 읽기 (float)
     const audioDuration = dataView.getFloat32(offset, true);

-    return {
-      sessionId,
-      text,
-      audioData,
-      audioLength: audioDuration
-    };
+    return {
+      text,
+      audioData,
+      audioDuration
+    };
   } catch (e) {
     console.error("바이너리 메시지 파싱 오류:", e);
     return null;
   }
 }
@@
-        if (result) {
-          // 세션 ID 업데이트
-          if (result.sessionId) {
-            sessionId = result.sessionId;
-            updateSessionId(sessionId);
-          }
+        if (result) {
           let messageText = "";

Also applies to: 391-396

Comment on lines +222 to +225
function appendLog(text) {
chatLog.innerHTML += `<div>${text}</div>`;
chatLog.scrollTop = chatLog.scrollHeight;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

XSS 위험: innerHTML에 신뢰되지 않은 데이터 삽입
서버/사용자 입력이 그대로 innerHTML에 들어갑니다. 최소한 HTML 이스케이프 후 삽입하세요.

적용 diff:

+// HTML 이스케이프 유틸
+function escapeHtml(s) {
+  return String(s).replace(/[&<>"'`]/g, c => ({
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#39;',
+    '`': '&#96;'
+  }[c]));
+}
@@
-function appendLog(text) {
-  chatLog.innerHTML += `<div>${text}</div>`;
-  chatLog.scrollTop = chatLog.scrollHeight;
-}
+function appendLog(html) {
+  const div = document.createElement('div');
+  div.innerHTML = html; // 호출부에서 동적 값은 반드시 escapeHtml로 감쌉니다.
+  chatLog.appendChild(div);
+  chatLog.scrollTop = chatLog.scrollHeight;
+}
@@
-        appendLog(`<b>[로그인 성공]</b> 게스트 ID: ${guestId}`);
-        appendLog(`<small>사용자: ${data.user.username} (${data.user.email})</small>`);
+        appendLog(`<b>[로그인 성공]</b> 게스트 ID: ${escapeHtml(guestId)}`);
+        appendLog(`<small>사용자: ${escapeHtml(data.user.username)}</small>`);
@@
-    appendLog(`<span style="color:red">[로그인 오류] ${error.message}</span>`);
+    appendLog(`<span style="color:red">[로그인 오류] ${escapeHtml(error.message)}</span>`);
@@
-      appendLog(`<i>[오디오 재생 실패: ${error.message}]</i>`);
+      appendLog(`<i>[오디오 재생 실패: ${escapeHtml(error.message)}]</i>`);
@@
-              appendLog(`<i>[WebSocket 세션 확인] User ID: ${receivedUserId.substring(0, 8)}...</i>`);
+              appendLog(`<i>[WebSocket 세션 확인] User ID: ${escapeHtml(receivedUserId.substring(0, 8))}...</i>`);
@@
-              messageText += `<b>AI:</b> ${chatData.text}`;
+              messageText += `<b>AI:</b> ${escapeHtml(chatData.text)}`;
@@
-              appendLog(`<small style='color:#666'>[메타데이터: ${JSON.stringify(chatData.metadata)}]</small>`);
+              appendLog(`<small style='color:#666'>[메타데이터: ${escapeHtml(JSON.stringify(chatData.metadata))}]</small>`);
@@
-            messageText += `<b>AI:</b> ${data.text}`;
+            messageText += `<b>AI:</b> ${escapeHtml(data.text)}`;
@@
-            appendLog(`<small style='color:#666'>[메타데이터: ${JSON.stringify(data.metadata)}]</small>`);
+            appendLog(`<small style='color:#666'>[메타데이터: ${escapeHtml(JSON.stringify(data.metadata))}]</small>`);
@@
-        appendLog(`<b>AI:</b> ${event.data}`);
+        appendLog(`<b>AI:</b> ${escapeHtml(event.data)}`);
@@
-        appendLog(`<b>AI:</b> ${event.data}`);
+        appendLog(`<b>AI:</b> ${escapeHtml(event.data)}`);
@@
-          if (result.text) {
-            messageText += `<b>AI:</b> ${result.text}`;
-          }
+          if (result.text) {
+            messageText += `<b>AI:</b> ${escapeHtml(result.text)}`;
+          }
@@
-      appendLog(`<span style='color:red'>[HTTP 오류] ${err}</span>`);
+      appendLog(`<span style='color:red'>[HTTP 오류] ${escapeHtml(String(err))}</span>`);
@@
-  appendLog(`<b>나:</b> ${msg}`);
+  appendLog(`<b>나:</b> ${escapeHtml(msg)}`);
@@
-  appendLog(`<i>[캐릭터 변경됨: ${characterSelect.options[characterSelect.selectedIndex].text}]</i>`);
+  appendLog(`<i>[캐릭터 변경됨: ${escapeHtml(characterSelect.options[characterSelect.selectedIndex].text)}]</i>`);

Also applies to: 84-86, 100-104, 213-216, 262-264, 273-276, 319-321, 337-339, 367-369, 375-379, 399-401, 487-489, 456-457, 502-503

🤖 Prompt for AI Agents
In test-clients/ai-chat-client/script.js around lines 84-86, 100-104, 213-216,
222-225, 262-264, 273-276, 319-321, 337-339, 367-369, 375-379, 399-401, 456-457,
487-489, 502-503: the code inserts untrusted server/user input directly via
innerHTML, causing XSS risk; replace those innerHTML assignments with safe DOM
methods (createElement + textContent or
element.appendChild(document.createTextNode(...))) or apply a proper HTML-escape
function before inserting, and maintain existing scroll behavior by updating
scrollTop after appending the safe node.

Comment on lines +453 to +471
function sendChat() {
const msg = userInput.value.trim();
if (!msg) return;
appendLog(`<b>나:</b> ${msg}`);
userInput.value = "";

const payload = {
message: msg,
action: "chat",
character_id: characterSelect.value,
use_tts: includeAudioCheckbox.checked,
request_at: new Date().toISOString()
};

console.log(includeAudioCheckbox.checked)

const headers = { "Content-Type": "application/json" };
if (authToken) {
headers["Authorization"] = `Bearer ${authToken}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

미로그인 송신 방지 및 디버그 로그 제거
로그인 전 송신 가능, 콘솔 디버그 로그 남김.

적용 diff:

 function sendChat() {
   const msg = userInput.value.trim();
   if (!msg) return;
-  appendLog(`<b>나:</b> ${msg}`);
+  if (!isLoggedIn || !authToken) {
+    appendLog(`<span style='color:red'>[전송 불가] 로그인 후 이용하세요.</span>`);
+    return;
+  }
+  appendLog(`<b>나:</b> ${escapeHtml(msg)}`);
   userInput.value = "";
@@
-  console.log(includeAudioCheckbox.checked)
+  // 디버그 로그 제거
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function sendChat() {
const msg = userInput.value.trim();
if (!msg) return;
appendLog(`<b>나:</b> ${msg}`);
userInput.value = "";
const payload = {
message: msg,
action: "chat",
character_id: characterSelect.value,
use_tts: includeAudioCheckbox.checked,
request_at: new Date().toISOString()
};
console.log(includeAudioCheckbox.checked)
const headers = { "Content-Type": "application/json" };
if (authToken) {
headers["Authorization"] = `Bearer ${authToken}`;
function sendChat() {
const msg = userInput.value.trim();
if (!msg) return;
if (!isLoggedIn || !authToken) {
appendLog(`<span style='color:red'>[전송 불가] 로그인 후 이용하세요.</span>`);
return;
}
appendLog(`<b>나:</b> ${escapeHtml(msg)}`);
userInput.value = "";
const payload = {
message: msg,
action: "chat",
character_id: characterSelect.value,
use_tts: includeAudioCheckbox.checked,
request_at: new Date().toISOString()
};
// 디버그 로그 제거
const headers = { "Content-Type": "application/json" };
if (authToken) {
headers["Authorization"] = `Bearer ${authToken}`;
🤖 Prompt for AI Agents
In test-clients/ai-chat-client/script.js around lines 453 to 471, the sendChat
function currently allows sending before login and leaves a console.debug log;
prevent unauthenticated sends by checking authToken (or logged-in state) at the
top of sendChat and abort with a user-visible message/appendLog or disable the
send button when not logged in, and remove the stray
console.log(includeAudioCheckbox.checked) call; keep the existing Authorization
header logic (only add header when authToken exists).

Comment on lines +143 to +145
#audio-player {
display: none;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

오디오 표시 충돌: CSS가 항상 오디오를 숨깁니다
JS로 표시하려 해도 #audio-player가 CSS에서 display:none 고정이라 UI가 안 보입니다. 부모 #audio-section을 hidden으로 제어하는 방식으로 변경하세요.

적용 diff:

-#audio-player {
-  display: none;
-}
+/* #audio-section의 hidden 속성으로 표시/숨김을 제어합니다. */
+#audio-section[hidden] {
+  display: none;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#audio-player {
display: none;
}
/* #audio-section의 hidden 속성으로 표시/숨김을 제어합니다. */
#audio-section[hidden] {
display: none;
}
🤖 Prompt for AI Agents
test-clients/ai-chat-client/styles.css lines 143-145: current rule forces
#audio-player to always be hidden (display:none) which prevents JS from showing
it; remove this rule and instead control visibility via the parent container
(#audio-section) by toggling a hidden class or applying display:none on
#audio-section when you want to hide the player, leaving #audio-player free to
be shown by JS.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

@@ -0,0 +1,64 @@
using ProjectVG.Domain.Entities.ConversationHistorys;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

네임스페이스 오타: ConversationHistorys → ConversationHistory

컴파일 오류를 유발합니다. 즉시 수정 필요.

-using ProjectVG.Domain.Entities.ConversationHistorys;
+using ProjectVG.Domain.Entities.ConversationHistory;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using ProjectVG.Domain.Entities.ConversationHistorys;
using ProjectVG.Domain.Entities.ConversationHistory;
🤖 Prompt for AI Agents
In ProjectVG.Application/Models/Chat/ChatRequestCommand.cs at line 1, the using
directive references the misspelled namespace "ConversationHistorys" which
causes a compile error; change the using to the correct namespace
"ConversationHistory" and save the file, then rebuild to confirm the error is
resolved (also search the project for other references to "ConversationHistorys"
and correct them if present).

Comment on lines +1 to +64
using ProjectVG.Domain.Entities.ConversationHistorys;

namespace ProjectVG.Application.Models.Chat
{
public class ChatRequestCommand
{
/// === 요청 정보 ===
public Guid Id { get; }
public string UserPrompt { get; private set; } = string.Empty;
public Guid UserId { get; private set; }
public Guid CharacterId { get; private set; }
public DateTime UserRequestAt { get; private set; }
public DateTime ProcessAt { get; private set; }

/// === 요청 옵션 ===
public bool UseTTS { get; private set; } = true;

/// == 내부 처리 정보 ===
public IEnumerable<ConversationHistory>? ConversationHistory { get; private set; } = new List<ConversationHistory>();
public string UserIntent { get; private set; } = string.Empty;
public UserIntentType ProcessType { get; private set; } = UserIntentType.Undefined;
public double Cost { get; private set; }

public ChatRequestCommand()
{
Id = Guid.NewGuid();
ProcessAt = DateTime.UtcNow;
}

public ChatRequestCommand(Guid userId, Guid characterId, string userPrompt, DateTime requestedAt, bool useTTS)
{
Id = Guid.NewGuid();
UserId = userId;
CharacterId = characterId;
UserPrompt = userPrompt;
UserRequestAt = requestedAt;
ProcessAt = DateTime.UtcNow;
UseTTS = useTTS;
}

public void SetConversationHistory(IEnumerable<ConversationHistory> histories)
{
ConversationHistory = histories;
}

public void SetAnalysisResult(UserIntentType processType, string userIntent)
{
ProcessType = processType;
UserIntent = userIntent;
}

public void AddCost(double value)
{
Cost += value;
}
public string ToDebugString()
{
return $"[ChatRequestCommand] Id={Id}, UserId={UserId}, CharacterId={CharacterId}, " +
$"UserPrompt=\"{UserPrompt}\", UseTTS={UseTTS}, UserRequestAt={UserRequestAt:yyyy-MM-dd HH:mm:ss}, " +
$"ProcessAt={ProcessAt:yyyy-MM-dd HH:mm:ss}, ProcessType={ProcessType}, UserIntent=\"{UserIntent}\", Cost={Cost:F4}";
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

검증 스크립트: 오타 네임스페이스 전체 검색

레포 전반의 ConversationHistorys 사용 흔적을 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# 네임스페이스 오타 사용 위치 검색
rg -nP "ConversationHistorys|using\s+ProjectVG\.Domain\.Entities\.ConversationHistorys"

Length of output: 5421


ConversationHistorys 네임스페이스 복수형 오타 일괄 수정

  • ProjectVG.Domain.Entities.ConversationHistorysProjectVG.Domain.Entities.ConversationHistories로, 폴더명 및 모든 using·참조·마이그레이션 엔티티 이름 일괄 리팩터링 필요합니다.
🤖 Prompt for AI Agents
In ProjectVG.Application/Models/Chat/ChatRequestCommand.cs lines 1-64 the using
imports the misspelled namespace ProjectVG.Domain.Entities.ConversationHistorys;
update this file to use ProjectVG.Domain.Entities.ConversationHistories, then
refactor the domain folder, entity/type names, and all references across the
solution (usings, project/namespace declarations, DI registrations, tests, and
EF migrations) to the corrected plural form; also rename the physical folder and
any migration classes/tables if needed and run/adjust EF migrations to reflect
the renamed entity to keep consistency and compile-time correctness.

Comment on lines +101 to 107
return new ChatProcessContext(
command,
characterInfo,
conversationHistoryContext,
memoryContext
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

SessionId를 컨텍스트에 반영

위에서 제안한 SetSessionId를 이용해 컨텍스트에 세션 식별자를 주입하세요. 현재는 공백으로 남아 후속 전송/로그 상 식별이 어렵습니다.

-            return new ChatProcessContext(
+            var ctx = new ChatProcessContext(
                 command,
                 characterInfo, 
                 conversationHistoryContext, 
                 memoryContext
             );
+            ctx.SetSessionId(command.Id.ToString());
+            return ctx;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new ChatProcessContext(
command,
characterInfo,
conversationHistoryContext,
memoryContext
);
}
var ctx = new ChatProcessContext(
command,
characterInfo,
conversationHistoryContext,
memoryContext
);
ctx.SetSessionId(command.Id.ToString());
return ctx;
}
🤖 Prompt for AI Agents
In ProjectVG.Application/Services/Chat/ChatService.cs around lines 101 to 107,
the ChatProcessContext is created without a session identifier; use the proposed
SetSessionId to inject the session ID so downstream sends/logs can be
correlated. After constructing the ChatProcessContext (or during construction if
constructor supports it), obtain the session id from the incoming command (e.g.,
command.SessionId or equivalent) and call context.SetSessionId(sessionId) (or
pass it into the constructor), ensuring you handle null/empty by using a safe
default or validating before setting.

Comment on lines +118 to 123
await _chatSuccessHandler.HandleAsync(context);

using var scope = _scopeFactory.CreateScope();
var resultProcessor = scope.ServiceProvider.GetRequiredService<ChatResultProcessor>();
await resultProcessor.SendResultsAsync(context);
await resultProcessor.PersistResultsAsync(context);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

결과 전송 순서 및 DI 일관성 수정: 저장 후 전송, 스코프 재해결 제거

지속화 전에 전송하면 저장 실패 시 사용자에게만 노출되고 기록이 남지 않을 수 있습니다. 저장을 먼저 수행하고, 생성자 주입한 _resultProcessor를 재사용하세요.

-                await _chatSuccessHandler.HandleAsync(context);
-
-                using var scope = _scopeFactory.CreateScope();
-                var resultProcessor = scope.ServiceProvider.GetRequiredService<ChatResultProcessor>();
-                await resultProcessor.PersistResultsAsync(context);
+                await _resultProcessor.PersistResultsAsync(context);
+                await _chatSuccessHandler.HandleAsync(context);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await _chatSuccessHandler.HandleAsync(context);
using var scope = _scopeFactory.CreateScope();
var resultProcessor = scope.ServiceProvider.GetRequiredService<ChatResultProcessor>();
await resultProcessor.SendResultsAsync(context);
await resultProcessor.PersistResultsAsync(context);
}
await _resultProcessor.PersistResultsAsync(context);
await _chatSuccessHandler.HandleAsync(context);
}
🤖 Prompt for AI Agents
In ProjectVG.Application/Services/Chat/ChatService.cs around lines 118-123,
change the order so results are persisted before sending and stop re-resolving
ChatResultProcessor from a new scope: remove the "using var scope =
_scopeFactory.CreateScope();" and the ServiceProvider.GetRequiredService call,
instead call the constructor-injected field (e.g. _resultProcessor) and await
_resultProcessor.PersistResultsAsync(context) before awaiting
_chatSuccessHandler.HandleAsync(context); if _resultProcessor is not yet
injected, add it to the constructor and assign to a readonly field.

Comment on lines +134 to +142
private void LogChatRequestCommand(ChatRequestCommand command)
{
_logger.LogInformation("Starting chat process: {CommandInfo}", command.ToDebugString());
}

private void LogChatProcessContext(ChatProcessContext context)
{
_logger.LogInformation("Chat process completed: {ContextInfo}", context.ToDebugString());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

민감 정보 로그 레벨 하향 (Information → Debug)

ToDebugString()는 PII/전체 컨텍스트를 포함합니다. 운영 환경에서는 Debug 레벨로만 출력하세요.

-            _logger.LogInformation("Starting chat process: {CommandInfo}", command.ToDebugString());
+            _logger.LogDebug("Starting chat process: {CommandInfo}", command.ToDebugString());
...
-            _logger.LogInformation("Chat process completed: {ContextInfo}", context.ToDebugString());
+            _logger.LogDebug("Chat process completed: {ContextInfo}", context.ToDebugString());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void LogChatRequestCommand(ChatRequestCommand command)
{
_logger.LogInformation("Starting chat process: {CommandInfo}", command.ToDebugString());
}
private void LogChatProcessContext(ChatProcessContext context)
{
_logger.LogInformation("Chat process completed: {ContextInfo}", context.ToDebugString());
}
private void LogChatRequestCommand(ChatRequestCommand command)
{
_logger.LogDebug("Starting chat process: {CommandInfo}", command.ToDebugString());
}
private void LogChatProcessContext(ChatProcessContext context)
{
_logger.LogDebug("Chat process completed: {ContextInfo}", context.ToDebugString());
}
🤖 Prompt for AI Agents
In ProjectVG.Application/Services/Chat/ChatService.cs around lines 134 to 142,
the current LogChatRequestCommand and LogChatProcessContext methods write
sensitive PII/full context via ToDebugString() at Information level; change the
log level from _logger.LogInformation(...) to _logger.LogDebug(...) for both
calls so the detailed/contextual output is only emitted at Debug level
(optionally guard with _logger.IsEnabled(LogLevel.Debug) if you want to avoid
building the debug string when debug logging is disabled).

Comment on lines +96 to +116
[emotion:emotion_name]""dialogue""(action:action_name)""more dialogue""

private List<ChatMessageSegment> ParseChatResponseToSegments(string llmText, string? voiceId = null)
{
if (string.IsNullOrWhiteSpace(llmText))
return new List<ChatMessageSegment>();
## Available Options
**Available emotions:** {emotionList}
**Available actions:** {actionList}

string response = llmText.Trim();
var segments = new List<ChatMessageSegment>();
var seenTexts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
## Format Examples

var matches = Regex.Matches(response, @"\[(.*?)\]\s*([^\[]+)");
var emotionMap = GetEmotionMap(voiceId);
### Example 1 - Simple response with single dialogue
[emotion:neutral](action:tilting_head)""너 방금 뭐라 말했어?""

if (matches.Count > 0)
{
ProcessMatches(matches, emotionMap, segments, seenTexts);
}
else
{
var segment = ChatMessageSegment.CreateTextOnly(response, 0);
segment.Emotion = "neutral";
segments.Add(segment);
}
### Example 2 - Multiple dialogue segments
[emotion:neutral](action:sighing)""애휴 너 정말 멍청하구나?""""어떻게 하는지 내가 알려줄게""

return segments;
### Example 3 - Emotional response with complex actions
[emotion:confused](action:blushing)""바, 바보야! 그렇게 말하지 말라고...!""[emotion:shy](action:looking_away)""그렇게 말하면 부, 부끄럽잖아...""

### Example 4 - Action-focused response
[emotion:happy](action:clapping)""와! 정말 대단해!""(action:jumping)""너무 기뻐서 어떻게 해야 할지 모르겠어!""

## STRICT RULES - MUST BE FOLLOWED
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

예시 불일치: 지원되지 않는 액션 'jumping' 사용

CharacterConstants.SupportedActionsjumping이 없습니다. 예시를 지원 목록 내 값으로 교체하세요.

-### Example 4 - Action-focused response
-[emotion:happy](action:clapping)""와! 정말 대단해!""(action:jumping)""너무 기뻐서 어떻게 해야 할지 모르겠어!""
+### Example 4 - Action-focused response
+[emotion:happy](action:clapping)""와! 정말 대단해!""(action:waving)""너무 기뻐서 어떻게 해야 할지 모르겠어!""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[emotion:emotion_name]""dialogue""(action:action_name)""more dialogue""
private List<ChatMessageSegment> ParseChatResponseToSegments(string llmText, string? voiceId = null)
{
if (string.IsNullOrWhiteSpace(llmText))
return new List<ChatMessageSegment>();
## Available Options
**Available emotions:** {emotionList}
**Available actions:** {actionList}
string response = llmText.Trim();
var segments = new List<ChatMessageSegment>();
var seenTexts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
## Format Examples
var matches = Regex.Matches(response, @"\[(.*?)\]\s*([^\[]+)");
var emotionMap = GetEmotionMap(voiceId);
### Example 1 - Simple response with single dialogue
[emotion:neutral](action:tilting_head)""너 방금 뭐라 말했어?""
if (matches.Count > 0)
{
ProcessMatches(matches, emotionMap, segments, seenTexts);
}
else
{
var segment = ChatMessageSegment.CreateTextOnly(response, 0);
segment.Emotion = "neutral";
segments.Add(segment);
}
### Example 2 - Multiple dialogue segments
[emotion:neutral](action:sighing)""애휴 너 정말 멍청하구나?""""어떻게 하는지 내가 알려줄게""
return segments;
### Example 3 - Emotional response with complex actions
[emotion:confused](action:blushing)""바, 바보야! 그렇게 말하지 말라고...!""[emotion:shy](action:looking_away)""그렇게 말하면 부, 부끄럽잖아...""
### Example 4 - Action-focused response
[emotion:happy](action:clapping)""와! 정말 대단해!""(action:jumping)""너무 기뻐서 어떻게 해야 할지 모르겠어!""
## STRICT RULES - MUST BE FOLLOWED
### Example 4 - Action-focused response
[emotion:happy](action:clapping)""와! 정말 대단해!""(action:waving)""너무 기뻐서 어떻게 해야 할지 모르겠어!""
## STRICT RULES - MUST BE FOLLOWED
🤖 Prompt for AI Agents
In ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs around lines
96 to 116, the example uses an unsupported action 'jumping' which is not present
in CharacterConstants.SupportedActions; replace 'jumping' with one of the valid
actions from CharacterConstants.SupportedActions so the example matches
supported values, update the example text accordingly to use an allowed action
token, and ensure any related documentation or tests reference the same
supported action name.

Comment on lines +142 to 196
private List<ChatSegment> ParseCustomFormat(string response)
{
for (int i = 0; i < matches.Count; i++)
var segments = new List<ChatSegment>();
var currentEmotion = "neutral";
var order = 0;

try
{
var match = matches[i];
if (match.Groups.Count >= 3)
var position = 0;

while (position < response.Length)
{
var originalEmotion = match.Groups[1].Value.Trim();
var mappedEmotion = emotionMap != null && emotionMap.ContainsKey(originalEmotion)
? emotionMap[originalEmotion]
: originalEmotion;
var text = match.Groups[2].Value.Trim();
// Look for emotion pattern: [emotion:감정]
var emotionPattern = @"\[emotion:([^\]]+)\]";
var emotionMatch = Regex.Match(response.Substring(position), emotionPattern);

if (emotionMatch.Success && emotionMatch.Index == 0)
{
// Update current emotion
currentEmotion = emotionMatch.Groups[1].Value;
position += emotionMatch.Length;
continue;
}

// Look for text pattern: "텍스트"
var textPattern = "\"([^\"]+)\"";
var textMatch = Regex.Match(response.Substring(position), textPattern);

if (!seenTexts.Contains(text))
if (textMatch.Success && textMatch.Index == 0)
{
seenTexts.Add(text);
var segment = ChatMessageSegment.CreateTextOnly(text, segments.Count);
segment.Emotion = mappedEmotion;
segments.Add(segment);
// Create text segment with current emotion
var textContent = textMatch.Groups[1].Value;
var textSegment = ChatSegment.CreateText(textContent, currentEmotion, order++);
segments.Add(textSegment);
position += textMatch.Length;
continue;
}

// Look for action pattern: (action:액션)
var actionPattern = @"\(action:([^)]+)\)";
var actionMatch = Regex.Match(response.Substring(position), actionPattern);

if (actionMatch.Success && actionMatch.Index == 0)
{
// Create action segment
var actionContent = actionMatch.Groups[1].Value;
var actionSegment = ChatSegment.CreateAction(actionContent, order++);
segments.Add(actionSegment);
position += actionMatch.Length;
continue;
}

// If no pattern matched, advance position to avoid infinite loop
position++;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

파서 성능/정확도 개선: 정규식 재사용 및 허용 목록 검증

  • 매 반복마다 Substring/정규식 생성은 비효율적(O(n^2) 가능).
  • 감정/액션은 허용 목록으로 검증해 기본값 대체가 안전합니다.
-        private List<ChatSegment> ParseCustomFormat(string response)
+        private static readonly Regex EmotionRegex = new(@"\[emotion:([^\]]+)\]", RegexOptions.Compiled);
+        private static readonly Regex TextRegex = new("\"([^\"]+)\"", RegexOptions.Compiled);
+        private static readonly Regex ActionRegex = new(@"\(action:([^)]+)\)", RegexOptions.Compiled);
+
+        private List<ChatSegment> ParseCustomFormat(string response)
         {
             var segments = new List<ChatSegment>();
             var currentEmotion = "neutral";
             var order = 0;
 
             try 
             {
-                var position = 0;
+                var position = 0;
                 
                 while (position < response.Length)
                 {
-                    // Look for emotion pattern: [emotion:감정]
-                    var emotionPattern = @"\[emotion:([^\]]+)\]";
-                    var emotionMatch = Regex.Match(response.Substring(position), emotionPattern);
+                    // emotion
+                    var emotionMatch = EmotionRegex.Match(response, position);
                     
-                    if (emotionMatch.Success && emotionMatch.Index == 0)
+                    if (emotionMatch.Success && emotionMatch.Index == position)
                     {
-                        // Update current emotion
-                        currentEmotion = emotionMatch.Groups[1].Value;
+                        var candidate = emotionMatch.Groups[1].Value;
+                        currentEmotion = CharacterConstants.SupportedEmotions.Contains(candidate) ? candidate : "neutral";
                         position += emotionMatch.Length;
                         continue;
                     }
 
-                    // Look for text pattern: "텍스트"
-                    var textPattern = "\"([^\"]+)\"";
-                    var textMatch = Regex.Match(response.Substring(position), textPattern);
+                    // "텍스트"
+                    var textMatch = TextRegex.Match(response, position);
                     
-                    if (textMatch.Success && textMatch.Index == 0)
+                    if (textMatch.Success && textMatch.Index == position)
                     {
                         // Create text segment with current emotion
                         var textContent = textMatch.Groups[1].Value;
                         var textSegment = ChatSegment.CreateText(textContent, currentEmotion, order++);
                         segments.Add(textSegment);
                         position += textMatch.Length;
                         continue;
                     }
 
-                    // Look for action pattern: (action:액션)
-                    var actionPattern = @"\(action:([^)]+)\)";
-                    var actionMatch = Regex.Match(response.Substring(position), actionPattern);
+                    // (action:액션)
+                    var actionMatch = ActionRegex.Match(response, position);
                     
-                    if (actionMatch.Success && actionMatch.Index == 0)
+                    if (actionMatch.Success && actionMatch.Index == position)
                     {
-                        // Create action segment
-                        var actionContent = actionMatch.Groups[1].Value;
+                        var actionContent = actionMatch.Groups[1].Value;
+                        if (!CharacterConstants.SupportedActions.Contains(actionContent))
+                        {
+                            position += actionMatch.Length;
+                            continue; // 미지원 액션은 스킵
+                        }
                         var actionSegment = ChatSegment.CreateAction(actionContent, order++);
                         segments.Add(actionSegment);
                         position += actionMatch.Length;
                         continue;
                     }
 
                     // If no pattern matched, advance position to avoid infinite loop
                     position++;
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private List<ChatSegment> ParseCustomFormat(string response)
{
for (int i = 0; i < matches.Count; i++)
var segments = new List<ChatSegment>();
var currentEmotion = "neutral";
var order = 0;
try
{
var match = matches[i];
if (match.Groups.Count >= 3)
var position = 0;
while (position < response.Length)
{
var originalEmotion = match.Groups[1].Value.Trim();
var mappedEmotion = emotionMap != null && emotionMap.ContainsKey(originalEmotion)
? emotionMap[originalEmotion]
: originalEmotion;
var text = match.Groups[2].Value.Trim();
// Look for emotion pattern: [emotion:감정]
var emotionPattern = @"\[emotion:([^\]]+)\]";
var emotionMatch = Regex.Match(response.Substring(position), emotionPattern);
if (emotionMatch.Success && emotionMatch.Index == 0)
{
// Update current emotion
currentEmotion = emotionMatch.Groups[1].Value;
position += emotionMatch.Length;
continue;
}
// Look for text pattern: "텍스트"
var textPattern = "\"([^\"]+)\"";
var textMatch = Regex.Match(response.Substring(position), textPattern);
if (!seenTexts.Contains(text))
if (textMatch.Success && textMatch.Index == 0)
{
seenTexts.Add(text);
var segment = ChatMessageSegment.CreateTextOnly(text, segments.Count);
segment.Emotion = mappedEmotion;
segments.Add(segment);
// Create text segment with current emotion
var textContent = textMatch.Groups[1].Value;
var textSegment = ChatSegment.CreateText(textContent, currentEmotion, order++);
segments.Add(textSegment);
position += textMatch.Length;
continue;
}
// Look for action pattern: (action:액션)
var actionPattern = @"\(action:([^)]+)\)";
var actionMatch = Regex.Match(response.Substring(position), actionPattern);
if (actionMatch.Success && actionMatch.Index == 0)
{
// Create action segment
var actionContent = actionMatch.Groups[1].Value;
var actionSegment = ChatSegment.CreateAction(actionContent, order++);
segments.Add(actionSegment);
position += actionMatch.Length;
continue;
}
// If no pattern matched, advance position to avoid infinite loop
position++;
}
private static readonly Regex EmotionRegex = new(@"\[emotion:([^\]]+)\]", RegexOptions.Compiled);
private static readonly Regex TextRegex = new("\"([^\"]+)\"", RegexOptions.Compiled);
private static readonly Regex ActionRegex = new(@"\(action:([^)]+)\)", RegexOptions.Compiled);
private List<ChatSegment> ParseCustomFormat(string response)
{
var segments = new List<ChatSegment>();
var currentEmotion = "neutral";
var order = 0;
try
{
var position = 0;
while (position < response.Length)
{
// emotion
var emotionMatch = EmotionRegex.Match(response, position);
if (emotionMatch.Success && emotionMatch.Index == position)
{
var candidate = emotionMatch.Groups[1].Value;
currentEmotion = CharacterConstants.SupportedEmotions.Contains(candidate)
? candidate
: "neutral";
position += emotionMatch.Length;
continue;
}
// "텍스트"
var textMatch = TextRegex.Match(response, position);
if (textMatch.Success && textMatch.Index == position)
{
var textContent = textMatch.Groups[1].Value;
var textSegment = ChatSegment.CreateText(textContent, currentEmotion, order++);
segments.Add(textSegment);
position += textMatch.Length;
continue;
}
// (action:액션)
var actionMatch = ActionRegex.Match(response, position);
if (actionMatch.Success && actionMatch.Index == position)
{
var actionContent = actionMatch.Groups[1].Value;
if (!CharacterConstants.SupportedActions.Contains(actionContent))
{
position += actionMatch.Length;
continue; // 미지원 액션은 스킵
}
var actionSegment = ChatSegment.CreateAction(actionContent, order++);
segments.Add(actionSegment);
position += actionMatch.Length;
continue;
}
// If no pattern matched, advance position to avoid infinite loop
position++;
}
🤖 Prompt for AI Agents
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs around lines
142-196: the parser currently calls Substring and constructs Regex objects on
every loop which is slow and risks O(n^2) behavior; also emotion and action
values are not validated against an allow-list. Fix by precompiling the three
Regex patterns as static readonly Regex instances (use RegexOptions.Compiled |
RegexOptions.CultureInvariant) and use their Match overloads that accept a
startIndex (avoid Substring and manual index arithmetic), validate captured
emotion/action values against a configured allow-list (fall back to "neutral" or
ignore/create no segment for invalid values), and increment position using
match.Length returned by those Match calls to preserve behavior while improving
performance and correctness.

Comment on lines +35 to 70
_logger?.LogWarning("ACTION 파싱 실패: {ActionStr}", actionStr);
return CreateDefaultValidResponse();
if (!response.TryGetValue("PROCESS_TYPE", out var processTypeStr) ||
!int.TryParse(processTypeStr, out var processTypeValue)) {
return (UserIntentType.Chat, "일반적인 대화");
}

var action = (UserInputAction)actionValue;

// 액션별 처리 로직
return action switch
{
UserInputAction.Ignore => ParseIgnoreAction(response),
UserInputAction.Reject => ParseRejectAction(response),
UserInputAction.Chat => ParseChatAction(response),
UserInputAction.Undefined => ParseChatAction(response),
_ => CreateDefaultValidResponse()
};
}
catch (Exception ex)
{
_logger?.LogError(ex, "LLM 응답 파싱 중 예외 발생: {Response}", llmResponse);
return CreateDefaultValidResponse();
}
}

private UserInputAnalysis ParseIgnoreAction(Dictionary<string, string> response)
{
var failureReason = response.GetValueOrDefault("FAILURE_REASON", "잘못된 입력");
_logger?.LogDebug("무시 액션 파싱: {Reason}", failureReason);
return UserInputAnalysis.CreateIgnore(failureReason);
}

private UserInputAnalysis ParseRejectAction(Dictionary<string, string> response)
{
var failureReason = response.GetValueOrDefault("FAILURE_REASON", "부적절한 요청");
_logger?.LogDebug("거절 액션 파싱: {Reason}", failureReason);
return UserInputAnalysis.CreateReject(failureReason);
}



private UserInputAnalysis ParseChatAction(Dictionary<string, string> response)
{
var conversationContext = response.GetValueOrDefault("CONTEXT", "일반적인 대화");
var userIntent = response.GetValueOrDefault("INTENT", "대화");
var enhancedQuery = response.GetValueOrDefault("ENHANCED_QUERY", "");

// 키워드 파싱
var keywords = ParseKeywords(response.GetValueOrDefault("KEYWORDS", ""));

// 컨텍스트 시간 파싱
var contextTime = ParseContextTime(response.GetValueOrDefault("CONTEXT_TIME", ""));

_logger?.LogDebug("대화 액션 파싱: 맥락={Context}, 의도={Intent}, 키워드={Keywords}",
conversationContext, userIntent, string.Join(",", keywords));

return UserInputAnalysis.CreateValid(
conversationContext,
userIntent,
UserInputAction.Chat,
keywords,
enhancedQuery,
contextTime);
}

private List<string> ParseKeywords(string keywordsStr)
{
if (string.IsNullOrEmpty(keywordsStr)) return new List<string>();

return keywordsStr.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();
}
var processType = (UserIntentType)processTypeValue;
var intent = response.GetValueOrDefault("INTENT", "일반적인 대화");

private DateTime? ParseContextTime(string timeStr)
{
if (string.IsNullOrEmpty(timeStr) || timeStr == "null") return null;

if (DateTime.TryParse(timeStr, out var parsedTime))
{
return parsedTime;
return (processType, intent);
}
catch (Exception) {
return (UserIntentType.Chat, "일반적인 대화");
}

_logger?.LogWarning("시간 파싱 실패: {TimeStr}", timeStr);
return null;
}

private UserInputAnalysis CreateDefaultValidResponse()
{
_logger?.LogInformation("기본 유효 응답 생성");
return UserInputAnalysis.CreateValid("일반적인 대화", "대화", UserInputAction.Chat, new List<string>());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

파싱 견고성 향상: 키 대소문자/범위 검증/따옴표 트림

LLM 출력 편차에 대비해 case-insensitive 키 처리, 유효한 enum 범위 확인, INTENT 양끝 따옴표 제거를 권장합니다.

-            try {
-                var lines = llmResponse.Split('\n', StringSplitOptions.RemoveEmptyEntries);
-                var response = new Dictionary<string, string>();
+            try {
+                var lines = llmResponse.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+                var response = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
...
-                        if (key == "PROCESS_TYPE" || key == "INTENT") {
+                        if (string.Equals(key, "PROCESS_TYPE", StringComparison.OrdinalIgnoreCase) ||
+                            string.Equals(key, "INTENT", StringComparison.OrdinalIgnoreCase)) {
                             response[key] = value;
                         }
...
-                if (!response.TryGetValue("PROCESS_TYPE", out var processTypeStr) ||
-                    !int.TryParse(processTypeStr, out var processTypeValue)) {
+                if (!response.TryGetValue("PROCESS_TYPE", out var processTypeStr) ||
+                    !int.TryParse(processTypeStr, out var processTypeValue) ||
+                    !Enum.IsDefined(typeof(UserIntentType), processTypeValue)) {
                     return (UserIntentType.Chat, "일반적인 대화");
                 }
 
                 var processType = (UserIntentType)processTypeValue;
-                var intent = response.GetValueOrDefault("INTENT", "일반적인 대화");
+                var intent = response.GetValueOrDefault("INTENT", "일반적인 대화") ?? "일반적인 대화";
+                intent = intent.Trim().Trim('"');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public (UserIntentType ProcessType, string Intent) Parse(string llmResponse, string input)
{
try
{
_logger?.LogDebug("LLM 응답 파싱 시작: {Response}", llmResponse);
try {
var lines = llmResponse.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var response = new Dictionary<string, string>();
// 각 라인을 파싱하여 키-값 쌍으로 저장
foreach (var line in lines)
{
// 각 라인을 파싱하여 키-값 쌍으로 저장 (값 내 콜론 허용)
foreach (var line in lines) {
var trimmedLine = line.Trim();
if (string.IsNullOrEmpty(trimmedLine)) continue;
var colonIndex = trimmedLine.IndexOf(':');
if (colonIndex > 0)
{
if (colonIndex > 0 && colonIndex < trimmedLine.Length - 1) {
var key = trimmedLine.Substring(0, colonIndex).Trim();
var value = trimmedLine.Substring(colonIndex + 1).Trim();
response[key] = value;
if (key == "PROCESS_TYPE" || key == "INTENT") {
response[key] = value;
}
}
}
// 필수 필드인 ACTION 파싱
if (!response.TryGetValue("ACTION", out var actionStr) ||
!int.TryParse(actionStr, out var actionValue))
{
_logger?.LogWarning("ACTION 파싱 실패: {ActionStr}", actionStr);
return CreateDefaultValidResponse();
if (!response.TryGetValue("PROCESS_TYPE", out var processTypeStr) ||
!int.TryParse(processTypeStr, out var processTypeValue)) {
return (UserIntentType.Chat, "일반적인 대화");
}
var action = (UserInputAction)actionValue;
// 액션별 처리 로직
return action switch
{
UserInputAction.Ignore => ParseIgnoreAction(response),
UserInputAction.Reject => ParseRejectAction(response),
UserInputAction.Chat => ParseChatAction(response),
UserInputAction.Undefined => ParseChatAction(response),
_ => CreateDefaultValidResponse()
};
}
catch (Exception ex)
{
_logger?.LogError(ex, "LLM 응답 파싱 중 예외 발생: {Response}", llmResponse);
return CreateDefaultValidResponse();
}
}
private UserInputAnalysis ParseIgnoreAction(Dictionary<string, string> response)
{
var failureReason = response.GetValueOrDefault("FAILURE_REASON", "잘못된 입력");
_logger?.LogDebug("무시 액션 파싱: {Reason}", failureReason);
return UserInputAnalysis.CreateIgnore(failureReason);
}
private UserInputAnalysis ParseRejectAction(Dictionary<string, string> response)
{
var failureReason = response.GetValueOrDefault("FAILURE_REASON", "부적절한 요청");
_logger?.LogDebug("거절 액션 파싱: {Reason}", failureReason);
return UserInputAnalysis.CreateReject(failureReason);
}
private UserInputAnalysis ParseChatAction(Dictionary<string, string> response)
{
var conversationContext = response.GetValueOrDefault("CONTEXT", "일반적인 대화");
var userIntent = response.GetValueOrDefault("INTENT", "대화");
var enhancedQuery = response.GetValueOrDefault("ENHANCED_QUERY", "");
// 키워드 파싱
var keywords = ParseKeywords(response.GetValueOrDefault("KEYWORDS", ""));
// 컨텍스트 시간 파싱
var contextTime = ParseContextTime(response.GetValueOrDefault("CONTEXT_TIME", ""));
_logger?.LogDebug("대화 액션 파싱: 맥락={Context}, 의도={Intent}, 키워드={Keywords}",
conversationContext, userIntent, string.Join(",", keywords));
return UserInputAnalysis.CreateValid(
conversationContext,
userIntent,
UserInputAction.Chat,
keywords,
enhancedQuery,
contextTime);
}
private List<string> ParseKeywords(string keywordsStr)
{
if (string.IsNullOrEmpty(keywordsStr)) return new List<string>();
return keywordsStr.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();
}
var processType = (UserIntentType)processTypeValue;
var intent = response.GetValueOrDefault("INTENT", "일반적인 대화");
private DateTime? ParseContextTime(string timeStr)
{
if (string.IsNullOrEmpty(timeStr) || timeStr == "null") return null;
if (DateTime.TryParse(timeStr, out var parsedTime))
{
return parsedTime;
return (processType, intent);
}
catch (Exception) {
return (UserIntentType.Chat, "일반적인 대화");
}
_logger?.LogWarning("시간 파싱 실패: {TimeStr}", timeStr);
return null;
}
private UserInputAnalysis CreateDefaultValidResponse()
{
_logger?.LogInformation("기본 유효 응답 생성");
return UserInputAnalysis.CreateValid("일반적인 대화", "대화", UserInputAction.Chat, new List<string>());
}
public (UserIntentType ProcessType, string Intent) Parse(string llmResponse, string input)
{
try {
var lines = llmResponse.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var response = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// 각 라인을 파싱하여 키-값 쌍으로 저장 (값 내 콜론 허용)
foreach (var line in lines) {
var trimmedLine = line.Trim();
if (string.IsNullOrEmpty(trimmedLine)) continue;
var colonIndex = trimmedLine.IndexOf(':');
if (colonIndex > 0 && colonIndex < trimmedLine.Length - 1) {
var key = trimmedLine.Substring(0, colonIndex).Trim();
var value = trimmedLine.Substring(colonIndex + 1).Trim();
if (string.Equals(key, "PROCESS_TYPE", StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, "INTENT", StringComparison.OrdinalIgnoreCase)) {
response[key] = value;
}
}
}
if (!response.TryGetValue("PROCESS_TYPE", out var processTypeStr) ||
!int.TryParse(processTypeStr, out var processTypeValue) ||
!Enum.IsDefined(typeof(UserIntentType), processTypeValue)) {
return (UserIntentType.Chat, "일반적인 대화");
}
var processType = (UserIntentType)processTypeValue;
var intent = response.GetValueOrDefault("INTENT", "일반적인 대화") ?? "일반적인 대화";
intent = intent.Trim().Trim('"');
return (processType, intent);
}
catch (Exception) {
return (UserIntentType.Chat, "일반적인 대화");
}
}
🤖 Prompt for AI Agents
In ProjectVG.Application/Services/Chat/Factories/UserInputAnalysisLLMFormat.cs
around lines 35 to 70, make the parser more robust by handling case-insensitive
keys, validating the parsed PROCESS_TYPE is a defined UserIntentType, and
trimming surrounding quotes from INTENT (and PROCESS_TYPE if quoted) before
parsing; implement key normalization (e.g., ToUpperInvariant) when extracting
keys, strip leading/trailing quotes from values, TryParse the PROCESS_TYPE after
trimming quotes and then check Enum.IsDefined or a safe range before casting,
and fall back to (UserIntentType.Chat, "일반적인 대화") on any invalid or out-of-range
value.

Comment on lines +39 to +41
_logger.LogDebug("LLM 요청 시작: {Model}, 사용자 메시지: {UserPrompt}",
request.Model,
request.UserMessage[..Math.Min(50, request.UserMessage.Length)]);
request.UserPrompt[..Math.Min(50, request.UserPrompt.Length)]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

NRE/PII 로그 리스크: UserPrompt 안전 슬라이싱 적용 필요

UserPrompt가 null일 경우 NRE가 발생합니다. 또한 원문 전체가 로그에 남지 않도록 미리보기 텍스트를 별도 변수로 안전 처리하세요.

적용 diff:

-                _logger.LogDebug("LLM 요청 시작: {Model}, 사용자 메시지: {UserPrompt}", 
-                    request.Model, 
-                    request.UserPrompt[..Math.Min(50, request.UserPrompt.Length)]);
+                var userPromptPreview = request.UserPrompt ?? string.Empty;
+                if (userPromptPreview.Length > 50) userPromptPreview = userPromptPreview[..50];
+                _logger.LogDebug("LLM 요청 시작: {Model}, 사용자 메시지(50자 미리보기): {UserPrompt}",
+                    request.Model,
+                    userPromptPreview);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_logger.LogDebug("LLM 요청 시작: {Model}, 사용자 메시지: {UserPrompt}",
request.Model,
request.UserMessage[..Math.Min(50, request.UserMessage.Length)]);
request.UserPrompt[..Math.Min(50, request.UserPrompt.Length)]);
var userPromptPreview = request.UserPrompt ?? string.Empty;
if (userPromptPreview.Length > 50)
userPromptPreview = userPromptPreview[..50];
_logger.LogDebug(
"LLM 요청 시작: {Model}, 사용자 메시지(50자 미리보기): {UserPrompt}",
request.Model,
userPromptPreview);
🤖 Prompt for AI Agents
In ProjectVG.Infrastructure/Integrations/LLMClient/LLMClient.cs around lines
39-41, the current logging slices request.UserPrompt directly which can throw an
NRE and may expose full PII; create a separate safe preview variable (e.g., var
prompt = request.UserPrompt ?? string.Empty), produce a trimmed/normalized
preview of up to 50 characters using Math.Min or span-slicing, optionally
replace newlines and/or mask sensitive content, and then log that preview
instead of slicing request.UserPrompt inline to avoid NREs and reduce PII
exposure.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
ProjectVG.Infrastructure/Persistence/EfCore/Data/DesignTimeDbContextFactory.cs (1)

1-5: 컴파일 오류: System 네임스페이스 없이 Environment 사용

파일 상단에 System using이 없어 현재 코드는 빌드 실패합니다. 아래 한 줄을 추가하세요.

using System;
scripts/migrate.ps1 (1)

9-16: migrations add의 비반복성 해결 — 기존 마이그레이션 존재 시 add 생략

고정 이름 InitialCreate는 반복 실행 시 실패합니다. 기존 마이그레이션이 있으면 add를 건너뛰도록 만들어 스크립트를 idempotent하게 합시다. (위 경로 변수 제안과 함께 사용)

-# 1. 마이그레이션 생성
-dotnet ef migrations add InitialCreate --project "./ProjectVG.Infrastructure" --startup-project "./ProjectVG.Api"
-
-if ($LASTEXITCODE -ne 0) {
-    Write-Error "Migration 생성 실패"
-    exit $LASTEXITCODE
-}
+# 1. 마이그레이션 생성 (기존 마이그레이션이 없을 때만)
+$migrationsPath = Join-Path $infraProj "Migrations"
+$hasMigrations = Test-Path "$migrationsPath\*.cs"
+if (-not $hasMigrations) {
+    dotnet ef migrations add InitialCreate --project $infraProj --startup-project $apiProj
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "Migration 생성 실패"
+        exit $LASTEXITCODE
+    }
+} else {
+    Write-Host "기존 마이그레이션 감지: add 단계 건너뜀 ($migrationsPath)"
+}
ProjectVG.Infrastructure/Migrations/20250828173942_InitialCreate.cs (2)

45-57: Users: 식별·상태 제약 보강 필요

  • UID는 고유 인덱스가 있으나 허용 문자/길이 검증이 없습니다.
  • Status는 int(ENUM 추정)인데 값 범위 제약/기본값이 없습니다.
  • ProviderId는 단독 인덱스뿐이라, 다른 Provider에서 동일 ProviderId가 충돌할 수 있습니다(일반적으로 (Provider, ProviderId) Unique 필요).
- migrationBuilder.CreateIndex(
-     name: "IX_Users_ProviderId",
-     table: "Users",
-     column: "ProviderId");
+ // 단일 인덱스 대신 복합 Unique
+ migrationBuilder.CreateIndex(
+     name: "UX_Users_Provider_ProviderId",
+     table: "Users",
+     columns: new[] { "Provider", "ProviderId" },
+     unique: true);
+
+ migrationBuilder.AddCheckConstraint(
+   name: "CK_Users_UID_Format",
+   table: "Users",
+   sql: "LEN([UID]) BETWEEN 3 AND 16 AND [UID] NOT LIKE '%[^0-9A-Za-z_-]%'");
+
+ migrationBuilder.AddCheckConstraint(
+   name: "CK_Users_Status_Range",
+   table: "Users",
+   sql: "[Status] IN (0,1,2)"); -- 필요 값 집합으로 조정

선택: Username 검색이 잦다면 비고유 인덱스(IX_Users_Username) 추가 고려.


59-73: 대화 이력 스키마: 긴 콘텐츠/JSON 유효성/소프트삭제 쿼리 최적화

  • Content 4000자는 대화 기록에 부족할 수 있습니다. nvarchar(max) 권장.
  • MetadataJson은 JSON이지만 형식 검증 없음 → ISJSON 체크 권장.
  • IsDeleted가 있으나 필터드 인덱스가 없어 조회 핫패스 비효율 우려.
- Content = table.Column<string>(type: "nvarchar(4000)", maxLength: 4000, nullable: false),
+ Content = table.Column<string>(type: "nvarchar(max)", nullable: false),

- MetadataJson = table.Column<string>(type: "nvarchar(4000)", maxLength: 4000, nullable: false),
+ MetadataJson = table.Column<string>(type: "nvarchar(max)", nullable: false),
+ migrationBuilder.AddCheckConstraint(
+   name: "CK_ConversationHistories_MetadataJson_IsJson",
+   table: "ConversationHistories",
+   sql: "ISJSON([MetadataJson]) = 1");
+
+ migrationBuilder.CreateIndex(
+   name: "IX_ConversationHistories_UserId_CharacterId_Timestamp_NotDeleted",
+   table: "ConversationHistories",
+   columns: new[] { "UserId", "CharacterId", "Timestamp" },
+   filter: "[IsDeleted] = 0");
♻️ Duplicate comments (2)
test-clients/ai-chat-client/index.html (1)

54-60: 오디오 섹션이 영구 숨김 상태(표시 불가) — 부모 컨테이너 토글 필요

부모 #audio-sectionstyle="display: none;"로 고정 숨김이라 JS가 오디오만 토글해도 UI가 나타나지 않습니다. 부모 컨테이너에 hidden 속성을 사용하고, JS에서는 #audio-section.hidden을 토글하도록 바꾸세요.

적용 diff:

-    <div id="audio-section" style="display: none;">
+    <div id="audio-section" hidden>
       <div style="display: flex; align-items: center; gap: 0.5em; margin-bottom: 0.5em;">
         <span style="color: #4caf50; font-weight: 500;">🔊</span>
         <span style="color: #37474f; font-size: 0.9em;">음성 재생 중...</span>
       </div>
       <audio id="audio-player" controls style="width: 100%;"></audio>
     </div>

스크립트 측(참고, 별도 파일)에서는 document.getElementById('audio-section').hidden = !shouldShow;처럼 컨테이너를 토글하세요.

ProjectVG.Application/Services/Auth/AuthService.cs (1)

43-47: 게스트 이메일 형식: 서브도메인 대신 plus-tag 사용 권장(중복 코멘트)

유니크 확보 목적이라면 guest+{token}@guest.local 형식이 단일 도메인 내 관리·검증이 용이합니다. 현재 guest@guest{uuid}.local은 서브도메인 폭증과 검증 복잡도를 야기합니다.

-                        Username: $"guest_{uuid}",
-                        Email: $"guest@guest{uuid}.local",
+                        Username: $"guest_{uuid}",
+                        Email: $"guest+{uuid}@guest.local",
🧹 Nitpick comments (13)
test-clients/ai-chat-client/index.html (3)

5-8: 모바일 대응을 위한 viewport 메타 추가 권장

반응형 렌더링을 위해 viewport 메타 태그를 추가하세요.

   <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>AI 채팅 + 음성 데모</title>

40-41: 채팅 로그 접근성 개선: 실시간 갱신 안내

새 메시지 도착을 스크린 리더가 인지하도록 aria-live를 추가하세요.

-    <div id="chat-log"></div>
+    <div id="chat-log" aria-live="polite" aria-atomic="false"></div>

25-27: 게스트 ID 입력 UX 개선: 자동완성 힌트 추가

브라우저 자동완성 지원을 위해 적절한 autocomplete 힌트를 부여하세요.

-        <input id="guest-id" type="text" placeholder="게스트 ID를 입력하세요" value="test-guest-123" 
+        <input id="guest-id" type="text" placeholder="게스트 ID를 입력하세요" value="test-guest-123" autocomplete="username"
                style="flex: 1; padding: 0.75em; border-radius: 8px; border: 1px solid #c8e6c9; font-size: 1em;" />
ProjectVG.Infrastructure/Persistence/EfCore/Data/DesignTimeDbContextFactory.cs (1)

29-33: 환경별 설정(appsettings.{ENV}.json) 병합 권장

디자인 타임에도 환경별 설정을 합치면 로컬/CI 오작동을 줄일 수 있습니다.

             var configuration = new ConfigurationBuilder()
                 .SetBasePath(basePath)
                 .AddJsonFile("appsettings.json", optional: true)
+                .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true)
                 .AddEnvironmentVariables()
                 .Build();
scripts/migrate.ps1 (2)

2-2: 드롭 명령에 --no-build 추가로 실행 시간 단축

이미 빌드된 상태에서 불필요한 리빌드를 피할 수 있습니다.

-dotnet ef database drop --project "./ProjectVG.Infrastructure" --startup-project "./ProjectVG.Api" --force
+dotnet ef database drop --project "./ProjectVG.Infrastructure" --startup-project "./ProjectVG.Api" --force --no-build

2-2: $PSScriptRoot 기준 경로 사용으로 실행 위치 독립성 확보

현재는 CWD에 종속적입니다. 스크립트 위치 기준 절대 경로로 치환하면 어디서 실행해도 안전합니다.

+# 스크립트 기준 프로젝트 경로
+$infraProj = (Resolve-Path (Join-Path $PSScriptRoot "..\ProjectVG.Infrastructure")).Path
+$apiProj   = (Resolve-Path (Join-Path $PSScriptRoot "..\ProjectVG.Api")).Path
@@
-dotnet ef database drop --project "./ProjectVG.Infrastructure" --startup-project "./ProjectVG.Api" --force
+dotnet ef database drop --project $infraProj --startup-project $apiProj --force
@@
-dotnet ef migrations add InitialCreate --project "./ProjectVG.Infrastructure" --startup-project "./ProjectVG.Api"
+dotnet ef migrations add InitialCreate --project $infraProj --startup-project $apiProj
@@
-dotnet ef database update --project "./ProjectVG.Infrastructure" --startup-project "./ProjectVG.Api"
+dotnet ef database update --project $infraProj --startup-project $apiProj

Also applies to: 10-10, 18-18

ProjectVG.Infrastructure/Migrations/20250828173942_InitialCreate.cs (3)

91-98: 시드 데이터: 필수 컬럼에 빈 문자열

Summary/UserAlias/ImageUrl이 NOT NULL인데 “”로 시드됩니다. 도메인 무결성 측면에서 최소한의 유효 값 또는 NULL 허용이 필요합니다. 현재 데이터로는 “필수지만 의미 없음” 상태가 됩니다.

시드 값 보완 또는 해당 컬럼을 NULL 허용(+도메인 레벨에서 필수 검증) 중 택일 권장.


136-144: UID 고유 인덱스: LGTM + 포함 컬럼 고려

UID Unique 인덱스는 적절합니다. UID 기반 인증 조회에 Email/Status가 함께 필요하다면 INCLUDE(Email, Status) 포함 인덱스로 확장 고려.


1-1: BOM/주석 사소한 정리

파일 시작의 BOM 표시와 pragma 주석 형식 불일치가 보입니다. 팀 코드스타일 규칙에 맞춰 통일 권장.

Also applies to: 6-6

ProjectVG.Application/Services/Auth/AuthService.cs (1)

143-150: GenerateGuestUuid: 케이스 정규화 및 API 간소화

입력 케이스 차이로 다른 토큰이 나오는 것을 방지하고, 정적 HashData API로 간결화하세요.

-        private static string GenerateGuestUuid(string providerUserId)
+        private static string GenerateGuestUuid(string providerUserId)
         {
-            // SHA256 해시를 사용하여 일관된 UUID 생성
-            using var sha256 = System.Security.Cryptography.SHA256.Create();
-            var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(providerUserId));
-            var hashString = Convert.ToHexString(hash);
-            return hashString.Substring(0, Math.Min(hashString.Length, 16)).ToLowerInvariant();
+            // SHA256 해시를 사용하여 일관된 토큰 생성(소문자/트림 정규화)
+            var bytes = System.Text.Encoding.UTF8.GetBytes(providerUserId.Trim().ToLowerInvariant());
+            var hash = System.Security.Cryptography.SHA256.HashData(bytes);
+            var hex = Convert.ToHexString(hash).ToLowerInvariant();
+            return hex[..16];
         }
ProjectVG.Tests/Services/Chat/Factories/ChatLLMFormatTests.cs (3)

408-424: 섹션 커버리지 보강 제안: 요일 정보까지 확인

SystemMessage가 요일도 포함하도록 구현되어 있는데(“Day of Week: …”), 본 테스트는 이를 검증하지 않습니다. 작은 추가로 회귀를 더 잘 잡을 수 있습니다.

             result.Should().Contain("Current Time:");
+            result.Should().Contain("Day of Week:");

34-41: 시간 의존성 축소를 위해 고정 시각 사용 권장

지금은 검증에 사용되진 않지만, 향후 시간 문자열을 단언할 경우 대비해 DateTime.Now 대신 고정 시각을 쓰면 테스트 안정성이 올라갑니다.

-                DateTime.Now, 
+                new DateTime(2025, 08, 28, 12, 00, 00, DateTimeKind.Utc),

475-482: 비용 함수에 단조성 검증 추가 제안

> 0만으론 충분치 않습니다. 토큰이 증가하면 비용이 감소하지 않는 단조성도 함께 보장하세요.

         [Fact]
         public void CalculateCost_WithValidTokens_ShouldReturnPositiveCost()
         {
             // Act
             var cost = _format.CalculateCost(1000, 500);
 
             // Assert
             cost.Should().BeGreaterThan(0);
         }
+
+        [Fact]
+        public void CalculateCost_ShouldBeMonotonicWithMoreTokens()
+        {
+            var c1 = _format.CalculateCost(100, 50);
+            var c2 = _format.CalculateCost(200, 100);
+            c2.Should().BeGreaterOrEqualTo(c1);
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 38656e1 and 71b3cec.

📒 Files selected for processing (24)
  • ProjectVG.Application/ApplicationServiceCollectionExtensions.cs (2 hunks)
  • ProjectVG.Application/Services/Auth/AuthService.cs (2 hunks)
  • ProjectVG.Application/Services/User/IUserService.cs (1 hunks)
  • ProjectVG.Application/Services/User/UserService.cs (1 hunks)
  • ProjectVG.Infrastructure/Migrations/20250828173942_InitialCreate.Designer.cs (5 hunks)
  • ProjectVG.Infrastructure/Migrations/20250828173942_InitialCreate.cs (5 hunks)
  • ProjectVG.Infrastructure/Migrations/ProjectVGDbContextModelSnapshot.cs (5 hunks)
  • ProjectVG.Infrastructure/Persistence/Data/DatabaseSeedData.cs (2 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Data/DesignTimeDbContextFactory.cs (1 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250724230132_InitialCreate.Designer.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.Designer.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.Designer.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.Designer.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.Designer.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.cs (0 hunks)
  • ProjectVG.Infrastructure/Persistence/Repositories/User/IUserRepository.cs (1 hunks)
  • ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs (1 hunks)
  • ProjectVG.Tests/Services/Chat/Factories/ChatLLMFormatTests.cs (1 hunks)
  • scripts/migrate.ps1 (1 hunks)
  • test-clients/ai-chat-client/index.html (1 hunks)
💤 Files with no reviewable changes (10)
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825051022_UpdateUserEntityWithUIDAndStatus.Designer.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250724230132_InitialCreate.Designer.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023833_AddUIDToUserTable.Designer.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023623_AddUIDToUser.Designer.cs
  • ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825023955_AddUIDFieldToUser.Designer.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • ProjectVG.Application/ApplicationServiceCollectionExtensions.cs
🧰 Additional context used
🧬 Code graph analysis (6)
ProjectVG.Application/Services/User/UserService.cs (3)
ProjectVG.Application/Services/Auth/AuthService.cs (3)
  • Task (24-102)
  • Task (104-125)
  • Task (127-141)
ProjectVG.Application/Services/User/IUserService.cs (11)
  • Task (7-7)
  • Task (8-8)
  • Task (10-10)
  • Task (11-11)
  • Task (12-12)
  • Task (13-13)
  • Task (14-14)
  • Task (16-16)
  • Task (17-17)
  • Task (18-18)
  • Task (19-19)
ProjectVG.Application/Models/User/UserDto.cs (3)
  • UserDto (5-53)
  • UserDto (17-28)
  • UserDto (30-39)
ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs (2)
ProjectVG.Application/Services/User/UserService.cs (13)
  • Task (19-43)
  • Task (45-56)
  • Task (58-62)
  • Task (64-68)
  • Task (70-74)
  • Task (76-80)
  • Task (82-86)
  • Task (88-88)
  • Task (89-89)
  • Task (90-90)
  • Task (91-91)
  • Task (93-94)
  • Task (96-107)
ProjectVG.Domain/Entities/User/User.cs (1)
  • User (16-52)
ProjectVG.Application/Services/User/IUserService.cs (2)
ProjectVG.Application/Models/User/UserDto.cs (3)
  • UserDto (5-53)
  • UserDto (17-28)
  • UserDto (30-39)
ProjectVG.Api/Models/Auth/Request/RegisterRequest.cs (1)
  • UserDto (21-31)
ProjectVG.Infrastructure/Persistence/Repositories/User/IUserRepository.cs (1)
ProjectVG.Domain/Entities/User/User.cs (1)
  • User (16-52)
ProjectVG.Tests/Services/Chat/Factories/ChatLLMFormatTests.cs (4)
ProjectVG.Domain/Entities/Character/Character.cs (1)
  • Character (8-47)
ProjectVG.Application/Services/Chat/Factories/ChatLLMFormat.cs (7)
  • ChatLLMFormat (9-220)
  • List (134-140)
  • List (142-205)
  • List (207-214)
  • GetSystemMessage (15-84)
  • GetInstructions (86-132)
  • CalculateCost (216-219)
ProjectVG.Application/Models/Chat/ChatProcessContext.cs (3)
  • ChatProcessContext (6-138)
  • ChatProcessContext (24-32)
  • ChatProcessContext (34-49)
ProjectVG.Application/Models/Character/CharacterDto.cs (2)
  • CharacterDto (5-32)
  • CharacterDto (19-31)
ProjectVG.Infrastructure/Migrations/20250828173942_InitialCreate.Designer.cs (3)
ProjectVG.Infrastructure/Migrations/ProjectVGDbContextModelSnapshot.cs (1)
  • DbContext (13-271)
ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.Designer.cs (3)
  • ProjectVG (12-296)
  • DbContext (14-295)
  • b (200-277)
ProjectVG.Infrastructure/Persistence/EfCore/Migrations/20250825135004_IncreaseUIDLength.cs (1)
  • IncreaseUIDLength (9-128)
🔇 Additional comments (14)
scripts/migrate.ps1 (2)

2-2: --yes 제거 적절

EF Core CLI는 --yes를 지원하지 않으며, --force만으로 비대화형 드롭이 보장됩니다. 변경안 승인합니다.


1-2: scripts/migrate.ps1에 환경 변수 검사 추가해 Production/CI 환경에서 DB 드롭 방지
scripts/migrate.ps1 상단에 다음 가드를 추가하세요. 이 스크립트가 CI나 배포 파이프라인에서 실행될 때도 동작하는지 확인 바랍니다.

# 안전가드: Development 환경에서만 DB 드롭 허용
if ($env:ASPNETCORE_ENVIRONMENT -ne 'Development') {
    Write-Error "안전가드: Development 외 환경에서 DB 드롭 차단 (ASPNETCORE_ENVIRONMENT=$($env:ASPNETCORE_ENVIRONMENT))"
    exit 1
}
ProjectVG.Infrastructure/Persistence/Data/DatabaseSeedData.cs (1)

53-59: 스크립트 실행 결과를 토대로 TargetFramework 및 LangVersion 설정을 확인한 후, C# 11 원시 문자열 리터럴 사용 가능 여부를 검토해 주세요.

ProjectVG.Infrastructure/Migrations/20250828173942_InitialCreate.cs (3)

100-107: 시드 사용자 중복/제약 충돌 가능성 점검 요청

프로덕션에 동일 UID/Email/Provider 조합이 이미 존재할 경우 배포 시 Unique/체크 제약에 충돌할 수 있습니다. 배포 전 대상 DB에서 중복 검사 필요.

중복 점검 예시(SSMS/SQL):

SELECT Email, COUNT(*) c FROM Users GROUP BY Email HAVING COUNT(*)>1;
SELECT UID, COUNT(*) c FROM Users GROUP BY UID HAVING COUNT(*)>1;
SELECT Provider, ProviderId, COUNT(*) c FROM Users GROUP BY Provider, ProviderId HAVING COUNT(*)>1;

129-133: 대화 이력 인덱스 구성은 적절합니다

CharacterId/Timestamp/UserId 단일 인덱스와 (UserId,CharacterId,Timestamp) 복합 인덱스 구성은 조회 패턴에 유용합니다. 위 필터드 인덱스 제안이 추가되면 더 좋아집니다.


8-12: 해결: 마이그레이션 체인 충돌 없음
해당 디렉터리에 InitialCreate 외 다른 마이그레이션이 존재하지 않으므로 체인 충돌 우려가 없습니다.

ProjectVG.Infrastructure/Migrations/20250828173942_InitialCreate.Designer.cs (2)

12-17: 네임스페이스/클래스명 변경 자체는 괜찮으나, 체인 정합성 재확인 필요

Designer가 InitialCreate로 재생성되어 기존 체인과 어긋납니다. 앞서 지적한 체인 정리(리베이스 or 증분 마이그레이션) 후 재생성 권장.


231-255: Users 시드 타임스탬프 업데이트: LGTM

시드 값의 시간 업데이트는 정상입니다. 위에서 제안한 복합 Unique 인덱스 적용 시 Designer도 재생성 필요.

ProjectVG.Infrastructure/Migrations/ProjectVGDbContextModelSnapshot.cs (2)

11-15: 스냅샷 네임스페이스 이동: OK, 단 일관성 유지

스냅샷이 Migrations 네임스페이스로 이동했습니다. 기존 마이그레이션들과 혼재하지 않도록 체인 정리 후 고정하세요.


228-253: Users 인덱스/제약 스냅샷 반영 확인

UID Unique/Email Unique 반영은 적절합니다. (Provider, ProviderId) Unique로 전환 시 스냅샷 재생성 필수.

ProjectVG.Application/Services/User/IUserService.cs (1)

13-13: TryGetByEmailAsync 추가 LGTM

서비스 인터페이스 확장 방향과 일관성이 좋습니다.

ProjectVG.Application/Services/User/UserService.cs (1)

76-80: TryGetByEmailAsync 구현 LGTM

리포지토리 조회 → DTO 매핑 흐름이 명확합니다.

ProjectVG.Tests/Services/Chat/Factories/ChatLLMFormatTests.cs (2)

50-79: 파서의 기준 사례와 순서 보존을 잘 검증함

액션/텍스트/감정 전환 및 Order 값을 일관되게 확인하고 있어 회귀 방지에 유효합니다.


242-259: 비정형 패턴(잘못된 emotion/action) 허용 오차 검증 좋아요

오류 토큰을 건너뛰고 나머지를 파싱하는 회복력 검증이 적절합니다.

Also applies to: 262-275

Comment on lines 82 to 85
public async Task<UserDto?> TryGetByProviderAsync(string provider, string providerId)
{
var users = await _userRepository.GetAllAsync();
var user = users.FirstOrDefault(u => u.Provider == provider && u.ProviderId == providerId);
var user = await _userRepository.GetByProviderAsync(provider, providerId);
return user is null ? null : new UserDto(user);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Provider+ProviderId 직접 조회로 전환은 적절 — 인덱스/읽기 최적화 권장

대량 트래픽 대비를 위해 DB에 (Provider, ProviderId) 복합 인덱스(가능하면 Unique) 추가 및 리드 전용 쿼리는 AsNoTracking() 적용을 권장합니다(리포지토리 측 변경).

인덱스 존재 여부 점검:


🏁 Script executed:

#!/bin/bash
rg -nP --type=cs -C2 'HasIndex\(\s*new\s*{\s*u\.Provider\s*,\s*u\.ProviderId\s*}\s*\)' ProjectVG.Infrastructure || true

Length of output: 120


🏁 Script executed:

#!/bin/bash
# 인덱스 설정이 존재하는지 전체 코드베이스에서 재확인
rg -nP --type=cs 'HasIndex' -C2 .

Length of output: 6166


Provider+ProviderId 복합 인덱스 및 AsNoTracking 적용 필요
현재 ProjectVGDbContext에는 ProviderId 단일 인덱스만 설정되어 있으며 Provider 필드나 (Provider, ProviderId) 복합 인덱스가 없습니다. 대량 트래픽 대비를 위해 해당 복합 인덱스를 추가하고, 리드 전용 리포지토리 메서드에 AsNoTracking()을 적용하세요.

🤖 Prompt for AI Agents
In ProjectVG.Application/Services/User/UserService.cs around lines 82 to 85, the
call to _userRepository.GetByProviderAsync relies on a non-ideal DB schema and
tracking behavior; update the data layer instead of this service: add a
composite index on (Provider, ProviderId) in ProjectVGDbContext (and
create/apply a migration) to optimize lookups, and modify the repository's read
method used by GetByProviderAsync to use AsNoTracking() for read-only queries so
EF Core does not track entities during high-throughput reads; keep the service
logic the same and ensure tests/migrations are updated accordingly.

Comment on lines +22 to 30
Summary = table.Column<string>(type: "nvarchar(max)", nullable: false),
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
Role = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
Personality = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
SpeechStyle = table.Column<string>(type: "nvarchar(max)", nullable: false),
UserAlias = table.Column<string>(type: "nvarchar(max)", nullable: false),
Background = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
ImageUrl = table.Column<string>(type: "nvarchar(max)", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

nvarchar(max) 남용 및 제약 부재로 인한 성능/데이터 품질 리스크

Summary/UserAlias/ImageUrl이 모두 nvarchar(max) + NOT NULL입니다. 길이 제약과 형식 제약이 없어 행 외 저장/스캔 비용이 커지고, 빈 문자열이 유효 데이터로 축적될 수 있습니다. 최소한의 길이 제한과 URL/공백 방지 체크를 권장합니다.

- Summary = table.Column<string>(type: "nvarchar(max)", nullable: false),
+ Summary = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),

- UserAlias = table.Column<string>(type: "nvarchar(max)", nullable: false),
+ UserAlias = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),

- ImageUrl = table.Column<string>(type: "nvarchar(max)", nullable: false),
+ ImageUrl = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),

추가로(같은 Up 내부 하단에):

+ migrationBuilder.AddCheckConstraint(
+   name: "CK_Characters_UserAlias_NotBlank",
+   table: "Characters",
+   sql: "LEN(LTRIM(RTRIM([UserAlias]))) > 0");
+
+ migrationBuilder.AddCheckConstraint(
+   name: "CK_Characters_ImageUrl_Format",
+   table: "Characters",
+   sql: "[ImageUrl] = '' OR [ImageUrl] LIKE 'http%'");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Summary = table.Column<string>(type: "nvarchar(max)", nullable: false),
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
Role = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
Personality = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
SpeechStyle = table.Column<string>(type: "nvarchar(max)", nullable: false),
UserAlias = table.Column<string>(type: "nvarchar(max)", nullable: false),
Background = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
ImageUrl = table.Column<string>(type: "nvarchar(max)", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
// --- In Up(), inside the CreateTable("Characters", …) call, replace these three definitions:
Summary = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
UserAlias = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
ImageUrl = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
// (keep the other columns as before)
// …
IsActive = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Characters", x => x.Id);
});
// --- Then, still within Up(), after the CreateTable call, add:
migrationBuilder.AddCheckConstraint(
name: "CK_Characters_UserAlias_NotBlank",
table: "Characters",
sql: "LEN(LTRIM(RTRIM([UserAlias]))) > 0");
migrationBuilder.AddCheckConstraint(
name: "CK_Characters_ImageUrl_Format",
table: "Characters",
sql: "[ImageUrl] = '' OR [ImageUrl] LIKE 'http%'");

Comment on lines 32 to 33
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

타임스탬프 기본값 없음

CreatedAt(Characters/Users/ConversationHistories)에 기본값이 없어 모든 INSERT 시 애플리케이션이 값을 채워야 합니다. 운영 편의와 일관성을 위해 GETUTCDATE() 기본값을 권장합니다.

- CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
+ CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),

Also applies to: 51-52

🤖 Prompt for AI Agents
In ProjectVG.Infrastructure/Migrations/20250828173942_InitialCreate.cs around
lines 32-33 (and similarly lines 51-52), the CreatedAt columns lack a database
default so every INSERT must supply a timestamp; update the migration so
CreatedAt is defined with a SQL default of GETUTCDATE() (e.g., set
defaultValueSql: "GETUTCDATE()") while leaving UpdatedAt as nullable (no
default), then regenerate or update the migration so the CreatedAt column uses
GETUTCDATE() by default for new rows.

Comment on lines +47 to +53
b.Property<string>("ImageUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");

b.Property<bool>("IsActive")
.HasColumnType("bit");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

모델에 반영된 nvarchar(max) 신규 컬럼 길이/제약 보강

Designer는 auto-generated이므로 스키마 변경을 적용하면 자동 반영됩니다. 본 파일은 수정하지 말고, 마이그레이션/모델 구성에서 길이·체크 제약을 추가해 재생성하세요.

Also applies to: 73-83

Comment on lines +44 to +50
b.Property<string>("ImageUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");

b.Property<bool>("IsActive")
.HasColumnType("bit");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

스냅샷에도 nvarchar(max) 컬럼 반영됨 — 모델에 길이/체크 제약 추가 후 재생성 필요

Summary/UserAlias/ImageUrl 길이 제약 및 체크 제약을 모델에서 구성하면 스냅샷이 따라옵니다.

Also applies to: 70-80

🤖 Prompt for AI Agents
In ProjectVG.Infrastructure/Migrations/ProjectVGDbContextModelSnapshot.cs around
lines 44-50 (and similarly 70-80), the snapshot shows ImageUrl/Summary/UserAlias
as nvarchar(max) because the model lacks length/check constraints; update the
entity model (use DataAnnotations or Fluent API) to set the required MaxLength
and any check constraints for Summary, UserAlias and ImageUrl, then regenerate
the EF Core migration snapshot by creating a new migration (or removing and
recreating the pending migration) so the snapshot reflects the new column
types/constraints.

Comment on lines +35 to +38
// Try to get connection string from environment variable or configuration
var connectionString = configuration["DB_CONNECTION_STRING"]
?? configuration.GetConnectionString("DefaultConnection")
?? "Server=localhost,1433;Database=ProjectVG;User Id=sa;Password=ProjectVG123!;TrustServerCertificate=true;MultipleActiveResultSets=true";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

하드코딩된 DB 비밀번호 제거 및 실패-빠른 종료로 전환

레포에 비밀이 노출됩니다(보안 리스크) + 실수로 프로덕션에서 사용될 여지도 있습니다. 기본값을 제거하고 명시적으로 실패시키세요.

-            var connectionString = configuration["DB_CONNECTION_STRING"] 
-                ?? configuration.GetConnectionString("DefaultConnection")
-                ?? "Server=localhost,1433;Database=ProjectVG;User Id=sa;Password=ProjectVG123!;TrustServerCertificate=true;MultipleActiveResultSets=true";
+            var connectionString = configuration["DB_CONNECTION_STRING"] 
+                ?? configuration.GetConnectionString("DefaultConnection");
+            if (string.IsNullOrWhiteSpace(connectionString))
+                throw new InvalidOperationException("DB connection string is missing. Set DB_CONNECTION_STRING or ConnectionStrings:DefaultConnection.");

로컬 검증 스크립트:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "1) 레포 내 하드코딩 비밀 검색"
rg -n -S "ProjectVG123!" || true

echo "2) DesignTimeDbContextFactory에 System using 존재 확인"
rg -n "^using System;" ProjectVG.Infrastructure/Persistence/EfCore/Data/DesignTimeDbContextFactory.cs || true

echo "3) API 쪽 설정 파일 존재 확인"
fd -a "appsettings*.json" ProjectVG.Api || true

echo "4) API 루트의 .env 존재 확인"
fd -a ".env" ProjectVG.Api || true

Length of output: 1444


하드코딩된 DB 암호 전역 제거 및 설정 누락 시 즉시 종료 구현
DesignTimeDbContextFactory.cs에서 기본 커넥션 문자열 제거 후 누락 시 InvalidOperationException을 던지도록 변경하세요. 또한 아래 파일들에서 ProjectVG123! 비밀번호를 모두 제거하고 환경변수나 비밀관리자를 통해 주입하도록 수정해야 합니다:

  • scripts/start-db.ps1
  • env.example
  • README.md
  • ProjectVG.Api/appsettings.Development.json
🤖 Prompt for AI Agents
In
ProjectVG.Infrastructure/Persistence/EfCore/Data/DesignTimeDbContextFactory.cs
around lines 35 to 38, remove the hardcoded fallback connection string and
instead throw an InvalidOperationException when no connection string is found
(capture both configuration["DB_CONNECTION_STRING"] and
configuration.GetConnectionString("DefaultConnection") and if both are
null/empty throw with a clear message); then update the listed files
(scripts/start-db.ps1, env.example, README.md,
ProjectVG.Api/appsettings.Development.json) to remove the literal
"ProjectVG123!" password, replace it with references to an environment variable
or secret (e.g. DB_CONNECTION_STRING or separate DB_PASSWORD placeholder) and
document/instruct how to inject the secret via environment variables or a secret
manager so the app/launch scripts fail-fast when no secret is provided.

Comment on lines 11 to +12
Task<User?> GetByProviderIdAsync(string providerId);
Task<User?> GetByProviderAsync(string provider, string providerId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

GetByProviderIdAsync 잔존 → 이중 API로 혼선 발생 (Deprecated 또는 제거 권장)

신규 GetByProviderAsync(provider, providerId) 도입과 동시에 GetByProviderIdAsync(providerId)가 그대로 남아 있어 호출부 혼선을 유발합니다. 구형 메서드는 폐기(deprecate)하거나 인터페이스에서 제거하세요.

적용 예(Deprecated로 유도):

-        Task<User?> GetByProviderIdAsync(string providerId);
+        [System.Obsolete("Use GetByProviderAsync(string provider, string providerId)")]
+        Task<User?> GetByProviderIdAsync(string providerId);

다음 스크립트로 구형 메서드 사용처를 확인하고 마이그레이션 범위를 파악하세요:


🏁 Script executed:

#!/bin/bash
rg -nP --type=cs -C2 '\bGetByProviderIdAsync\s*\('

Length of output: 1381


IUserRepository.GetByProviderIdAsync에 Obsolete 추가 및 마이그레이션
IUserRepository.cs의 GetByProviderIdAsync에

[Obsolete("Use GetByProviderAsync(string provider, string providerId)")]

속성을 추가하고, SqlServerUserRepository.cs 구현체에도 동일하게 표시하세요. 호출부를 모두 GetByProviderAsync로 이전한 뒤 다음 릴리스에 완전 제거하세요.

🤖 Prompt for AI Agents
In ProjectVG.Infrastructure/Persistence/Repositories/User/IUserRepository.cs
around lines 11-12, mark GetByProviderIdAsync as obsolete and guide migration:
add [Obsolete("Use GetByProviderAsync(string provider, string providerId)")]
above the Task<User?> GetByProviderIdAsync(string providerId); declaration, then
apply the same attribute to the corresponding implementation method in
ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs
(the method that implements GetByProviderIdAsync). After adding the attributes,
search and update all call sites to use GetByProviderAsync(provider, providerId)
instead of GetByProviderIdAsync, and leave a TODO comment to remove
GetByProviderIdAsync in the next release.

Comment on lines +49 to +52
public async Task<User?> GetByProviderAsync(string provider, string providerId)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Provider == provider && u.ProviderId == providerId && u.Status != AccountStatus.Deleted);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

읽기 전용 쿼리에 추적 비활성화 및 성능 개선

GetByProviderAsync는 변경 추적이 불필요하므로 AsNoTracking()을 추가해 오버헤드를 줄이세요.

-            return await _context.Users.FirstOrDefaultAsync(u => u.Provider == provider && u.ProviderId == providerId && u.Status != AccountStatus.Deleted);
+            return await _context.Users
+                .AsNoTracking()
+                .FirstOrDefaultAsync(u => u.Provider == provider && u.ProviderId == providerId && u.Status != AccountStatus.Deleted);

또한 DB 차원에서 (Provider, ProviderId) 복합 인덱스를 추가(가능하면 Unique)하면 조회 성능과 무결성이 향상됩니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async Task<User?> GetByProviderAsync(string provider, string providerId)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Provider == provider && u.ProviderId == providerId && u.Status != AccountStatus.Deleted);
}
public async Task<User?> GetByProviderAsync(string provider, string providerId)
{
return await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Provider == provider
&& u.ProviderId == providerId
&& u.Status != AccountStatus.Deleted);
}
🤖 Prompt for AI Agents
In
ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs
around lines 49 to 52, the query in GetByProviderAsync is enabling change
tracking unnecessarily; add AsNoTracking() to the EF Core query to reduce
overhead for this read-only operation, e.g. call
_context.Users.AsNoTracking().FirstOrDefaultAsync(...). Also add a composite
index on (Provider, ProviderId) at the DB/schema level (make it unique if
business rules allow) to improve lookup performance and enforce integrity.

Comment on lines +42 to +47
_context = new ChatProcessContext(
command,
character,
new List<ProjectVG.Domain.Entities.ConversationHistorys.ConversationHistory>(),
new List<string> { "Previous conversation memory" }
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

GetSystemMessage의 시간 정보 신뢰도 문제 (ChatProcessContext가 UserRequestAt을 전달하지 않음)

이 테스트는 시스템 프롬프트에 "Current Time:" 포함만 확인하지만, 현재 new ChatProcessContext(command, character, ...) 오버로드는 UserRequestAtcommand에서 복사하지 않아 기본값(DateTime.Now 초기화 시점)으로 들어갑니다. 운영 로직과 어긋나고, 시간 기반 기능이 생기면 회귀를 놓칩니다.

아래 애플리케이션 코드 수정이 필요합니다(다른 파일 변경):

--- a/ProjectVG.Application/Models/Chat/ChatProcessContext.cs
+++ b/ProjectVG.Application/Models/Chat/ChatProcessContext.cs
@@ public class ChatProcessContext
         public ChatProcessContext(
             ChatRequestCommand command,
             CharacterDto character,
             IEnumerable<ConversationHistory> conversationHistory,
             IEnumerable<string> memoryContext)
         {
             UserId = command.UserId;
             CharacterId = command.CharacterId;
             UserMessage = command.UserPrompt;
             MemoryStore = command.UserId.ToString();
             UseTTS = command.UseTTS;
+            UserRequestAt = command.UserRequestAt; // 누락 보완
             
             Character = character;
             ConversationHistory = conversationHistory;
             MemoryContext = memoryContext;
         }

수정 반영 후, 본 테스트 클래스에 “지정 시간 반영” 검증을 추가하시겠습니까?


🏁 Script executed:

#!/bin/bash
set -eu
# Locate ChatProcessContext file
CHAT_PC_PATH=$(fd ChatProcessContext.cs | head -n1)
echo "ChatProcessContext file: $CHAT_PC_PATH"
# Show all constructors
rg -nP --type=csharp 'public ChatProcessContext' -C3 "$CHAT_PC_PATH"
# Check for UserRequestAt assignment
rg -n 'UserRequestAt' -C3 "$CHAT_PC_PATH" || echo "⚠️ UserRequestAt not set in constructor"

# Locate ChatRequestCommand file
CMD_PATH=$(fd ChatRequestCommand.cs | head -n1)
echo "ChatRequestCommand file: $CMD_PATH"
# Verify property exists
rg -n 'UserRequestAt' -C3 "$CMD_PATH" || echo "⚠️ UserRequestAt property missing in ChatRequestCommand"

Length of output: 3520


다중 인자 생성자에 UserRequestAt 할당 누락
ChatProcessContext의 ChatProcessContext(ChatRequestCommand, CharacterDto, IEnumerable<ConversationHistory>, IEnumerable<string>) 생성자에 UserRequestAt = command.UserRequestAt;이 빠져 기본값(DateTime.Now)이 사용되고 있습니다. 해당 생성자에 할당문을 추가하고, 테스트에서 지정된 요청 시간을 검증하도록 수정하세요.

🤖 Prompt for AI Agents
In ProjectVG.Tests/Services/Chat/Factories/ChatLLMFormatTests.cs around lines 42
to 47, the test constructs ChatProcessContext using the multi-argument
constructor but the constructor doesn't propagate command.UserRequestAt, so the
default DateTime.Now is used; update the ChatProcessContext(ChatRequestCommand,
CharacterDto, IEnumerable<ConversationHistory>, IEnumerable<string>) constructor
to set UserRequestAt = command.UserRequestAt, then update this test to supply a
specific UserRequestAt value on the command and add an assertion that
context.UserRequestAt equals that supplied value.

Comment on lines +433 to +439
result.Should().Contain("MANDATORY OUTPUT FORMAT SPECIFICATION");
result.Should().Contain("[emotion:emotion_name]");
result.Should().Contain("(action:action_name)");
result.Should().Contain("dialogue");
result.Should().Contain("neutral, happy, sad, angry, shy, surprised, embarrassed, sleepy, confused, proud");
result.Should().Contain("blushing, nodding, shaking_head, waving, smiling");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

상수 목록 문자열에 대한 취약한 단언 — 순서/구성 변경에 쉽게 깨짐

SupportedEmotions/SupportedActions 변경(순서 추가/변경) 시 테스트가 불필요하게 깨집니다. 전체 문자열 매칭 대신 핵심 항목 포함 여부만 검증하거나 토큰 단위 비교로 완화하세요.

-            result.Should().Contain("neutral, happy, sad, angry, shy, surprised, embarrassed, sleepy, confused, proud");
-            result.Should().Contain("blushing, nodding, shaking_head, waving, smiling");
+            foreach (var e in new[] { "neutral", "happy", "confused" })
+                result.Should().Contain(e);
+            foreach (var a in new[] { "blushing", "waving", "smiling" })
+                result.Should().Contain(a);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
result.Should().Contain("MANDATORY OUTPUT FORMAT SPECIFICATION");
result.Should().Contain("[emotion:emotion_name]");
result.Should().Contain("(action:action_name)");
result.Should().Contain("dialogue");
result.Should().Contain("neutral, happy, sad, angry, shy, surprised, embarrassed, sleepy, confused, proud");
result.Should().Contain("blushing, nodding, shaking_head, waving, smiling");
}
result.Should().Contain("MANDATORY OUTPUT FORMAT SPECIFICATION");
result.Should().Contain("[emotion:emotion_name]");
result.Should().Contain("(action:action_name)");
result.Should().Contain("dialogue");
- result.Should().Contain("neutral, happy, sad, angry, shy, surprised, embarrassed, sleepy, confused, proud");
foreach (var e in new[] { "neutral", "happy", "confused" })
result.Should().Contain(e);
foreach (var a in new[] { "blushing", "waving", "smiling" })
result.Should().Contain(a);
}

@ImGdevel ImGdevel merged commit 962aabb into develop Aug 30, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Aug 30, 2025
Merged
@ImGdevel ImGdevel deleted the feature/memory branch September 3, 2025 05:39
This was referenced Sep 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant