From 0163524eb717c1e21e34bc88ac653cc4bac9e628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=8B=A0=EC=98=81?= Date: Tue, 29 Jul 2025 17:57:25 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=A2=85=ED=95=A9=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EC=A0=9C=EA=B3=B5=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=B4=88=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Delegates/PostChatReportDelegate.cs | 16 +++ .../Endpoints/ChatCompletionEndpoint.cs | 7 ++ .../Services/KernelService.cs | 84 ++++++++++++++ .../Models/InterviewReportModel.cs | 21 ++++ .../Clients/ChatApiClient.cs | 18 ++- .../Components/App.razor | 1 + .../Components/Pages/Home.razor | 23 ++++ .../Components/Pages/Report.razor | 104 ++++++++++++++++++ .../Services/ChatService.cs | 26 +++++ .../wwwroot/css/chat.css | 35 ++++++ .../wwwroot/js/chatFunctions.js | 36 ++++++ 11 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 src/InterviewAssistant.ApiService/Delegates/PostChatReportDelegate.cs create mode 100644 src/InterviewAssistant.Common/Models/InterviewReportModel.cs create mode 100644 src/InterviewAssistant.Web/Components/Pages/Report.razor diff --git a/src/InterviewAssistant.ApiService/Delegates/PostChatReportDelegate.cs b/src/InterviewAssistant.ApiService/Delegates/PostChatReportDelegate.cs new file mode 100644 index 0000000..14ce57c --- /dev/null +++ b/src/InterviewAssistant.ApiService/Delegates/PostChatReportDelegate.cs @@ -0,0 +1,16 @@ +using InterviewAssistant.Common.Models; +using InterviewAssistant.ApiService.Services; +using Microsoft.AspNetCore.Mvc; + +namespace InterviewAssistant.ApiService.Delegates; + +public static class ChatReportDelegate +{ + public static async Task PostChatReportAsync( + [FromBody] List messages, + IKernelService kernelService) + { + var report = await kernelService.GenerateReportAsync(messages); + return Results.Ok(report); + } +} \ No newline at end of file diff --git a/src/InterviewAssistant.ApiService/Endpoints/ChatCompletionEndpoint.cs b/src/InterviewAssistant.ApiService/Endpoints/ChatCompletionEndpoint.cs index 2392df4..32958f2 100644 --- a/src/InterviewAssistant.ApiService/Endpoints/ChatCompletionEndpoint.cs +++ b/src/InterviewAssistant.ApiService/Endpoints/ChatCompletionEndpoint.cs @@ -32,6 +32,13 @@ public static IEndpointRouteBuilder MapChatCompletionEndpoint(this IEndpointRout .WithName("PostInterviewData") .WithOpenApi(); + // 면접 리포트 생성 엔드포인트 + api.MapPost("report", ChatReportDelegate.PostChatReportAsync) + .Accepts>(contentType: "application/json") + .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") + .WithName("PostChatReport") + .WithOpenApi(); + return routeBuilder; } } diff --git a/src/InterviewAssistant.ApiService/Services/KernelService.cs b/src/InterviewAssistant.ApiService/Services/KernelService.cs index 9a3faa5..86bd0f6 100644 --- a/src/InterviewAssistant.ApiService/Services/KernelService.cs +++ b/src/InterviewAssistant.ApiService/Services/KernelService.cs @@ -3,6 +3,10 @@ using InterviewAssistant.ApiService.Repositories; using InterviewAssistant.ApiService.Models; +using InterviewAssistant.Common.Models; + +using System.Text; +using System.Text.Json; using Microsoft.Extensions.AI; using Microsoft.SemanticKernel; @@ -17,6 +21,8 @@ public interface IKernelService { IAsyncEnumerable InvokeInterviewAgentAsync(string resumeContent, string jobDescriptionContent, IEnumerable? messages = null); IAsyncEnumerable PreprocessAndInvokeAsync(Guid resumeId, Guid jobId, string resumeUrl, string jobDescriptionUrl); + + Task GenerateReportAsync(IEnumerable messages); } public class KernelService(Kernel kernel, IMcpClient mcpClient, IInterviewRepository repository) : IKernelService @@ -153,4 +159,82 @@ private static string NormalizeUri(string uri) return uri; } + + public async Task GenerateReportAsync(IEnumerable messages) + { + // 1. 대화 기록을 AI가 이해하기 쉬운 문자열로 변환 + var historyText = new StringBuilder(); + foreach (var message in messages) + { + historyText.AppendLine($"{message.Role}: {message.Message}"); + } + + // 2. AI에게 리포트 생성을 지시하는 프롬프트 정의 + var prompt = """ + You are an expert interview analyst. Your task is to analyze the following interview conversation and provide a structured report. + The conversation is between a 'User' (the candidate) and an 'Assistant' (the interviewer). + + Based on the entire conversation, please provide: + 1. A concise overall feedback. + 2. A list of 3 key strengths. + 3. A list of 3 key weaknesses or areas for improvement. + 4. Categorize all questions asked by the 'Assistant' into one of three types: '기술(Technical)', '경험(Experience)', or '인성(Personality)' and provide a count for each. + + IMPORTANT: Your entire output must be a single, valid JSON object. Do not include any text outside of this JSON object. + The JSON structure must be: + { + "overallFeedback": "...", + "strengths": ["...", "...", "..."], + "weaknesses": ["...", "...", "..."], + "chartData": { + "labels": ["기술", "경험", "인성"], + "values": [count_of_technical, count_of_experience, count_of_personality] + } + } + + --- INTERVIEW HISTORY --- + {{$history}} + """; + + // 3. AI 호출 및 결과 처리 + try + { + var reportFunction = kernel.CreateFunctionFromPrompt(prompt); + var arguments = new KernelArguments { { "history", historyText.ToString() } }; + var result = await kernel.InvokeAsync(reportFunction, arguments); + + if (string.IsNullOrWhiteSpace(result)) + { + return new InterviewReportModel { OverallFeedback = "AI로부터 응답을 받지 못했습니다." }; + } + + var jsonResponse = result.Trim(); + if (jsonResponse.StartsWith("```json")) + { + jsonResponse = jsonResponse.Substring(7); + } + if (jsonResponse.StartsWith("```")) + { + jsonResponse = jsonResponse.Substring(3); + } + if (jsonResponse.EndsWith("```")) + { + jsonResponse = jsonResponse.Substring(0, jsonResponse.Length - 3); + } + + // 4. AI가 생성한 JSON 문자열을 C# 객체로 변환 + var report = JsonSerializer.Deserialize(result, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return report ?? new InterviewReportModel { OverallFeedback = "리포트 분석에 실패했습니다." }; + } + catch (Exception ex) + { + // 예외 처리 (예: 로깅) + Console.WriteLine($"Error generating report: {ex.Message}"); + return new InterviewReportModel { OverallFeedback = "리포트 생성 중 오류가 발생했습니다." }; + } + } } diff --git a/src/InterviewAssistant.Common/Models/InterviewReportModel.cs b/src/InterviewAssistant.Common/Models/InterviewReportModel.cs new file mode 100644 index 0000000..95ce9bc --- /dev/null +++ b/src/InterviewAssistant.Common/Models/InterviewReportModel.cs @@ -0,0 +1,21 @@ +namespace InterviewAssistant.Common.Models; + +/// +/// 면접 결과 리포트의 데이터 구조 +/// +public class InterviewReportModel +{ + public string OverallFeedback { get; set; } = string.Empty; + public List Strengths { get; set; } = []; + public List Weaknesses { get; set; } = []; + public ChartDataModel ChartData { get; set; } = new(); +} + +/// +/// 차트 데이터 구조 +/// +public class ChartDataModel +{ + public List Labels { get; set; } = []; + public List Values { get; set; } = []; +} \ No newline at end of file diff --git a/src/InterviewAssistant.Web/Clients/ChatApiClient.cs b/src/InterviewAssistant.Web/Clients/ChatApiClient.cs index 1467a1c..b9ea3f2 100644 --- a/src/InterviewAssistant.Web/Clients/ChatApiClient.cs +++ b/src/InterviewAssistant.Web/Clients/ChatApiClient.cs @@ -20,6 +20,8 @@ public interface IChatApiClient /// 이력서 및 채용공고 URL이 포함된 요청 객체 /// API 응답 IAsyncEnumerable SendInterviewDataAsync(InterviewDataRequest request); + + Task GenerateReportAsync(List messages); } /// @@ -55,7 +57,7 @@ public async IAsyncEnumerable SendInterviewDataAsync(InterviewData { if (request == null) throw new ArgumentNullException(nameof(request)); // 명시적 예외 처리 - + _logger.LogInformation("ChatApiClient.cs: 인터뷰 데이터 전송 시작"); var httpResponse = await _http.PostAsJsonAsync("/api/chat/interview-data", request); @@ -72,4 +74,18 @@ public async IAsyncEnumerable SendInterviewDataAsync(InterviewData _logger.LogInformation("ChatApiClient.cs: 인터뷰 데이터 전송 완료"); } + + public async Task GenerateReportAsync(List messages) + { + _logger.LogInformation("API에 리포트 생성 요청"); + var httpResponse = await _http.PostAsJsonAsync("/api/chat/report", messages); + + if (httpResponse.IsSuccessStatusCode) + { + return await httpResponse.Content.ReadFromJsonAsync(); + } + + _logger.LogError("리포트 생성 실패: {StatusCode}", httpResponse.StatusCode); + return null; + } } diff --git a/src/InterviewAssistant.Web/Components/App.razor b/src/InterviewAssistant.Web/Components/App.razor index 95826f8..407659a 100644 --- a/src/InterviewAssistant.Web/Components/App.razor +++ b/src/InterviewAssistant.Web/Components/App.razor @@ -12,6 +12,7 @@ + diff --git a/src/InterviewAssistant.Web/Components/Pages/Home.razor b/src/InterviewAssistant.Web/Components/Pages/Home.razor index 3f6b540..a8c6a69 100644 --- a/src/InterviewAssistant.Web/Components/Pages/Home.razor +++ b/src/InterviewAssistant.Web/Components/Pages/Home.razor @@ -8,6 +8,7 @@ @inject IJSRuntime JSRuntime @inject ILogger Logger @rendermode InteractiveServer +@inject NavigationManager Navigation 면접 코치 - InterviewAssistant @@ -41,6 +42,17 @@
+ +
+

AI 면접 코치

+ @if (isLinkShared && messages.Count > 2 && isServerOutputEnded) + { + + } +
+
@if (!isLinkShared) @@ -120,6 +132,7 @@ private Guid currentResumeId; private Guid currentJobDescriptionId; private void CloseModal() => showModal = false; + private bool reportGenerating = false; //Blazor 컴포넌트가 처음 초기화될 때 자동으로 호출 protected override async Task OnInitializedAsync() @@ -330,4 +343,14 @@ } await base.OnAfterRenderAsync(firstRender); } + private async Task GoToReport() + { + reportGenerating = true; + StateHasChanged(); // 로딩 상태 UI 반영 + + await ChatService.GenerateReportAsync(messages); + + reportGenerating = false; + Navigation.NavigateTo("/report"); + } } \ No newline at end of file diff --git a/src/InterviewAssistant.Web/Components/Pages/Report.razor b/src/InterviewAssistant.Web/Components/Pages/Report.razor new file mode 100644 index 0000000..4e983d0 --- /dev/null +++ b/src/InterviewAssistant.Web/Components/Pages/Report.razor @@ -0,0 +1,104 @@ +@page "/report" +@using InterviewAssistant.Web.Services +@using Markdig +@inject IChatService ChatService +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +면접 결과 리포트 + +
+
+

면접 결과 리포트

+

AI가 분석한 면접 결과 요약입니다.

+
+ +
+
+
+
+
종합 분석
+
+
+

@reportSummary?.OverallFeedback

+
+
+
+
+
상세 분석
+
+
+
👍 강점
+
    + @foreach(var item in reportSummary?.Strengths ?? new()) + { +
  • @item
  • + } +
+
+
개선점
+
    + @foreach(var item in reportSummary?.Weaknesses ?? new()) + { +
  • @item
  • + } +
+
+
+
+
+
+
+
질문 유형 분석
+
+
+ +
+
+
+
+ +
+

전체 대화 기록

+ @foreach (var message in chatHistory) + { +
+
+ @((MarkupString)Markdown.ToHtml(message.Message).Trim()) +
+
+ } +
+
+ +@code { + [Inject] private NavigationManager Navigation { get; set; } = default!; + private List chatHistory = new(); + private InterviewReportModel? reportSummary; + private bool isInitialized = false; + + protected override async Task OnInitializedAsync() + { + await JSRuntime.InvokeVoidAsync("setBodyOverflow", "auto"); + + chatHistory = ChatService.GetLastChatHistory(); + reportSummary = ChatService.GetLastReportSummary(); + + if (chatHistory.Count == 0 || reportSummary == null) + { + // 데이터가 없으면 홈으로 보냄 + // NavigationManager.NavigateTo("/"); + } + + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && reportSummary?.ChartData != null) + { + await JSRuntime.InvokeVoidAsync("createChart", "typeChart", reportSummary.ChartData); + } + await base.OnAfterRenderAsync(firstRender); + } +} diff --git a/src/InterviewAssistant.Web/Services/ChatService.cs b/src/InterviewAssistant.Web/Services/ChatService.cs index 5cad000..9106047 100644 --- a/src/InterviewAssistant.Web/Services/ChatService.cs +++ b/src/InterviewAssistant.Web/Services/ChatService.cs @@ -23,6 +23,10 @@ public interface IChatService /// 이력서 및 채용공고 URL이 포함된 요청 객체 /// API 응답 IAsyncEnumerable SendInterviewDataAsync(InterviewDataRequest request); + + Task GenerateReportAsync(List messages); + List GetLastChatHistory(); + InterviewReportModel? GetLastReportSummary(); } /// @@ -34,6 +38,9 @@ public class ChatService(IChatApiClient client, ILoggerFactory loggerFactory) : private readonly ILogger _logger = (loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory))) .CreateLogger(); + private List _lastChatHistory = []; + private InterviewReportModel? _lastReportSummary; + /// public async IAsyncEnumerable SendMessageAsync(IEnumerable messages, Guid resumeId, Guid jobDescriptionId) { @@ -100,4 +107,23 @@ public async IAsyncEnumerable SendInterviewDataAsync(InterviewData _logger.LogInformation("ChatService.c: 인터뷰 데이터 전송 완료"); } + + public async Task GenerateReportAsync(List messages) + { + _logger.LogInformation("리포트 생성 요청 시작"); + // ChatApiClient에 report 호출 메서드를 추가해야 합니다. (다음 단계에서 추가) + var report = await _client.GenerateReportAsync(messages); + + if (report != null) + { + // 수신된 데이터를 서비스 내에 저장 + _lastChatHistory = new List(messages); + _lastReportSummary = report; + } + + return report; + } + + public List GetLastChatHistory() => _lastChatHistory; + public InterviewReportModel? GetLastReportSummary() => _lastReportSummary; } \ No newline at end of file diff --git a/src/InterviewAssistant.Web/wwwroot/css/chat.css b/src/InterviewAssistant.Web/wwwroot/css/chat.css index f5c968e..c6ebcfc 100644 --- a/src/InterviewAssistant.Web/wwwroot/css/chat.css +++ b/src/InterviewAssistant.Web/wwwroot/css/chat.css @@ -450,3 +450,38 @@ body { .submit-btn:hover { background-color: #3a6fc1; } + +/* 채팅 헤더 스타일 추가 */ +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 10%; + border-bottom: 1px solid var(--border-color); + background-color: var(--chat-bg); +} + +.chat-header h2 { + margin: 0; + font-size: 1.25rem; + color: var(--primary-color); +} + +/* 리포트 페이지 전용 스타일 */ +.report-container .history-section { + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1rem; +} + +/* 리포트 페이지의 대화 기록에 스크롤 적용 */ +.report-container .history-section .message { + max-width: 100%; /* 너비 제한 해제 */ +} + +/* 대화 기록을 담을 스크롤 가능한 컨테이너 */ +.history-scroll-container { + max-height: 600px; /* 최대 높이 지정 */ + overflow-y: auto; /* 세로 스크롤 생성 */ + padding-right: 10px; +} \ No newline at end of file diff --git a/src/InterviewAssistant.Web/wwwroot/js/chatFunctions.js b/src/InterviewAssistant.Web/wwwroot/js/chatFunctions.js index 653c880..5d33645 100644 --- a/src/InterviewAssistant.Web/wwwroot/js/chatFunctions.js +++ b/src/InterviewAssistant.Web/wwwroot/js/chatFunctions.js @@ -102,4 +102,40 @@ window.resetTextAreaHeight = function (elementId) { textarea.style.height = ""; textarea.style.overflowY = "hidden"; } +}; + +window.createChart = (canvasId, chartData) => { + const ctx = document.getElementById(canvasId); + if (!ctx) return; + + new Chart(ctx, { + type: "pie", // 'bar'로 바꾸면 막대그래프가 됩니다. + data: { + labels: chartData.labels, + datasets: [ + { + label: "질문 유형 분석", + data: chartData.values, + backgroundColor: [ + "rgba(255, 99, 132, 0.7)", + "rgba(54, 162, 235, 0.7)", + "rgba(255, 206, 86, 0.7)", + "rgba(75, 192, 192, 0.7)", + "rgba(153, 102, 255, 0.7)", + ], + borderColor: "rgba(255, 255, 255, 1)", + borderWidth: 1, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: true, + }, + }); +}; + +// body 태그의 overflow 스타일을 설정하는 함수 +window.setBodyOverflow = (style) => { + document.body.style.overflow = style; }; \ No newline at end of file From 8683b79fee9dd3aa976421b7aac9b35b2ac239a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=8B=A0=EC=98=81?= Date: Tue, 29 Jul 2025 18:35:19 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EC=B5=9C=EC=A2=85=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/KernelService.cs | 64 ++++++++++--------- .../Components/Pages/Report.razor | 14 ++-- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/InterviewAssistant.ApiService/Services/KernelService.cs b/src/InterviewAssistant.ApiService/Services/KernelService.cs index 86bd0f6..2390ac5 100644 --- a/src/InterviewAssistant.ApiService/Services/KernelService.cs +++ b/src/InterviewAssistant.ApiService/Services/KernelService.cs @@ -6,7 +6,7 @@ using InterviewAssistant.Common.Models; using System.Text; -using System.Text.Json; +using System.Text.Json; using Microsoft.Extensions.AI; using Microsoft.SemanticKernel; @@ -171,37 +171,38 @@ public async Task GenerateReportAsync(IEnumerable(reportFunction, arguments); + result = await kernel.InvokeAsync(reportFunction, arguments); if (string.IsNullOrWhiteSpace(result)) { @@ -213,28 +214,29 @@ 2. A list of 3 key strengths. { jsonResponse = jsonResponse.Substring(7); } - if (jsonResponse.StartsWith("```")) - { - jsonResponse = jsonResponse.Substring(3); - } if (jsonResponse.EndsWith("```")) { jsonResponse = jsonResponse.Substring(0, jsonResponse.Length - 3); } - // 4. AI가 생성한 JSON 문자열을 C# 객체로 변환 - var report = JsonSerializer.Deserialize(result, new JsonSerializerOptions + var report = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return report ?? new InterviewReportModel { OverallFeedback = "리포트 분석에 실패했습니다." }; } - catch (Exception ex) + catch (System.Text.Json.JsonException jsonEx) // 1. JSON 파싱 오류만 특정 + { + // 2. 실패 시 AI의 원본 응답과 함께 상세 로그 기록 + Console.WriteLine($"AI response JSON parsing failed: {jsonEx.Message}. Raw response from AI: {result}"); + // 3. 사용자에게 더 구체적인 오류 메시지 반환 + return new InterviewReportModel { OverallFeedback = "AI가 분석 결과를 잘못된 형식으로 반환했습니다. 잠시 후 다시 시도해 주세요." }; + } + catch (Exception ex) // 그 외 모든 오류 { - // 예외 처리 (예: 로깅) - Console.WriteLine($"Error generating report: {ex.Message}"); - return new InterviewReportModel { OverallFeedback = "리포트 생성 중 오류가 발생했습니다." }; + Console.WriteLine($"An unexpected error occurred while generating the report: {ex}"); // 전체 예외 정보 로깅 + return new InterviewReportModel { OverallFeedback = "리포트 생성 중 예상치 못한 오류가 발생했습니다." }; } } } diff --git a/src/InterviewAssistant.Web/Components/Pages/Report.razor b/src/InterviewAssistant.Web/Components/Pages/Report.razor index 4e983d0..f2a27bf 100644 --- a/src/InterviewAssistant.Web/Components/Pages/Report.razor +++ b/src/InterviewAssistant.Web/Components/Pages/Report.razor @@ -78,9 +78,7 @@ private bool isInitialized = false; protected override async Task OnInitializedAsync() - { - await JSRuntime.InvokeVoidAsync("setBodyOverflow", "auto"); - + { chatHistory = ChatService.GetLastChatHistory(); reportSummary = ChatService.GetLastReportSummary(); @@ -95,10 +93,16 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender && reportSummary?.ChartData != null) + if (firstRender) + { + // JS 호출 코드를 OnAfterRenderAsync로 이동 + await JSRuntime.InvokeVoidAsync("setBodyOverflow", "auto"); + + if (reportSummary?.ChartData != null) { await JSRuntime.InvokeVoidAsync("createChart", "typeChart", reportSummary.ChartData); } - await base.OnAfterRenderAsync(firstRender); + } + await base.OnAfterRenderAsync(firstRender); } } From afa99e6e0e6227c8651467f42a4a5c091509b19e Mon Sep 17 00:00:00 2001 From: dkswoals Date: Tue, 29 Jul 2025 19:59:11 +0900 Subject: [PATCH 3/5] =?UTF-8?q?test:=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Delegates/ChatReportDelegateTests.cs | 223 +++++++++++++ .../Services/KernelServiceReportTests.cs | 297 ++++++++++++++++++ 2 files changed, 520 insertions(+) create mode 100644 test/InterviewAssistant.ApiService.Tests/Delegates/ChatReportDelegateTests.cs create mode 100644 test/InterviewAssistant.ApiService.Tests/Services/KernelServiceReportTests.cs diff --git a/test/InterviewAssistant.ApiService.Tests/Delegates/ChatReportDelegateTests.cs b/test/InterviewAssistant.ApiService.Tests/Delegates/ChatReportDelegateTests.cs new file mode 100644 index 0000000..b4a79d5 --- /dev/null +++ b/test/InterviewAssistant.ApiService.Tests/Delegates/ChatReportDelegateTests.cs @@ -0,0 +1,223 @@ +using InterviewAssistant.ApiService.Delegates; +using InterviewAssistant.ApiService.Services; +using InterviewAssistant.Common.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Shouldly; + +namespace InterviewAssistant.ApiService.Tests.Delegates; + +[TestFixture] +public class ChatReportDelegateTests +{ + private IKernelService _kernelService; + private IServiceProvider _serviceProvider; + + [SetUp] + public void Setup() + { + _kernelService = Substitute.For(); + + // 완전한 서비스 프로바이더 설정 + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSingleton(sp => sp); + _serviceProvider = services.BuildServiceProvider(); + } + + [TearDown] + public void TearDown() + { + if (_serviceProvider is IDisposable disposableServiceProvider) + { + disposableServiceProvider.Dispose(); + } + } + + private HttpContext CreateHttpContext() + { + return new DefaultHttpContext + { + RequestServices = _serviceProvider + }; + } + + [Test] + public async Task PostChatReportAsync_WithValidMessages_ShouldReturnOkResult() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "안녕하세요" }, + new() { Role = MessageRoleType.Assistant, Message = "안녕하세요! 자기소개 부탁드립니다." }, + new() { Role = MessageRoleType.User, Message = "저는 개발자입니다" } + }; + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "전반적으로 좋은 면접이었습니다.", + Strengths = ["명확한 의사소통", "적극적인 태도"], + Weaknesses = ["구체적 예시 부족"], + ChartData = new ChartDataModel + { + Labels = ["기술", "경험", "인성"], + Values = [1, 1, 1] + } + }; + + _kernelService.GenerateReportAsync(messages).Returns(expectedReport); + + // Act + var result = await ChatReportDelegate.PostChatReportAsync(messages, _kernelService); + + // Assert + result.ShouldNotBeNull(); + + // HttpContext with proper ServiceProvider setup + var httpContext = CreateHttpContext(); + await result.ExecuteAsync(httpContext); + + httpContext.Response.StatusCode.ShouldBe(200); + } + + [Test] + public async Task PostChatReportAsync_WithEmptyMessages_ShouldStillReturnOkResult() + { + // Arrange + var messages = new List(); + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "면접 기록이 부족합니다.", + Strengths = [], + Weaknesses = [], + ChartData = new ChartDataModel + { + Labels = ["기술", "경험", "인성"], + Values = [0, 0, 0] + } + }; + + _kernelService.GenerateReportAsync(messages).Returns(expectedReport); + + // Act + var result = await ChatReportDelegate.PostChatReportAsync(messages, _kernelService); + + // Assert + result.ShouldNotBeNull(); + + var httpContext = CreateHttpContext(); + await result.ExecuteAsync(httpContext); + + httpContext.Response.StatusCode.ShouldBe(200); + } + + [Test] + public async Task PostChatReportAsync_WithKernelServiceException_ShouldThrowException() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + _kernelService.GenerateReportAsync(messages) + .Throws(new InvalidOperationException("커널 서비스 오류")); + + // Act & Assert + var exception = await Should.ThrowAsync( + async () => await ChatReportDelegate.PostChatReportAsync(messages, _kernelService)); + + exception.Message.ShouldBe("커널 서비스 오류"); + } + + [Test] + public async Task PostChatReportAsync_WithNullReport_ShouldReturnOkWithNull() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + _kernelService.GenerateReportAsync(messages).Returns(Task.FromResult(null)); + + // Act + var result = await ChatReportDelegate.PostChatReportAsync(messages, _kernelService); + + // Assert + result.ShouldNotBeNull(); + + var httpContext = CreateHttpContext(); + await result.ExecuteAsync(httpContext); + + httpContext.Response.StatusCode.ShouldBe(200); + } + + [Test] + public async Task PostChatReportAsync_ShouldCallKernelServiceOnce() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "테스트 리포트" + }; + + _kernelService.GenerateReportAsync(messages).Returns(expectedReport); + + // Act + await ChatReportDelegate.PostChatReportAsync(messages, _kernelService); + + // Assert + await _kernelService.Received(1).GenerateReportAsync(messages); + } + + [Test] + public async Task PostChatReportAsync_WithLargeMessageList_ShouldProcessCorrectly() + { + // Arrange + var messages = new List(); + for (int i = 0; i < 100; i++) + { + messages.Add(new ChatMessage + { + Role = i % 2 == 0 ? MessageRoleType.User : MessageRoleType.Assistant, + Message = $"메시지 {i}" + }); + } + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "대화가 길었습니다.", + Strengths = ["인내심"], + Weaknesses = ["간결함 부족"], + ChartData = new ChartDataModel + { + Labels = ["기술", "경험", "인성"], + Values = [30, 30, 40] + } + }; + + _kernelService.GenerateReportAsync(messages).Returns(expectedReport); + + // Act + var result = await ChatReportDelegate.PostChatReportAsync(messages, _kernelService); + + // Assert + result.ShouldNotBeNull(); + + var httpContext = CreateHttpContext(); + await result.ExecuteAsync(httpContext); + + httpContext.Response.StatusCode.ShouldBe(200); + await _kernelService.Received(1).GenerateReportAsync(messages); + } +} diff --git a/test/InterviewAssistant.ApiService.Tests/Services/KernelServiceReportTests.cs b/test/InterviewAssistant.ApiService.Tests/Services/KernelServiceReportTests.cs new file mode 100644 index 0000000..a303c3a --- /dev/null +++ b/test/InterviewAssistant.ApiService.Tests/Services/KernelServiceReportTests.cs @@ -0,0 +1,297 @@ +using System.Text.Json; +using InterviewAssistant.ApiService.Services; +using InterviewAssistant.ApiService.Repositories; +using InterviewAssistant.Common.Models; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Shouldly; + +namespace InterviewAssistant.ApiService.Tests.Services; + +[TestFixture] +public class KernelServiceReportTests +{ + private Kernel _kernel; + private IChatCompletionService _chatCompletionService; + private IMcpClient _mcpClient; + private IInterviewRepository _repository; + private KernelService _kernelService; + + [SetUp] + public void Setup() + { + // Create substitutes for interfaces only + _chatCompletionService = Substitute.For(); + _mcpClient = Substitute.For(); + _repository = Substitute.For(); + + // Create real kernel with mocked chat completion service + var builder = Kernel.CreateBuilder(); + builder.Services.AddSingleton(_chatCompletionService); + _kernel = builder.Build(); + + // Create kernel service + _kernelService = new KernelService(_kernel, _mcpClient, _repository); + } + + [TearDown] + public void TearDown() + { + // NUnit1032 경고를 해결하기 위한 TearDown + // IMcpClient가 IDisposable을 구현하는 경우에만 Dispose 호출 + if (_mcpClient is IDisposable disposableMcpClient) + { + disposableMcpClient.Dispose(); + } + } + + [Test] + public async Task GenerateReportAsync_WithValidMessages_ShouldReturnReport() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "안녕하세요, 면접 준비를 도와주세요" }, + new() { Role = MessageRoleType.Assistant, Message = "안녕하세요! 자기소개 부탁드립니다." }, + new() { Role = MessageRoleType.User, Message = "저는 3년차 개발자입니다" }, + new() { Role = MessageRoleType.Assistant, Message = "어떤 기술 스택을 주로 사용하시나요?" } + }; + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "전반적으로 좋은 면접이었습니다.", + Strengths = ["명확한 의사소통", "풍부한 경험", "기술적 이해도"], + Weaknesses = ["구체적 예시 부족", "질문 이해도", "답변 구조화"], + ChartData = new ChartDataModel + { + Labels = ["기술", "경험", "인성"], + Values = [2, 1, 1] + } + }; + + var expectedJsonResponse = JsonSerializer.Serialize(expectedReport); + + // Mock chat completion service to return expected JSON + _chatCompletionService + .GetChatMessageContentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult>( + [new ChatMessageContent(AuthorRole.Assistant, expectedJsonResponse)])); + + // Act + var result = await _kernelService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("전반적으로 좋은 면접이었습니다."); + result.Strengths.Count.ShouldBe(3); + result.Weaknesses.Count.ShouldBe(3); + result.ChartData.Labels.Count.ShouldBe(3); + result.ChartData.Values.Count.ShouldBe(3); + result.ChartData.Values.Sum().ShouldBe(4); + } + + [Test] + public async Task GenerateReportAsync_WithEmptyMessages_ShouldStillGenerateReport() + { + // Arrange + var messages = new List(); + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "면접 기록이 부족합니다.", + Strengths = [], + Weaknesses = [], + ChartData = new ChartDataModel + { + Labels = ["기술", "경험", "인성"], + Values = [0, 0, 0] + } + }; + + var expectedJsonResponse = JsonSerializer.Serialize(expectedReport); + + _chatCompletionService + .GetChatMessageContentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult>( + [new ChatMessageContent(AuthorRole.Assistant, expectedJsonResponse)])); + + // Act + var result = await _kernelService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("면접 기록이 부족합니다."); + } + + [Test] + public async Task GenerateReportAsync_WithInvalidJsonResponse_ShouldReturnErrorReport() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + // Mock invalid JSON response + _chatCompletionService + .GetChatMessageContentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult>( + [new ChatMessageContent(AuthorRole.Assistant, "잘못된 JSON 응답")])); + + // Act + var result = await _kernelService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("AI가 분석 결과를 잘못된 형식으로 반환했습니다. 잠시 후 다시 시도해 주세요."); + } + + [Test] + public async Task GenerateReportAsync_WithCodeBlockWrappedJson_ShouldParseCorrectly() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + var reportModel = new InterviewReportModel + { + OverallFeedback = "코드 블록으로 감싸진 응답 테스트", + Strengths = ["강점1"], + Weaknesses = ["약점1"], + ChartData = new ChartDataModel + { + Labels = ["기술", "경험", "인성"], + Values = [1, 0, 0] + } + }; + + var jsonResponse = JsonSerializer.Serialize(reportModel); + var wrappedResponse = $"```json\n{jsonResponse}\n```"; + + _chatCompletionService + .GetChatMessageContentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult>( + [new ChatMessageContent(AuthorRole.Assistant, wrappedResponse)])); + + // Act + var result = await _kernelService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("코드 블록으로 감싸진 응답 테스트"); + result.Strengths.Count.ShouldBe(1); + result.Weaknesses.Count.ShouldBe(1); + } + + [Test] + public async Task GenerateReportAsync_WithNullOrEmptyResponse_ShouldReturnErrorReport() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + _chatCompletionService + .GetChatMessageContentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult>( + [new ChatMessageContent(AuthorRole.Assistant, "")])); + + // Act + var result = await _kernelService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("AI로부터 응답을 받지 못했습니다."); + } + + [Test] + public async Task GenerateReportAsync_WithKernelException_ShouldReturnErrorReport() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + _chatCompletionService + .GetChatMessageContentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Throws(new InvalidOperationException("커널 서비스 오류")); + + // Act + var result = await _kernelService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("리포트 생성 중 예상치 못한 오류가 발생했습니다."); + } + + [Test] + public async Task GenerateReportAsync_WithMultipleRoles_ShouldHandleAllRoles() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.System, Message = "시스템 메시지" }, + new() { Role = MessageRoleType.User, Message = "사용자 메시지" }, + new() { Role = MessageRoleType.Assistant, Message = "어시스턴트 메시지" }, + new() { Role = MessageRoleType.Tool, Message = "도구 메시지" } + }; + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "다양한 역할 테스트", + Strengths = [], + Weaknesses = [], + ChartData = new ChartDataModel { Labels = [], Values = [] } + }; + + var expectedJsonResponse = JsonSerializer.Serialize(expectedReport); + + _chatCompletionService + .GetChatMessageContentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult>( + [new ChatMessageContent(AuthorRole.Assistant, expectedJsonResponse)])); + + // Act + var result = await _kernelService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("다양한 역할 테스트"); + } +} From 58b6187f5370c39131a43f81d16e20d574c262e1 Mon Sep 17 00:00:00 2001 From: dkswoals Date: Tue, 29 Jul 2025 19:59:30 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test:=20ChatServiceReport=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ChatServiceReportTests.cs | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 test/InterviewAssistant.Web.Tests/Services/ChatServiceReportTests.cs diff --git a/test/InterviewAssistant.Web.Tests/Services/ChatServiceReportTests.cs b/test/InterviewAssistant.Web.Tests/Services/ChatServiceReportTests.cs new file mode 100644 index 0000000..425bc41 --- /dev/null +++ b/test/InterviewAssistant.Web.Tests/Services/ChatServiceReportTests.cs @@ -0,0 +1,235 @@ +using InterviewAssistant.Common.Models; +using InterviewAssistant.Web.Services; +using InterviewAssistant.Web.Clients; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; + +namespace InterviewAssistant.Web.Tests.Services; + +[TestFixture] +public class ChatServiceReportTests +{ + private IChatApiClient _apiClient; + private ILoggerFactory _loggerFactory; + private IChatService _chatService; + + [SetUp] + public void Setup() + { + _apiClient = Substitute.For(); + _loggerFactory = Substitute.For(); + _chatService = new ChatService(_apiClient, _loggerFactory); + } + + [TearDown] + public void Cleanup() + { + _loggerFactory?.Dispose(); + } + + [Test] + public async Task GenerateReportAsync_WithValidMessages_ShouldReturnReport() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "안녕하세요" }, + new() { Role = MessageRoleType.Assistant, Message = "안녕하세요! 자기소개 부탁드립니다." }, + new() { Role = MessageRoleType.User, Message = "저는 개발자입니다" } + }; + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "전반적으로 좋은 면접이었습니다.", + Strengths = ["명확한 의사소통", "적극적인 태도"], + Weaknesses = ["구체적 예시 부족"], + ChartData = new ChartDataModel + { + Labels = ["기술", "경험", "인성"], + Values = [1, 1, 1] + } + }; + + _apiClient.GenerateReportAsync(messages).Returns(expectedReport); + + // Act + var result = await _chatService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("전반적으로 좋은 면접이었습니다."); + result.Strengths.Count.ShouldBe(2); + result.Weaknesses.Count.ShouldBe(1); + result.ChartData.Values.Sum().ShouldBe(3); + } + + [Test] + public async Task GenerateReportAsync_WithApiFailure_ShouldReturnNull() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + _apiClient.GenerateReportAsync(messages).Returns((InterviewReportModel?)null); + + // Act + var result = await _chatService.GenerateReportAsync(messages); + + // Assert + result.ShouldBeNull(); + } + + [Test] + public async Task GenerateReportAsync_ShouldStoreDataInService() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "테스트 리포트" + }; + + _apiClient.GenerateReportAsync(messages).Returns(expectedReport); + + // Act + await _chatService.GenerateReportAsync(messages); + + // Assert + var storedHistory = _chatService.GetLastChatHistory(); + var storedReport = _chatService.GetLastReportSummary(); + + storedHistory.Count.ShouldBe(1); + storedHistory[0].Message.ShouldBe("테스트 메시지"); + storedReport.ShouldNotBeNull(); + storedReport.OverallFeedback.ShouldBe("테스트 리포트"); + } + + [Test] + public async Task GenerateReportAsync_WithNullReport_ShouldNotStoreData() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + _apiClient.GenerateReportAsync(messages).Returns((InterviewReportModel?)null); + + // Act + await _chatService.GenerateReportAsync(messages); + + // Assert + var storedHistory = _chatService.GetLastChatHistory(); + var storedReport = _chatService.GetLastReportSummary(); + + storedHistory.Count.ShouldBe(0); + storedReport.ShouldBeNull(); + } + + [Test] + public void GetLastChatHistory_InitialState_ShouldReturnEmptyList() + { + // Act + var result = _chatService.GetLastChatHistory(); + + // Assert + result.ShouldNotBeNull(); + result.Count.ShouldBe(0); + } + + [Test] + public void GetLastReportSummary_InitialState_ShouldReturnNull() + { + // Act + var result = _chatService.GetLastReportSummary(); + + // Assert + result.ShouldBeNull(); + } + + [Test] + public async Task GenerateReportAsync_ShouldCallApiClientOnce() + { + // Arrange + var messages = new List + { + new() { Role = MessageRoleType.User, Message = "테스트 메시지" } + }; + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "테스트 리포트" + }; + + _apiClient.GenerateReportAsync(messages).Returns(expectedReport); + + // Act + await _chatService.GenerateReportAsync(messages); + + // Assert + await _apiClient.Received(1).GenerateReportAsync(messages); + } + + [Test] + public async Task GenerateReportAsync_WithEmptyMessages_ShouldStillCallApi() + { + // Arrange + var messages = new List(); + + var expectedReport = new InterviewReportModel + { + OverallFeedback = "면접 기록이 부족합니다." + }; + + _apiClient.GenerateReportAsync(messages).Returns(expectedReport); + + // Act + var result = await _chatService.GenerateReportAsync(messages); + + // Assert + result.ShouldNotBeNull(); + result.OverallFeedback.ShouldBe("면접 기록이 부족합니다."); + await _apiClient.Received(1).GenerateReportAsync(messages); + } + + [Test] + public async Task GenerateReportAsync_MultipleCallsOverwriteStoredData() + { + // Arrange + var firstMessages = new List + { + new() { Role = MessageRoleType.User, Message = "첫 번째 메시지" } + }; + + var secondMessages = new List + { + new() { Role = MessageRoleType.User, Message = "두 번째 메시지" } + }; + + var firstReport = new InterviewReportModel { OverallFeedback = "첫 번째 리포트" }; + var secondReport = new InterviewReportModel { OverallFeedback = "두 번째 리포트" }; + + _apiClient.GenerateReportAsync(firstMessages).Returns(firstReport); + _apiClient.GenerateReportAsync(secondMessages).Returns(secondReport); + + // Act + await _chatService.GenerateReportAsync(firstMessages); + await _chatService.GenerateReportAsync(secondMessages); + + // Assert + var storedHistory = _chatService.GetLastChatHistory(); + var storedReport = _chatService.GetLastReportSummary(); + + storedHistory.Count.ShouldBe(1); + storedHistory[0].Message.ShouldBe("두 번째 메시지"); + storedReport.ShouldNotBeNull(); + storedReport.OverallFeedback.ShouldBe("두 번째 리포트"); + } +} From eea11545a902402c1ada8678d6994bddd21cfb03 Mon Sep 17 00:00:00 2001 From: dkswoals Date: Tue, 29 Jul 2025 20:14:29 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:L=20HOMETests=20Home=5FHandleKeyDo?= =?UTF-8?q?wn=5FPreventsDuplicateWithIsSendFlag=20=EB=8C=80=EA=B8=B0=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Pages/HomeTests.cs | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/test/InterviewAssistant.AppHost.Tests/Components/Pages/HomeTests.cs b/test/InterviewAssistant.AppHost.Tests/Components/Pages/HomeTests.cs index fcc799c..63feb1f 100644 --- a/test/InterviewAssistant.AppHost.Tests/Components/Pages/HomeTests.cs +++ b/test/InterviewAssistant.AppHost.Tests/Components/Pages/HomeTests.cs @@ -346,7 +346,7 @@ public async Task Home_Serveroutput_Prohibit_UserTransport() $"Enter 전 메시지 수: {messageCountBeforeEnter}, Enter 후 메시지 수: {messageCountAfterEnter}"); } - /// + /// /// 중복 이벤트 방지 플래그가 제대로 동작하는지 확인합니다. /// [Test] @@ -357,14 +357,32 @@ public async Task Home_HandleKeyDown_PreventsDuplicateWithIsSendFlag() await Page.Locator("input#resumeUrl").FillAsync("https://example.com/resume.pdf"); await Page.Locator("input#jobUrl").FillAsync("https://example.com/job.pdf"); await Page.Locator("button.submit-btn").ClickAsync(); + + // 모달 닫힘 대기 await Page.WaitForSelectorAsync(".modal", new PageWaitForSelectorOptions { State = WaitForSelectorState.Detached, - Timeout = 5000 + Timeout = 10000 + }); + + // 초기 AI 응답 완료까지 대기 (다른 테스트와 동일하게) + await Page.WaitForSelectorAsync(".welcome-message", new PageWaitForSelectorOptions + { + State = WaitForSelectorState.Detached, + Timeout = 15000 + }); + + await Page.WaitForSelectorAsync(".response-status", new PageWaitForSelectorOptions + { + State = WaitForSelectorState.Detached, + Timeout = RESPONSE_TIMEOUT // 60초로 증가 }); var textarea = Page.Locator("textarea#messageInput"); + // Textarea가 활성화될 때까지 명시적으로 대기 (타임아웃 증가) + await Expect(textarea).ToBeEnabledAsync(new() { Timeout = 20000 }); + // 초기 메시지 개수 확인 var initialMessageCount = await Page.EvaluateAsync("document.querySelectorAll('.message').length"); @@ -372,12 +390,12 @@ public async Task Home_HandleKeyDown_PreventsDuplicateWithIsSendFlag() await textarea.FillAsync("첫 번째 메시지"); await textarea.PressAsync("Enter"); - // 고정된 대기 시간으로 처리 완료 대기 - await Task.Delay(2000); + // 메시지 전송 후 충분한 대기 시간 + await Task.Delay(3000); var afterFirstSend = await Page.EvaluateAsync("document.querySelectorAll('.message').length"); - // Assert 1: 첫 번째 메시지 전송 확인 (메시지가 추가되었을 수도, 안 되었을 수도 있음) + // Assert 1: 첫 번째 메시지 전송 확인 Console.WriteLine($"초기 메시지 수: {initialMessageCount}, 첫 전송 후: {afterFirstSend}"); // Act 2: isSend 플래그를 true로 설정하여 중복 전송 차단 테스트 @@ -391,14 +409,17 @@ public async Task Home_HandleKeyDown_PreventsDuplicateWithIsSendFlag() var flagStatus = await Page.EvaluateAsync("window.isSend === true"); Console.WriteLine($"isSend 플래그 상태: {flagStatus}"); + // textarea가 여전히 활성화되어 있는지 재확인 + await Expect(textarea).ToBeEnabledAsync(new() { Timeout = 5000 }); + await textarea.PressAsync("Enter"); - await Task.Delay(1500); // 처리 시간 대기 + await Task.Delay(2000); // 처리 시간 대기 var afterBlockedSend = await Page.EvaluateAsync("document.querySelectorAll('.message').length"); Console.WriteLine($"차단 테스트 후 메시지 수: {afterBlockedSend}"); // Assert 2: isSend 플래그가 true일 때 메시지 전송 차단 확인 - afterBlockedSend.ShouldBe(afterFirstSend, "isSend 플래그가 true일 때 메시지 전송이 차단되어야 합니다_1."); + afterBlockedSend.ShouldBe(afterFirstSend, "isSend 플래그가 true일 때 메시지 전송이 차단되어야 합니다."); // Act 3: 플래그 해제 후 정상 전송 확인 await Page.EvaluateAsync("window.isSend = false;"); @@ -410,9 +431,12 @@ public async Task Home_HandleKeyDown_PreventsDuplicateWithIsSendFlag() // 새로운 메시지로 다시 시도 await textarea.ClearAsync(); await textarea.FillAsync("플래그 해제 후 메시지"); - await textarea.PressAsync("Enter"); - await Task.Delay(2000); // 처리 시간 대기 + // textarea 상태 재확인 + await Expect(textarea).ToBeEnabledAsync(new() { Timeout = 5000 }); + + await textarea.PressAsync("Enter"); + await Task.Delay(3000); // 처리 시간 대기 var afterFlagReset = await Page.EvaluateAsync("document.querySelectorAll('.message').length"); Console.WriteLine($"플래그 해제 후 메시지 수: {afterFlagReset}"); @@ -422,9 +446,9 @@ public async Task Home_HandleKeyDown_PreventsDuplicateWithIsSendFlag() var blockedCorrectly = (afterBlockedSend == afterFirstSend); var allowedAfterReset = (afterFlagReset >= afterBlockedSend); - blockedCorrectly.ShouldBeTrue("isSend 플래그가 true일 때 메시지 전송이 차단되어야 합니다_2."); + blockedCorrectly.ShouldBeTrue("isSend 플래그가 true일 때 메시지 전송이 차단되어야 합니다."); - // 만약 메시지 전송 기능이 실제로 작동한다면 + // 테스트 환경에서 실제 메시지 전송이 작동하는지 확인 if (afterFirstSend > initialMessageCount || afterFlagReset > afterBlockedSend) { Console.WriteLine("메시지 전송 기능이 작동하는 환경입니다."); @@ -438,4 +462,4 @@ public async Task Home_HandleKeyDown_PreventsDuplicateWithIsSendFlag() // 핵심 검증: 플래그가 true일 때와 false일 때의 동작 차이 Console.WriteLine($"테스트 결과 - 초기:{initialMessageCount}, 첫전송:{afterFirstSend}, 차단:{afterBlockedSend}, 해제:{afterFlagReset}"); } -} \ No newline at end of file +}