Show field-level inspection details
This commit is contained in:
@@ -1,10 +1,32 @@
|
|||||||
namespace TornadoAce_CJOnStyle.Domain
|
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(
|
public sealed record InspectionResult(
|
||||||
string CueSheetId,
|
string CueSheetId,
|
||||||
string ProgramTitle,
|
string ProgramTitle,
|
||||||
string SceneName,
|
string SceneName,
|
||||||
double MatchRate,
|
double MatchRate,
|
||||||
string Severity,
|
string Severity,
|
||||||
string Decision);
|
string Decision,
|
||||||
|
IReadOnlyList<InspectionFieldResult> FieldResults)
|
||||||
|
{
|
||||||
|
public InspectionResult(
|
||||||
|
string cueSheetId,
|
||||||
|
string programTitle,
|
||||||
|
string sceneName,
|
||||||
|
double matchRate,
|
||||||
|
string severity,
|
||||||
|
string decision)
|
||||||
|
: this(cueSheetId, programTitle, sceneName, matchRate, severity, decision, [])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,18 +44,28 @@ namespace TornadoAce_CJOnStyle.Services
|
|||||||
private InspectionResult Inspect(CueSheetRecord cueSheet, SceneDataSnapshot snapshot, SceneTemplateDefinition template)
|
private InspectionResult Inspect(CueSheetRecord cueSheet, SceneDataSnapshot snapshot, SceneTemplateDefinition template)
|
||||||
{
|
{
|
||||||
var expectedObjects = BuildVisibleTextObjects(cueSheet, template);
|
var expectedObjects = BuildVisibleTextObjects(cueSheet, template);
|
||||||
var fieldScores = expectedObjects
|
var fieldResults = template.Mappings
|
||||||
.Select(expected =>
|
.Select(mapping =>
|
||||||
{
|
{
|
||||||
snapshot.VisibleTextObjects.TryGetValue(expected.Key, out var actualValue);
|
var expectedValue = expectedObjects[mapping.SceneObjectName];
|
||||||
var score = CalculateSimilarity(expected.Value, actualValue ?? string.Empty);
|
snapshot.VisibleTextObjects.TryGetValue(mapping.SceneObjectName, out var actualValue);
|
||||||
return new FieldInspectionScore(expected.Key, score);
|
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();
|
.ToArray();
|
||||||
|
|
||||||
var matchRate = fieldScores.Length == 0 ? 100 : fieldScores.Average(field => field.Score);
|
var matchRate = fieldResults.Length == 0 ? 100 : fieldResults.Average(field => field.MatchRate);
|
||||||
var hasImportantMismatch = fieldScores.Any(field => field.Score < 99.5 && IsImportantSceneObject(field.SceneObjectName));
|
var hasImportantMismatch = fieldResults.Any(field => field.Severity == "High");
|
||||||
var mismatchCount = fieldScores.Count(field => field.Score < 99.5);
|
var mismatchCount = fieldResults.Count(field => field.MatchRate < 99.5);
|
||||||
|
|
||||||
var severity = hasImportantMismatch || mismatchCount > 1
|
var severity = hasImportantMismatch || mismatchCount > 1
|
||||||
? "High"
|
? "High"
|
||||||
@@ -70,7 +80,7 @@ namespace TornadoAce_CJOnStyle.Services
|
|||||||
_ => "1차 통과"
|
_ => "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)
|
private SceneDataSnapshot GenerateExtractedScene(CueSheetRecord cueSheet, SceneTemplateDefinition template)
|
||||||
@@ -135,6 +145,26 @@ namespace TornadoAce_CJOnStyle.Services
|
|||||||
|| sceneObjectName.Contains("COMPOSITION", StringComparison.OrdinalIgnoreCase);
|
|| 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)
|
private static double CalculateSimilarity(string expected, string actual)
|
||||||
{
|
{
|
||||||
var normalizedExpected = Normalize(expected);
|
var normalizedExpected = Normalize(expected);
|
||||||
@@ -186,8 +216,6 @@ namespace TornadoAce_CJOnStyle.Services
|
|||||||
|
|
||||||
return previousRow[target.Length];
|
return previousRow[target.Length];
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record FieldInspectionScore(string SceneObjectName, double Score);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record CueSheetSelection(CueSheetRecord Record, int Position, int TotalCount);
|
public sealed record CueSheetSelection(CueSheetRecord Record, int Position, int TotalCount);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
|||||||
LoadEndpoints();
|
LoadEndpoints();
|
||||||
LoadRequirementCoverage();
|
LoadRequirementCoverage();
|
||||||
LoadInspectionQueue(initialInspectionSummary.Results);
|
LoadInspectionQueue(initialInspectionSummary.Results);
|
||||||
|
LoadInspectionDetails(initialInspectionSummary.Results);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string buildPhase = string.Empty;
|
private string buildPhase = string.Empty;
|
||||||
@@ -128,6 +129,8 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
|||||||
|
|
||||||
public ObservableCollection<InspectionQueueItemViewModel> InspectionQueue { get; } = [];
|
public ObservableCollection<InspectionQueueItemViewModel> InspectionQueue { get; } = [];
|
||||||
|
|
||||||
|
public ObservableCollection<InspectionDetailItemViewModel> InspectionDetails { get; } = [];
|
||||||
|
|
||||||
public string LoginOperatorName
|
public string LoginOperatorName
|
||||||
{
|
{
|
||||||
get => loginOperatorName;
|
get => loginOperatorName;
|
||||||
@@ -209,6 +212,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
|||||||
StatusCards[2].Metric = $"{summary.AverageMatchRate:0.0}%";
|
StatusCards[2].Metric = $"{summary.AverageMatchRate:0.0}%";
|
||||||
StatusCards[2].Detail = $"확인 {summary.ReviewCount}건";
|
StatusCards[2].Detail = $"확인 {summary.ReviewCount}건";
|
||||||
LoadInspectionQueue(summary.Results);
|
LoadInspectionQueue(summary.Results);
|
||||||
|
LoadInspectionDetails(summary.Results);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadStatusCards()
|
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-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-04", "Tornado Scene API", "PoC", "Text/Image 추출 샘플", Brush(0xFF37D67A)));
|
||||||
RequirementCoverage.Add(new PocRequirementViewModel("RFP-05", "관리자 매핑 기능", "초안", "매핑표 UI", Brush(0xFFB48CFF)));
|
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<InspectionResult> results)
|
private void LoadInspectionQueue(IEnumerable<InspectionResult> results)
|
||||||
@@ -270,6 +274,31 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadInspectionDetails(IEnumerable<InspectionResult> 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)
|
private void ApplySceneSnapshot(SceneDataSnapshot scene)
|
||||||
{
|
{
|
||||||
SceneSnapshotLabel = $"{scene.SceneName} / {scene.VisibleTextObjects.Count} text objects";
|
SceneSnapshotLabel = $"{scene.SceneName} / {scene.VisibleTextObjects.Count} text objects";
|
||||||
|
|||||||
@@ -74,4 +74,14 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
|||||||
string Severity,
|
string Severity,
|
||||||
string Decision,
|
string Decision,
|
||||||
SolidColorBrush SeverityBrush);
|
SolidColorBrush SeverityBrush);
|
||||||
|
|
||||||
|
public sealed record InspectionDetailItemViewModel(
|
||||||
|
string CueSheetId,
|
||||||
|
string SceneObject,
|
||||||
|
string ExpectedText,
|
||||||
|
string ActualText,
|
||||||
|
string MatchRate,
|
||||||
|
string Severity,
|
||||||
|
string Decision,
|
||||||
|
SolidColorBrush SeverityBrush);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -529,38 +529,103 @@
|
|||||||
BorderBrush="{StaticResource PanelStrokeBrush}"
|
BorderBrush="{StaticResource PanelStrokeBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="8">
|
CornerRadius="8">
|
||||||
<StackPanel Spacing="14">
|
<ScrollViewer VerticalScrollBarVisibility="Auto" VerticalScrollMode="Auto">
|
||||||
<TextBlock Style="{StaticResource SectionTitleTextStyle}" Text="Scene 데이터 API 샘플" />
|
<StackPanel Spacing="14">
|
||||||
<Border
|
<TextBlock Style="{StaticResource SectionTitleTextStyle}" Text="Scene 데이터 API 샘플" />
|
||||||
Padding="14"
|
<Border
|
||||||
Background="#101419"
|
Padding="14"
|
||||||
BorderBrush="{StaticResource PanelStrokeBrush}"
|
Background="#101419"
|
||||||
BorderThickness="1"
|
BorderBrush="{StaticResource PanelStrokeBrush}"
|
||||||
CornerRadius="8">
|
BorderThickness="1"
|
||||||
<StackPanel Spacing="8">
|
CornerRadius="8">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock
|
||||||
|
Style="{StaticResource LabelTextStyle}"
|
||||||
|
Text="{x:Bind ViewModel.SceneSnapshotLabel, Mode=OneWay}" />
|
||||||
|
<TextBlock
|
||||||
|
FontFamily="Cascadia Mono"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{StaticResource AccentCyanBrush}"
|
||||||
|
Text="{x:Bind ViewModel.SceneSnapshotText, Mode=OneWay}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Style="{StaticResource LabelTextStyle}" Text="LLM 검수 정책" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Style="{StaticResource LabelTextStyle}"
|
FontFamily="Bahnschrift SemiBold"
|
||||||
Text="{x:Bind ViewModel.SceneSnapshotLabel, Mode=OneWay}" />
|
FontSize="18"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
Text="{x:Bind ViewModel.LlmPolicyText, Mode=OneWay}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
FontFamily="Cascadia Mono"
|
Style="{StaticResource BodyTextStyle}"
|
||||||
FontSize="13"
|
Text="{x:Bind ViewModel.LlmPolicyDetail, Mode=OneWay}" />
|
||||||
Foreground="{StaticResource AccentCyanBrush}"
|
|
||||||
Text="{x:Bind ViewModel.SceneSnapshotText, Mode=OneWay}"
|
|
||||||
TextWrapping="WrapWholeWords" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
|
||||||
<StackPanel Spacing="6">
|
<Border
|
||||||
<TextBlock Style="{StaticResource LabelTextStyle}" Text="LLM 검수 정책" />
|
Height="1"
|
||||||
<TextBlock
|
Background="{StaticResource PanelStrokeBrush}" />
|
||||||
FontFamily="Bahnschrift SemiBold"
|
|
||||||
FontSize="18"
|
<TextBlock Style="{StaticResource SectionTitleTextStyle}" Text="검수 상세" />
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"
|
<ItemsControl ItemsSource="{x:Bind ViewModel.InspectionDetails, Mode=OneWay}">
|
||||||
Text="{x:Bind ViewModel.LlmPolicyText, Mode=OneWay}" />
|
<ItemsControl.ItemTemplate>
|
||||||
<TextBlock
|
<DataTemplate x:DataType="vm:InspectionDetailItemViewModel">
|
||||||
Style="{StaticResource BodyTextStyle}"
|
<Border
|
||||||
Text="{x:Bind ViewModel.LlmPolicyDetail, Mode=OneWay}" />
|
Margin="0,0,0,10"
|
||||||
|
Padding="10"
|
||||||
|
Background="{StaticResource PanelAltBrush}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<StackPanel Spacing="7">
|
||||||
|
<Grid ColumnSpacing="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock
|
||||||
|
FontFamily="Cascadia Mono"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource TextMutedBrush}"
|
||||||
|
Text="{x:Bind CueSheetId}" />
|
||||||
|
<TextBlock
|
||||||
|
FontFamily="Bahnschrift SemiBold"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
Text="{x:Bind SceneObject}" />
|
||||||
|
</StackPanel>
|
||||||
|
<Border
|
||||||
|
Grid.Column="1"
|
||||||
|
Padding="8,3"
|
||||||
|
Background="{x:Bind SeverityBrush}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<TextBlock
|
||||||
|
FontFamily="Bahnschrift SemiBold"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="#071013"
|
||||||
|
Text="{x:Bind Severity}" />
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock
|
||||||
|
Style="{StaticResource LabelTextStyle}"
|
||||||
|
Text="{x:Bind MatchRate}" />
|
||||||
|
<TextBlock
|
||||||
|
Style="{StaticResource BodyTextStyle}"
|
||||||
|
Text="{x:Bind ExpectedText}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock
|
||||||
|
Style="{StaticResource BodyTextStyle}"
|
||||||
|
Foreground="{StaticResource AccentCyanBrush}"
|
||||||
|
Text="{x:Bind ActualText}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock
|
||||||
|
Style="{StaticResource LabelTextStyle}"
|
||||||
|
Text="{x:Bind Decision}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</ScrollViewer>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
| RFP-03 | CG 포맷 제작 자동화, Data Field Mapping | 큐시트 필드와 Scene 객체 매핑 구현 | `SceneTemplateDefinition`, `PocAutomationEngine.GenerateScene` |
|
| RFP-03 | CG 포맷 제작 자동화, Data Field Mapping | 큐시트 필드와 Scene 객체 매핑 구현 | `SceneTemplateDefinition`, `PocAutomationEngine.GenerateScene` |
|
||||||
| RFP-04 | Tornado Scene API | Scene visible Text/Image 스냅샷 샘플 구현 | `SceneDataSnapshot.ToPreviewJson` |
|
| RFP-04 | Tornado Scene API | Scene visible Text/Image 스냅샷 샘플 구현 | `SceneDataSnapshot.ToPreviewJson` |
|
||||||
| RFP-05 | 큐시트 데이터와 Tornado Scene 연동 관리자 기능 | 매핑표 UI 초안 구현 | `FieldMappings` 화면 |
|
| RFP-05 | 큐시트 데이터와 Tornado Scene 연동 관리자 기능 | 매핑표 UI 초안 구현 | `FieldMappings` 화면 |
|
||||||
| RFP-06 | 자동 검수 1단계 Text Diffing | 문자열 정규화/유사도 기반 비교 구현 | `PocAutomationEngine.InspectWorkspace` |
|
| RFP-06 | 자동 검수 1단계 Text Diffing | 필드별 기대값/실제값/일치율 비교 구현 | `PocAutomationEngine.InspectWorkspace`, `InspectionFieldResult` |
|
||||||
| RFP-07 | 자동 검수 2단계 LLM 중요도 피드백 | LLM 검토 후보 분류와 정책 문구 구현 | 검수 결과 `Severity`, `Decision`, LLM 정책 패널 |
|
| RFP-07 | 자동 검수 2단계 LLM 중요도 피드백 | LLM 검토 후보 분류와 제작자 상세 UI 구현 | 검수 결과 `Severity`, `Decision`, `InspectionDetails` |
|
||||||
| RFP-08 | 제작자가 결과를 볼 수 있는 GUI | WinUI 3 운영 콘솔 초안 구현 | `MainPage.xaml` |
|
| RFP-08 | 제작자가 결과를 볼 수 있는 GUI | WinUI 3 운영 콘솔 초안 구현 | `MainPage.xaml` |
|
||||||
|
|
||||||
## 초기 PoC 완료 기준
|
## 초기 PoC 완료 기준
|
||||||
@@ -35,5 +35,5 @@
|
|||||||
- 큐시트/ERP Mock 데이터를 JSON 파일로 분리하여 실제 API 응답 구조와 맞춘다.
|
- 큐시트/ERP Mock 데이터를 JSON 파일로 분리하여 실제 API 응답 구조와 맞춘다.
|
||||||
- Tornado2/Tornado3 어댑터를 동일 인터페이스로 유지하고, 버전별 구현체를 분리한다.
|
- Tornado2/Tornado3 어댑터를 동일 인터페이스로 유지하고, 버전별 구현체를 분리한다.
|
||||||
- Scene 내부 비노출 객체를 구분할 메타데이터 규칙을 정의한다.
|
- Scene 내부 비노출 객체를 구분할 메타데이터 규칙을 정의한다.
|
||||||
- 1차 검수 실패 사유를 필드 단위로 화면에 표시한다.
|
- 1차 검수 실패 사유를 필드 단위로 누적하고 LLM 요청 후보로 전달한다.
|
||||||
- LLM 요청/응답 스키마와 감사 로그 보관 정책을 별도 모델로 둔다.
|
- LLM 요청/응답 스키마와 감사 로그 보관 정책을 별도 모델로 둔다.
|
||||||
|
|||||||
Reference in New Issue
Block a user