Persist PoC run audit logs

This commit is contained in:
2026-06-09 14:47:26 +09:00
parent c7adde3df2
commit 8b5b5b52a5
5 changed files with 120 additions and 37 deletions

View File

@@ -0,0 +1,66 @@
using System.Text.Json;
namespace TornadoAce_CJOnStyle.Services
{
public sealed class PocRunLogWriter
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};
public string Write(PocRunResult result)
{
var logDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"TornadoAce_CJOnStyle",
"Logs");
Directory.CreateDirectory(logDirectory);
var fileName = $"{DateTime.Now:yyyyMMdd-HHmmss}-{SanitizeFileName(result.Selection.Record.CueSheetId)}.json";
var logPath = Path.Combine(logDirectory, fileName);
var entry = PocRunLogEntry.From(result);
File.WriteAllText(logPath, JsonSerializer.Serialize(entry, JsonOptions));
return logPath;
}
private static string SanitizeFileName(string value)
{
var invalidChars = Path.GetInvalidFileNameChars();
var chars = value.Select(character => invalidChars.Contains(character) ? '_' : character).ToArray();
return new string(chars);
}
}
public sealed record PocRunLogEntry(
DateTime CreatedAt,
string CueSheetId,
string ProgramTitle,
string? ErpProgramCode,
string SceneName,
double AverageMatchRate,
int ReviewCount,
int LlmRequestCount,
IReadOnlyList<string> MappingChecks,
IReadOnlyList<string> AdapterChecks,
IReadOnlyList<string> LlmAuditSummaries)
{
public static PocRunLogEntry From(PocRunResult result)
{
return new PocRunLogEntry(
DateTime.Now,
result.Selection.Record.CueSheetId,
result.Selection.Record.ProgramTitle,
result.Schedule?.ProgramCode,
result.GeneratedScene.SceneName,
result.InspectionSummary.AverageMatchRate,
result.InspectionSummary.ReviewCount,
result.LlmReviewRequests.Count,
result.MappingValidations.Select(check => $"{check.CheckId}:{check.Status}:{check.Detail}").ToArray(),
result.AdapterChecks.Select(check => $"{check.CheckId}:{check.Status}:{check.Evidence}").ToArray(),
result.LlmReviewRequests.Select(request => request.AuditSummary).ToArray());
}
}
}

View File

