1
0
Fork 0

feat(tdd): implement SSOT canon, AGENTS.md, ultra-strict design audit, E2E scenarios, and Forgejo release pipeline
Some checks are pending
Forgejo Release / build-and-release (push) Waiting to run

This commit is contained in:
Yun Chan 2026-09-03 12:21:40 +09:00
parent 24aa70ae32
commit 4cd9c678c0
25 changed files with 1684 additions and 238 deletions

View file

@ -97,17 +97,25 @@ public partial class App : Application
private async Task RunQuickAsync(IReadOnlyList<string> files, string outputExtension)
{
// 옵션 팝업~변환 경로 전체를 명시적 종료 모드로 먼저 감싼다.
// 핵심(크래시 방지): 기본 OnLastWindowClose면 옵션 팝업(첫·유일 창)이 닫히는 순간 "마지막 창 종료" 규칙이
// 발동해 앱이 종료 시퀀스에 진입하고, 직후 ShutdownMode 설정/변환이 InvalidOperationException으로 크래시한다.
// (또한 진행 창을 닫아도 변환을 취소한 '뒤' 명시 종료해 ffmpeg 고아 프로세스를 막는다.)
ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown;
// 빠른 변환 전, 출력 형식에 맞춘 간단 옵션 팝업.
// '자세히 옵션…'이면 풀 UI(MainWindow)로 전환, '취소'면 종료, '변환'이면 선택 옵션으로 진행.
var optWin = new QuickOptionsWindow(outputExtension, files.Count, Settings);
var confirmed = optWin.ShowDialog();
if (optWin.OpenFullUi) { ShowConvertDialog(files); return; }
if (optWin.OpenFullUi)
{
// 풀 UI로 전환 — 종료 책임을 메인 창 수명(OnLastWindowClose)에 되돌린다.
ShutdownMode = System.Windows.ShutdownMode.OnLastWindowClose;
ShowConvertDialog(files);
return;
}
if (confirmed != true) { Shutdown(0); return; }
// 변환 경로 동안은 명시적 종료 모드 — 진행 창을 닫아도 변환을 취소(ffmpeg 종료)한 '뒤' 앱을 종료한다.
// (기본 OnLastWindowClose면 창 닫는 즉시 종료가 시작돼 ffmpeg가 고아로 백그라운드에 남는다.)
ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown;
var logPath = Path.Combine(Path.GetTempPath(), "Everything2Everything_quick.log");
var log = new System.Text.StringBuilder();
log.AppendLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Quick start → {outputExtension}, {files.Count} file(s)");

View file

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.6</Version>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<Nullable>enable</Nullable>
@ -35,3 +36,4 @@
</ItemGroup>
</Project>

View file

@ -118,6 +118,17 @@
<Setter Property="Foreground" Value="{StaticResource FsTextSecondary}"/>
</Style>
<!-- Fluent typography aliases (Used across dialogs) -->
<Style x:Key="TextSubtitle" TargetType="TextBlock" BasedOn="{StaticResource FsBodyStyle}">
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="TextCaption" TargetType="TextBlock" BasedOn="{StaticResource FsCaptionStyle}"/>
<Style x:Key="TextBody" TargetType="TextBlock" BasedOn="{StaticResource FsBodyStyle}"/>
<Style x:Key="TextBodyStrong" TargetType="TextBlock" BasedOn="{StaticResource FsBodyStyle}">
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<!-- Slider style: 2px track, 14px round thumb, blue fill up to value -->
<Style x:Key="FsSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="Transparent"/>

View file

@ -208,7 +208,7 @@
<!-- AI Task -->
<StackPanel x:Name="AiTaskPanel" Margin="0,0,0,32">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Image Width="15" Height="15" VerticalAlignment="Center" Margin="0,0,6,0"
RenderOptions.BitmapScalingMode="HighQuality"
Source="pack://application:,,,/Assets/glyph-ai.png"/>
@ -504,16 +504,17 @@
Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}"
BorderThickness="1" CornerRadius="20" Padding="4">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ToggleButton x:Name="TabActiveBtn"
Style="{StaticResource FsTabPillStyle}"
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Active">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Active Queue" VerticalAlignment="Center"/>
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}">
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
<TextBlock x:Name="TabActiveBadge" Text="0"
FontSize="10" FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource FsTextTertiary}"/>
</Border>
</StackPanel>
@ -523,11 +524,12 @@
Style="{StaticResource FsTabPillStyle}"
Command="{Binding TabCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="Past">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Past Results" VerticalAlignment="Center"/>
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}">
<Border Margin="8,0,0,0" Style="{StaticResource FsTabBadgeStyle}" VerticalAlignment="Center">
<TextBlock x:Name="TabPastBadge" Text="0"
FontSize="10" FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource FsTextTertiary}"/>
</Border>
</StackPanel>
@ -536,22 +538,47 @@
</Border>
<!-- Actions -->
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Content="⚙ 설정" Margin="0,0,8,0"
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding SettingsCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Button Content="Register Menu" Margin="0,0,8,0"
Command="{Binding SettingsCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Settings24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="설정" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding RegisterCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Button Content="Diagnose" Margin="0,0,8,0"
Command="{Binding RegisterCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="AppsAddIn24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="우클릭 메뉴 등록" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding DiagnoseCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Button Content="Export Log" Margin="0,0,8,0"
Command="{Binding DiagnoseCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Stethoscope24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="진단" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding ExportLogCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Button x:Name="ClearAllButton" Content="Clear All"
Command="{Binding ExportLogCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="ArrowDownload24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="로그 내보내기" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="ClearAllButton" Margin="0,0,8,0"
Style="{StaticResource FsSecondaryButtonStyle}"
Command="{Binding ClearAllCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
Command="{Binding ClearAllCommand, RelativeSource={RelativeSource AncestorType=Window}}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Delete24" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="목록 비우기" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Border>
@ -700,9 +727,35 @@
</Grid>
<!-- Past Results view (default) -->
<ScrollViewer x:Name="PastResultsView" VerticalScrollBarVisibility="Auto"
Padding="24">
<ItemsControl x:Name="PastResultsList"
<Grid x:Name="PastResultsContainer">
<!-- 빈 상태 안내 (Empty State) -->
<Grid x:Name="PastResultsEmpty" Visibility="Collapsed">
<Border Background="{StaticResource FsBgBase}" Padding="32">
<Grid MaxWidth="440" HorizontalAlignment="Center" VerticalAlignment="Center">
<Rectangle RadiusX="10" RadiusY="10"
Stroke="{StaticResource FsBorderHairline}"
StrokeThickness="1.25"
StrokeDashArray="5 4"
Fill="#04FFFFFF"/>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="48,52">
<ui:SymbolIcon Symbol="History24" FontSize="48"
Foreground="{StaticResource FsTextTertiary}"
HorizontalAlignment="Center"/>
<TextBlock Margin="0,16,0,0" FontSize="16" FontWeight="SemiBold"
Foreground="{StaticResource FsTextPrimary}"
HorizontalAlignment="Center"
Text="변환 이력이 없습니다"/>
<TextBlock Margin="0,6,0,0" Style="{StaticResource FsCaptionStyle}"
HorizontalAlignment="Center"
TextAlignment="Center"
Text="파일을 변환하면 완료된 내역과 절감된 용량이 여기에 기록됩니다"/>
</StackPanel>
</Grid>
</Border>
</Grid>
<ScrollViewer x:Name="PastResultsView" VerticalScrollBarVisibility="Auto"
Padding="24">
<ItemsControl x:Name="PastResultsList"
ItemsSource="{Binding PastResults, RelativeSource={RelativeSource AncestorType=Window}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
@ -813,6 +866,7 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Grid>
<!-- /좌측 컬럼 끝 -->

