From 0337977bff4c7b090150da04415a553b117acced Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 9 Jun 2026 14:16:30 +0900 Subject: [PATCH] Show field-level inspection details --- .../Domain/VerificationModels.cs | 24 +++- .../Services/PocAutomationEngine.cs | 50 ++++++-- .../ViewModels/MainViewModel.cs | 31 ++++- .../ViewModels/ShellModels.cs | 10 ++ TornadoAce_CJOnStyle/Views/MainPage.xaml | 121 ++++++++++++++---- docs/poc-requirements.md | 6 +- 6 files changed, 198 insertions(+), 44 deletions(-) diff --git a/TornadoAce_CJOnStyle/Domain/VerificationModels.cs b/TornadoAce_CJOnStyle/Domain/VerificationModels.cs index 357d7a7..b817b2a 100644 --- a/TornadoAce_CJOnStyle/Domain/VerificationModels.cs +++ b/TornadoAce_CJOnStyle/Domain/VerificationModels.cs @@ -1,10 +1,32 @@ namespace TornadoAce_CJOnStyle.Domain { + public sealed record InspectionFieldResult( + string CueSheetField, + string SceneObjectName, + string ExpectedText, + string ActualText, + double MatchRate, + string Severity, + string Decision); + public sealed record InspectionResult( string CueSheetId, string ProgramTitle, string SceneName, double MatchRate, string Severity, - string Decision); + string Decision, + IReadOnlyList FieldResults) + { + public InspectionResult( + string cueSheetId, + string programTitle, + string sceneName, + double matchRate, + string severity, + string decision) + : this(cueSheetId, programTitle, sceneName, matchRate, severity, decision, []) + { + } + } } diff --git a/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs b/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs index 4c242b9..2452774 100644 --- a/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs +++ b/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs @@ -44,18 +44,28 @@ namespace TornadoAce_CJOnStyle.Services private InspectionResult Inspect(CueSheetRecord cueSheet, SceneDataSnapshot snapshot, SceneTemplateDefinition template) { var expectedObjects = BuildVisibleTextObjects(cueSheet, template); - var fieldScores = expectedObjects - .Select(expected => + var fieldResults = template.Mappings + .Select(mapping => { - snapshot.VisibleTextObjects.TryGetValue(expected.Key, out var actualValue); - var score = CalculateSimilarity(expected.Value, actualValue ?? string.Empty); - return new FieldInspectionScore(expected.Key, score); + var expectedValue = expectedObjects[mapping.SceneObjectName]; + snapshot.VisibleTextObjects.TryGetValue(mapping.SceneObjectName, out var actualValue); + var score = CalculateSimilarity(expectedValue, actualValue ?? string.Empty); + var severity = ResolveFieldSeverity(mapping.SceneObjectName, score); + + return new InspectionFieldResult( + mapping.CueSheetField, + mapping.SceneObjectName, + expectedValue, + actualValue ?? string.Empty, + score, + severity, + ResolveFieldDecision(severity)); }) .ToArray(); - var matchRate = fieldScores.Length == 0 ? 100 : fieldScores.Average(field => field.Score); - var hasImportantMismatch = fieldScores.Any(field => field.Score < 99.5 && IsImportantSceneObject(field.SceneObjectName)); - var mismatchCount = fieldScores.Count(field => field.Score < 99.5); + var matchRate = fieldResults.Length == 0 ? 100 : fieldResults.Average(field => field.MatchRate); + var hasImportantMismatch = fieldResults.Any(field => field.Severity == "High"); + var mismatchCount = fieldResults.Count(field => field.MatchRate < 99.5); var severity = hasImportantMismatch || mismatchCount > 1 ? "High" @@ -70,7 +80,7 @@ namespace TornadoAce_CJOnStyle.Services _ => "1차 통과" }; - return new InspectionResult(cueSheet.CueSheetId, cueSheet.ProgramTitle, snapshot.SceneName, matchRate, severity, decision); + return new InspectionResult(cueSheet.CueSheetId, cueSheet.ProgramTitle, snapshot.SceneName, matchRate, severity, decision, fieldResults); } private SceneDataSnapshot GenerateExtractedScene(CueSheetRecord cueSheet, SceneTemplateDefinition template) @@ -135,6 +145,26 @@ namespace TornadoAce_CJOnStyle.Services || sceneObjectName.Contains("COMPOSITION", StringComparison.OrdinalIgnoreCase); } + private static string ResolveFieldSeverity(string sceneObjectName, double matchRate) + { + if (matchRate >= 99.5) + { + return "Low"; + } + + return IsImportantSceneObject(sceneObjectName) ? "High" : "Medium"; + } + + private static string ResolveFieldDecision(string severity) + { + return severity switch + { + "High" => "운영자 확인", + "Medium" => "LLM 후보", + _ => "일치" + }; + } + private static double CalculateSimilarity(string expected, string actual) { var normalizedExpected = Normalize(expected); @@ -186,8 +216,6 @@ namespace TornadoAce_CJOnStyle.Services return previousRow[target.Length]; } - - private sealed record FieldInspectionScore(string SceneObjectName, double Score); } public sealed record CueSheetSelection(CueSheetRecord Record, int Position, int TotalCount); diff --git a/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs b/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs index 26755c9..e427f88 100644 --- a/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs +++ b/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs @@ -45,6 +45,7 @@ namespace TornadoAce_CJOnStyle.ViewModels LoadEndpoints(); LoadRequirementCoverage(); LoadInspectionQueue(initialInspectionSummary.Results); + LoadInspectionDetails(initialInspectionSummary.Results); } private string buildPhase = string.Empty; @@ -128,6 +129,8 @@ namespace TornadoAce_CJOnStyle.ViewModels public ObservableCollection InspectionQueue { get; } = []; + public ObservableCollection InspectionDetails { get; } = []; + public string LoginOperatorName { get => loginOperatorName; @@ -209,6 +212,7 @@ namespace TornadoAce_CJOnStyle.ViewModels StatusCards[2].Metric = $"{summary.AverageMatchRate:0.0}%"; StatusCards[2].Detail = $"확인 {summary.ReviewCount}건"; LoadInspectionQueue(summary.Results); + LoadInspectionDetails(summary.Results); } private void LoadStatusCards() @@ -251,7 +255,7 @@ namespace TornadoAce_CJOnStyle.ViewModels RequirementCoverage.Add(new PocRequirementViewModel("RFP-03", "Data Field Mapping", "PoC", "Scene 스냅샷 생성", Brush(0xFF37D67A))); RequirementCoverage.Add(new PocRequirementViewModel("RFP-04", "Tornado Scene API", "PoC", "Text/Image 추출 샘플", Brush(0xFF37D67A))); RequirementCoverage.Add(new PocRequirementViewModel("RFP-05", "관리자 매핑 기능", "초안", "매핑표 UI", Brush(0xFFB48CFF))); - RequirementCoverage.Add(new PocRequirementViewModel("RFP-06", "1·2단계 자동 검수", "PoC", "Diff + LLM 후보", Brush(0xFF45D6D1))); + RequirementCoverage.Add(new PocRequirementViewModel("RFP-06", "1·2단계 자동 검수", "PoC", "필드 상세 Diff + LLM 후보", Brush(0xFF45D6D1))); } private void LoadInspectionQueue(IEnumerable results) @@ -270,6 +274,31 @@ namespace TornadoAce_CJOnStyle.ViewModels } } + private void LoadInspectionDetails(IEnumerable results) + { + InspectionDetails.Clear(); + + var detailItems = results + .SelectMany(result => result.FieldResults.Select(field => new { Result = result, Field = field })) + .Where(item => item.Field.MatchRate < 99.5) + .OrderBy(item => item.Field.MatchRate) + .ThenBy(item => item.Result.CueSheetId) + .Take(5); + + foreach (var item in detailItems) + { + InspectionDetails.Add(new InspectionDetailItemViewModel( + item.Result.CueSheetId, + item.Field.SceneObjectName, + item.Field.ExpectedText, + item.Field.ActualText, + $"{item.Field.MatchRate:0.0}%", + item.Field.Severity, + item.Field.Decision, + SeverityBrush(item.Field.Severity))); + } + } + private void ApplySceneSnapshot(SceneDataSnapshot scene) { SceneSnapshotLabel = $"{scene.SceneName} / {scene.VisibleTextObjects.Count} text objects"; diff --git a/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs b/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs index af966ab..e0f5d81 100644 --- a/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs +++ b/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs @@ -74,4 +74,14 @@ namespace TornadoAce_CJOnStyle.ViewModels string Severity, string Decision, SolidColorBrush SeverityBrush); + + public sealed record InspectionDetailItemViewModel( + string CueSheetId, + string SceneObject, + string ExpectedText, + string ActualText, + string MatchRate, + string Severity, + string Decision, + SolidColorBrush SeverityBrush); } diff --git a/TornadoAce_CJOnStyle/Views/MainPage.xaml b/TornadoAce_CJOnStyle/Views/MainPage.xaml index 4756e67..628a7b7 100644 --- a/TornadoAce_CJOnStyle/Views/MainPage.xaml +++ b/TornadoAce_CJOnStyle/Views/MainPage.xaml @@ -529,38 +529,103 @@ BorderBrush="{StaticResource PanelStrokeBrush}" BorderThickness="1" CornerRadius="8"> - - - - + + + + + + + + + + + + FontFamily="Bahnschrift SemiBold" + FontSize="18" + Foreground="{StaticResource TextPrimaryBrush}" + Text="{x:Bind ViewModel.LlmPolicyText, Mode=OneWay}" /> + Style="{StaticResource BodyTextStyle}" + Text="{x:Bind ViewModel.LlmPolicyDetail, Mode=OneWay}" /> - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/docs/poc-requirements.md b/docs/poc-requirements.md index a190536..1b0c041 100644 --- a/docs/poc-requirements.md +++ b/docs/poc-requirements.md @@ -18,8 +18,8 @@ | RFP-03 | CG 포맷 제작 자동화, Data Field Mapping | 큐시트 필드와 Scene 객체 매핑 구현 | `SceneTemplateDefinition`, `PocAutomationEngine.GenerateScene` | | RFP-04 | Tornado Scene API | Scene visible Text/Image 스냅샷 샘플 구현 | `SceneDataSnapshot.ToPreviewJson` | | RFP-05 | 큐시트 데이터와 Tornado Scene 연동 관리자 기능 | 매핑표 UI 초안 구현 | `FieldMappings` 화면 | -| RFP-06 | 자동 검수 1단계 Text Diffing | 문자열 정규화/유사도 기반 비교 구현 | `PocAutomationEngine.InspectWorkspace` | -| RFP-07 | 자동 검수 2단계 LLM 중요도 피드백 | LLM 검토 후보 분류와 정책 문구 구현 | 검수 결과 `Severity`, `Decision`, LLM 정책 패널 | +| RFP-06 | 자동 검수 1단계 Text Diffing | 필드별 기대값/실제값/일치율 비교 구현 | `PocAutomationEngine.InspectWorkspace`, `InspectionFieldResult` | +| RFP-07 | 자동 검수 2단계 LLM 중요도 피드백 | LLM 검토 후보 분류와 제작자 상세 UI 구현 | 검수 결과 `Severity`, `Decision`, `InspectionDetails` | | RFP-08 | 제작자가 결과를 볼 수 있는 GUI | WinUI 3 운영 콘솔 초안 구현 | `MainPage.xaml` | ## 초기 PoC 완료 기준 @@ -35,5 +35,5 @@ - 큐시트/ERP Mock 데이터를 JSON 파일로 분리하여 실제 API 응답 구조와 맞춘다. - Tornado2/Tornado3 어댑터를 동일 인터페이스로 유지하고, 버전별 구현체를 분리한다. - Scene 내부 비노출 객체를 구분할 메타데이터 규칙을 정의한다. -- 1차 검수 실패 사유를 필드 단위로 화면에 표시한다. +- 1차 검수 실패 사유를 필드 단위로 누적하고 LLM 요청 후보로 전달한다. - LLM 요청/응답 스키마와 감사 로그 보관 정책을 별도 모델로 둔다.