@@ -13,6 +13,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
{ {
private readonly AutomationWorkspace workspace; private readonly AutomationWorkspace workspace;
private readonly PocAutomationEngine automationEngine; private readonly PocAutomationEngine automationEngine;
private readonly PocRunLogWriter pocRunLogWriter = new();
private int selectedCueSheetIndex; private int selectedCueSheetIndex;
private CueSheetRecord activeCueSheet = null!; private CueSheetRecord activeCueSheet = null!;
private ErpScheduleRecord? activeSchedule; private ErpScheduleRecord? activeSchedule;
@@ -40,6 +41,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
LaunchMessage = "TornadoAce"; LaunchMessage = "TornadoAce";
LlmPolicyText = "사내 LLM 우선, 외부 서비스는 정책 승인 후"; LlmPolicyText = "사내 LLM 우선, 외부 서비스는 정책 승인 후";
LlmPolicyDetail = "1차 Text Diffing 실패 건만 2차 LLM 판정으로 이동하고, 가격·구성·방송시간 변경은 높은 중요도로 표시합니다."; LlmPolicyDetail = "1차 Text Diffing 실패 건만 2차 LLM 판정으로 이동하고, 가격·구성·방송시간 변경은 높은 중요도로 표시합니다.";
PocRunLogText = "PoC 로그 대기";
LoadStatusCards(); LoadStatusCards();
LoadPipelineSteps(); LoadPipelineSteps();
@@ -63,6 +65,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
private string sceneSnapshotText = string.Empty; private string sceneSnapshotText = string.Empty;
private string llmPolicyText = string.Empty; private string llmPolicyText = string.Empty;
private string llmPolicyDetail = string.Empty; private string llmPolicyDetail = string.Empty;
private string pocRunLogText = string.Empty;
private string loginOperatorName = string.Empty; private string loginOperatorName = string.Empty;
private string launchMessage = string.Empty; private string launchMessage = string.Empty;
private Visibility loginScreenVisibility = Visibility.Visible; private Visibility loginScreenVisibility = Visibility.Visible;
@@ -123,6 +126,12 @@ namespace TornadoAce_CJOnStyle.ViewModels
set => SetProperty(ref llmPolicyDetail, value); set => SetProperty(ref llmPolicyDetail, value);
} }
public string PocRunLogText
{
get => pocRunLogText;
set => SetProperty(ref pocRunLogText, value);
}
public ObservableCollection<StatusCardViewModel> StatusCards { get; } = []; public ObservableCollection<StatusCardViewModel> StatusCards { get; } = [];
public ObservableCollection<PipelineStepViewModel> PipelineSteps { get; } = []; public ObservableCollection<PipelineStepViewModel> PipelineSteps { get; } = [];
@@ -235,6 +244,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
private void RunPoc() private void RunPoc()
{ {
var result = automationEngine.RunFullPoc(selectedCueSheetIndex, workspace.ActiveTemplate); var result = automationEngine.RunFullPoc(selectedCueSheetIndex, workspace.ActiveTemplate);
var logPath = pocRunLogWriter.Write(result);
activeCueSheet = result.Selection.Record; activeCueSheet = result.Selection.Record;
activeSchedule = result.Schedule; activeSchedule = result.Schedule;
activeScene = result.GeneratedScene; activeScene = result.GeneratedScene;
@@ -243,6 +253,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
ActiveTemplateName = workspace.ActiveTemplate.TemplateName; ActiveTemplateName = workspace.ActiveTemplate.TemplateName;
InspectionSummary = FormatInspectionSummary(result.InspectionSummary); InspectionSummary = FormatInspectionSummary(result.InspectionSummary);
OperatorMessage = FormatPocRunSummary(result); OperatorMessage = FormatPocRunSummary(result);
PocRunLogText = $"로그 저장: {Path.GetFileName(logPath)}";
StatusCards[0].Metric = result.Selection.TotalCount.ToString(); StatusCards[0].Metric = result.Selection.TotalCount.ToString();
StatusCards[0].Detail = $"ERP {FormatScheduleStatus(result.Schedule)}"; StatusCards[0].Detail = $"ERP {FormatScheduleStatus(result.Schedule)}";

View File

@@ -116,41 +116,47 @@
<StackPanel <StackPanel
Grid.Column="1" Grid.Column="1"
Orientation="Horizontal" HorizontalAlignment="Right"
Spacing="10" Spacing="8"
VerticalAlignment="Top"> VerticalAlignment="Top">
<Button <StackPanel Orientation="Horizontal" Spacing="10">
Command="{x:Bind ViewModel.RunPocCommand}" <Button
Style="{StaticResource PrimaryActionButtonStyle}"> Command="{x:Bind ViewModel.RunPocCommand}"
<StackPanel Orientation="Horizontal" Spacing="7"> Style="{StaticResource PrimaryActionButtonStyle}">
<SymbolIcon Symbol="Play" /> <StackPanel Orientation="Horizontal" Spacing="7">
<TextBlock Text="PoC 실행" /> <SymbolIcon Symbol="Play" />
</StackPanel> <TextBlock Text="PoC 실행" />
</Button> </StackPanel>
<Button </Button>
Command="{x:Bind ViewModel.RefreshCueSheetCommand}" <Button
Style="{StaticResource GhostActionButtonStyle}"> Command="{x:Bind ViewModel.RefreshCueSheetCommand}"
<StackPanel Orientation="Horizontal" Spacing="7"> Style="{StaticResource GhostActionButtonStyle}">
<SymbolIcon Symbol="Refresh" /> <StackPanel Orientation="Horizontal" Spacing="7">
<TextBlock Text="큐시트 동기화" /> <SymbolIcon Symbol="Refresh" />
</StackPanel> <TextBlock Text="큐시트 동기화" />
</Button> </StackPanel>
<Button </Button>
Command="{x:Bind ViewModel.GenerateSceneCommand}" <Button
Style="{StaticResource GhostActionButtonStyle}"> Command="{x:Bind ViewModel.GenerateSceneCommand}"
<StackPanel Orientation="Horizontal" Spacing="7"> Style="{StaticResource GhostActionButtonStyle}">
<SymbolIcon Symbol="Play" /> <StackPanel Orientation="Horizontal" Spacing="7">
<TextBlock Text="Scene 생성" /> <SymbolIcon Symbol="Play" />
</StackPanel> <TextBlock Text="Scene 생성" />
</Button> </StackPanel>
<Button </Button>
Command="{x:Bind ViewModel.RunInspectionCommand}" <Button
Style="{StaticResource GhostActionButtonStyle}"> Command="{x:Bind ViewModel.RunInspectionCommand}"
<StackPanel Orientation="Horizontal" Spacing="7"> Style="{StaticResource GhostActionButtonStyle}">
<SymbolIcon Symbol="Accept" /> <StackPanel Orientation="Horizontal" Spacing="7">
<TextBlock Text="검수 실행" /> <SymbolIcon Symbol="Accept" />
</StackPanel> <TextBlock Text="검수 실행" />
</Button> </StackPanel>
</Button>
</StackPanel>
<TextBlock
HorizontalAlignment="Right"
Style="{StaticResource LabelTextStyle}"
Text="{x:Bind ViewModel.PocRunLogText, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>

View File

@@ -12,7 +12,7 @@
- `Domain`: 큐시트, Scene 템플릿, Scene 데이터 스냅샷, 검수 결과 모델. - `Domain`: 큐시트, Scene 템플릿, Scene 데이터 스냅샷, 검수 결과 모델.
- `Data`: 큐시트, ERP 편성, Scene 템플릿, Scene 추출값을 담은 JSON 기반 PoC 샘플. - `Data`: 큐시트, ERP 편성, Scene 템플릿, Scene 추출값을 담은 JSON 기반 PoC 샘플.
- `Services`: 실제 API/Tornado SDK 연결을 받을 인터페이스, JSON 로더, 목 워크스페이스, Mock Tornado adapter. - `Services`: 실제 API/Tornado SDK 연결을 받을 인터페이스, JSON 로더, 목 워크스페이스, Mock Tornado adapter, PoC 실행 로그 저장.
- `ViewModels`: 운영 콘솔에 표시할 상태 카드, 파이프라인, 매핑표, 검수 큐. - `ViewModels`: 운영 콘솔에 표시할 상태 카드, 파이프라인, 매핑표, 검수 큐.
- `Views`: WinUI 3 기반 첫 화면. 예제 프로젝트와 같은 데스크톱 앱 방향으로 유지. - `Views`: WinUI 3 기반 첫 화면. 예제 프로젝트와 같은 데스크톱 앱 방향으로 유지.

View File

@@ -26,7 +26,7 @@
1. Windows 데스크톱 앱으로 실행 가능한 WinUI 3 빌드가 유지된다. 1. Windows 데스크톱 앱으로 실행 가능한 WinUI 3 빌드가 유지된다.
2. CJ OnStyle/TornadoAce 정체성이 드러나는 로그인 도입 화면과 앱 아이콘이 적용된다. 2. CJ OnStyle/TornadoAce 정체성이 드러나는 로그인 도입 화면과 앱 아이콘이 적용된다.
3. `PoC 실행`으로 큐시트 선택, ERP 매칭, Scene 스냅샷 생성, 자동 검수, LLM 후보 생성이 한 번에 갱신된다. 3. `PoC 실행`으로 큐시트 선택, ERP 매칭, Scene 스냅샷 생성, 자동 검수, LLM 후보 생성, 실행 로그 저장이 한 번에 갱신된다.
4. RFP 구축 범위가 화면 또는 문서에서 추적 가능해야 한다. 4. RFP 구축 범위가 화면 또는 문서에서 추적 가능해야 한다.
5. 추후 실제 큐시트 API, ERP API, Tornado SDK, LLM 서비스로 교체할 인터페이스 경계가 유지된다. 5. 추후 실제 큐시트 API, ERP API, Tornado SDK, LLM 서비스로 교체할 인터페이스 경계가 유지된다.
@@ -37,4 +37,4 @@
- 실제 Tornado2 SDK 구현체와 Tornado3 SDK 구현체의 메서드 차이를 `ITornadoSceneAdapter`에 맞춰 정리한다. - 실제 Tornado2 SDK 구현체와 Tornado3 SDK 구현체의 메서드 차이를 `ITornadoSceneAdapter`에 맞춰 정리한다.
- Scene 내부 비노출 객체를 구분할 메타데이터 규칙을 정의한다. - Scene 내부 비노출 객체를 구분할 메타데이터 규칙을 정의한다.
- LLM 응답 스키마와 감사 로그 보관 정책을 실제 사내 정책에 맞춘다. - LLM 응답 스키마와 감사 로그 보관 정책을 실제 사내 정책에 맞춘다.
- PoC 실행 결과를 파일 또는 운영 로그로 저장하는 정책을 정한다. - PoC 실행 로그의 보관 기간, 열람 권한, 운영 로그 연동 방식을 정한다.