View file

@ -171,7 +171,8 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
TabPastBtn.IsChecked = tag == "Past";
ActiveQueueView.Visibility = tag == "Active" ? Visibility.Visible : Visibility.Collapsed;
PastResultsView.Visibility = tag == "Past" ? Visibility.Visible : Visibility.Collapsed;
PastResultsContainer.Visibility = tag == "Past" ? Visibility.Visible : Visibility.Collapsed;
UpdatePastResultsVisibility();
}
// ============== Drag & Drop ==============
@ -247,11 +248,19 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
ActiveQueueScroll.Visibility = hasItems ? Visibility.Visible : Visibility.Collapsed;
}
private void UpdatePastResultsVisibility()
{
var hasItems = _pastResults.Count > 0;
PastResultsEmpty.Visibility = hasItems ? Visibility.Collapsed : Visibility.Visible;
PastResultsView.Visibility = hasItems ? Visibility.Visible : Visibility.Collapsed;
}
private void UpdateBadges()
{
TabActiveBadge.Text = _activeQueue.Count.ToString(CultureInfo.InvariantCulture);
var count = _pastResults.Sum(g => g.Entries.Count);
TabPastBadge.Text = count.ToString(CultureInfo.InvariantCulture);
UpdatePastResultsVisibility();
}
private void UpdateProcessQueueButton()

View file

@ -10,6 +10,13 @@
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
Closing="OnWindowClosing">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>

View file

@ -6,9 +6,17 @@
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
Background="{StaticResource FsBgBase}">
Background="{DynamicResource FsBgBase}">
<Grid>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="{StaticResource FsBgBase}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
@ -51,10 +59,10 @@
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
IsEnabled="False" Click="OnVerifyOpenAi"/>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="OpenAiDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0"/>
<TextBlock x:Name="OpenAiStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}"/>
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="OpenAiStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
</StackPanel>
<!-- Anthropic -->
@ -68,23 +76,23 @@
Style="{StaticResource FsPasswordInputStyle}"
PasswordChanged="OnAnthropicKeyChanged"/>
<Button x:Name="AnthropicVerifyBtn" Grid.Column="1" Content="확인"
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
IsEnabled="False" Click="OnVerifyAnthropic"/>
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
IsEnabled="False" Click="OnVerifyAnthropic"/>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="AnthropicDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0"/>
<TextBlock x:Name="AnthropicStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}"/>
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="AnthropicStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
</StackPanel>
<!-- Codex CLI (OAuth) -->
<TextBlock Text="Codex CLI (OAuth · API 키 불필요)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
<Ellipse x:Name="CodexDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0"/>
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="CodexStatus" Text="확인 중…" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
<Button x:Name="CodexVerifyBtn" Content="테스트" Style="{StaticResource FsSecondaryButtonStyle}"
Margin="12,0,0,0" Padding="10,4" IsEnabled="False" Click="OnVerifyCodex"/>
Margin="12,0,0,0" Padding="10,4" IsEnabled="False" Click="OnVerifyCodex" VerticalAlignment="Center"/>
</StackPanel>
<!-- 모델 -->
@ -113,10 +121,10 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse x:Name="FfmpegDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0"/>
<TextBlock Text="FFmpeg" Style="{StaticResource FsBodyStyle}" FontWeight="Medium"/>
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock Text="FFmpeg" Style="{StaticResource FsBodyStyle}" FontWeight="Medium" VerticalAlignment="Center"/>
<TextBlock x:Name="FfmpegStatus" Text="미설치" Style="{StaticResource FsCaptionStyle}" Margin="8,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="영상/오디오 변환 (mp4·webm·mp3·flac…)" Style="{StaticResource FsCaptionStyle}" Margin="15,3,0,0"/>
@ -134,10 +142,10 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse x:Name="LibreDot" Style="{StaticResource FsLossDotStyle}"
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0"/>
<TextBlock Text="LibreOffice" Style="{StaticResource FsBodyStyle}" FontWeight="Medium"/>
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock Text="LibreOffice" Style="{StaticResource FsBodyStyle}" FontWeight="Medium" VerticalAlignment="Center"/>
<TextBlock x:Name="LibreStatus" Text="미설치" Style="{StaticResource FsCaptionStyle}" Margin="8,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="한글/Word/문서 변환 (HWP·DOCX→PDF/이미지). 한글은 H2Orestart 확장 필요." Style="{StaticResource FsCaptionStyle}" Margin="15,3,0,0" TextWrapping="Wrap"/>
@ -155,9 +163,9 @@
<!-- ===== 푸터 ===== -->
<Border Grid.Row="2" Background="{StaticResource FsBgPanel}"
BorderBrush="{StaticResource FsBorderSubtle}" BorderThickness="0,1,0,0" Padding="24,14">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="닫기" Style="{StaticResource FsSecondaryButtonStyle}" Padding="18,8" Click="OnClose"/>
<Button Content="저장" Style="{StaticResource FsPrimaryButtonStyle}" Padding="22,8" Margin="8,0,0,0" Click="OnSave"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Content="닫기" Style="{StaticResource FsSecondaryButtonStyle}" Padding="20,8" Click="OnClose"/>
<Button Content="저장" Style="{StaticResource FsPrimaryButtonStyle}" Padding="20,8" Margin="8,0,0,0" Click="OnSave"/>
</StackPanel>
</Border>
</Grid>