1
0
Fork 0

Phase 2: MSIX 패키지 + IExplorerCommand 셸 익스텐션

C++ 셸 익스텐션 (src/EverythingToJpeg.Shell/)
- WRL RuntimeClass 패턴으로 IExplorerCommand 두 핸들러 구현
- Quick(빠른 변환) / Dialog(설정 창) verb를 다른 CLSID로 분리
- Invoke()에서 EverythingToJpeg.exe로 verb + 파일 경로 전달
- VS 2026 빌드 검증, /utf-8 한글 라벨 지원

MSIX 패키징 (packaging/)
- Package.appxmanifest: com:SurrogateServer + desktop4:FileExplorerContextMenus
- 26개 확장자 × 2 verb 자동 노출
- BuildMsix.ps1: dotnet publish + msbuild + makeappx 일관 파이프라인
- CreateDevCert.ps1 / Install-EverythingToJpeg.ps1: 자체 서명 인증서 워크플로
- GenerateAssets.ps1: placeholder 로고 자동 생성

CI/CD
- .github/workflows/release.yml: 태그 푸시 시 미서명 MSIX 자동 빌드 + 첨부

검증
- 50MB MSIX 산출 확인 (packaging/dist/EverythingToJpeg-x64.msix)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yun Chan 2026-05-06 13:03:16 +09:00
parent 8fa4a613d7
commit f2c8610ff6
19 changed files with 973 additions and 41 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

138
packaging/BuildMsix.ps1 Normal file
View file

@ -0,0 +1,138 @@
#Requires -Version 5.1
# MSIX 빌드 파이프라인.
# 1) .NET App publish (framework-dependent)
# 2) C++ Shell DLL 빌드
# 3) 패키지 Layout 디렉토리 구성
# 4) makeappx pack
# 5) (선택) signtool sign
[CmdletBinding()]
param(
[string]$Configuration = 'Release',
[string]$Platform = 'x64',
[switch]$Sign,
[string]$PfxPath,
[securestring]$PfxPassword,
[string]$CertThumbprint,
[switch]$SelfContained
)
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent $PSScriptRoot
$packagingDir = $PSScriptRoot
$layoutDir = Join-Path $packagingDir 'Layout'
$distDir = Join-Path $packagingDir 'dist'
$manifestPath = Join-Path $packagingDir 'Package.appxmanifest'
$assetsSrc = Join-Path $packagingDir 'Assets'
$appProj = Join-Path $repoRoot 'src\EverythingToJpeg.App\EverythingToJpeg.App.csproj'
$shellProj = Join-Path $repoRoot 'src\EverythingToJpeg.Shell\EverythingToJpeg.Shell.vcxproj'
function Find-WindowsSdkTool {
param([string]$ToolName)
$sdkRoots = @(
"${env:ProgramFiles(x86)}\Windows Kits\10\bin",
"$env:ProgramFiles\Windows Kits\10\bin"
) | Where-Object { Test-Path $_ }
foreach ($root in $sdkRoots) {
$versions = Get-ChildItem -Path $root -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } |
Sort-Object Name -Descending
foreach ($v in $versions) {
$candidate = Join-Path $v.FullName "x64\$ToolName"
if (Test-Path $candidate) { return [string]$candidate }
}
}
return $null
}
$makeappx = Find-WindowsSdkTool 'makeappx.exe'
if (-not $makeappx) { throw 'makeappx.exe를 찾지 못했습니다. Windows 10 SDK가 필요합니다.' }
Write-Host "makeappx: $makeappx"
if ($Sign) {
$signtool = Find-WindowsSdkTool 'signtool.exe'
if (-not $signtool) { throw 'signtool.exe를 찾지 못했습니다.' }
Write-Host "signtool: $signtool"
}
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (-not (Test-Path $vswhere)) { throw 'vswhere.exe를 찾지 못했습니다.' }
$msbuild = (& $vswhere -latest -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1)
if (-not $msbuild) { throw 'MSBuild를 찾지 못했습니다.' }
Write-Host "msbuild: $msbuild"
# ---- 1) .NET App publish ----
Write-Host ''
Write-Host '[1/5] .NET App publish'
$publishOut = Join-Path $repoRoot ('artifacts\publish\app-' + $Platform.ToLower())
if (Test-Path $publishOut) { Remove-Item $publishOut -Recurse -Force }
$rid = if ($Platform -eq 'ARM64') { 'win-arm64' } else { 'win-x64' }
$selfFlag = if ($SelfContained) { 'true' } else { 'false' }
& dotnet publish $appProj -c $Configuration -r $rid --self-contained $selfFlag -o $publishOut | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'dotnet publish 실패' }
# ---- 2) C++ Shell DLL ----
Write-Host ''
Write-Host '[2/5] C++ Shell DLL 빌드'
& $msbuild $shellProj /t:Restore /p:RestorePackagesConfig=true /p:Configuration=$Configuration /p:Platform=$Platform /v:minimal | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'Shell restore 실패' }
& $msbuild $shellProj /p:Configuration=$Configuration /p:Platform=$Platform /m /v:minimal | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'Shell build 실패' }
$shellDll = Join-Path $repoRoot ("src\EverythingToJpeg.Shell\$Platform\$Configuration\EverythingToJpeg.Shell.dll")
if (-not (Test-Path $shellDll)) { throw "Shell DLL 산출물 없음: $shellDll" }
# ---- 3) Layout 디렉토리 ----
Write-Host ''
Write-Host '[3/5] Layout 디렉토리 구성'
if (Test-Path $layoutDir) { Remove-Item $layoutDir -Recurse -Force }
New-Item -ItemType Directory -Path $layoutDir | Out-Null
Copy-Item -Path (Join-Path $publishOut '*') -Destination $layoutDir -Recurse -Force
Copy-Item -Path $shellDll -Destination $layoutDir -Force
$layoutAssets = Join-Path $layoutDir 'Assets'
New-Item -ItemType Directory -Path $layoutAssets -Force | Out-Null
Copy-Item -Path (Join-Path $assetsSrc '*') -Destination $layoutAssets -Force
Copy-Item -Path $manifestPath -Destination (Join-Path $layoutDir 'AppxManifest.xml') -Force
# ---- 4) makeappx pack ----
Write-Host ''
Write-Host '[4/5] makeappx pack'
if (-not (Test-Path $distDir)) { New-Item -ItemType Directory -Path $distDir | Out-Null }
$msixPath = Join-Path $distDir ("EverythingToJpeg-$($Platform.ToLower()).msix")
if (Test-Path $msixPath) { Remove-Item $msixPath -Force }
& $makeappx pack /d $layoutDir /p $msixPath /o | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'makeappx pack 실패' }
Write-Host "✅ MSIX 산출: $msixPath"
# ---- 5) (선택) sign ----
if ($Sign) {
Write-Host ''
Write-Host '[5/5] signtool sign'
if ($CertThumbprint) {
& $signtool sign /fd SHA256 /sha1 $CertThumbprint /tr 'http://timestamp.digicert.com' /td SHA256 $msixPath | Out-Host
} elseif ($PfxPath) {
if (-not $PfxPassword) {
$PfxPassword = Read-Host -AsSecureString -Prompt 'PFX 비밀번호'
}
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($PfxPassword)
try {
$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
& $signtool sign /fd SHA256 /a /f $PfxPath /p $plain /tr 'http://timestamp.digicert.com' /td SHA256 $msixPath | Out-Host
} finally {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
} else {
throw '서명을 하려면 -CertThumbprint 또는 -PfxPath 가 필요합니다.'
}
if ($LASTEXITCODE -ne 0) { throw 'signtool sign 실패' }
Write-Host '✅ 서명 완료'
} else {
Write-Host ''
Write-Host '[5/5] 서명 건너뜀 (-Sign 미지정). 사이드로드 시 인증서 필요.'
}
Write-Host ''
Write-Host "최종 산출물: $msixPath"

View file

@ -0,0 +1,44 @@
#Requires -Version 5.1
# 자체 서명 코드 사이닝 인증서 생성 + PFX export.
# Subject가 Package.appxmanifest 의 <Identity Publisher="..."> 와 정확히 일치해야 한다.
[CmdletBinding()]
param(
[string]$Subject = 'CN=EverythingToJpegDev',
[string]$OutputPfx = (Join-Path $PSScriptRoot 'EverythingToJpeg-DevCert.pfx'),
[securestring]$Password
)
$ErrorActionPreference = 'Stop'
if (-not $Password) {
Write-Host '인증서 PFX 보호용 비밀번호를 입력하세요. (5대 PC에 설치할 때 필요합니다)'
$Password = Read-Host -AsSecureString -Prompt '비밀번호'
}
Write-Host "Creating self-signed code-signing certificate: $Subject"
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject $Subject `
-KeyAlgorithm RSA `
-KeyLength 3072 `
-Provider 'Microsoft Enhanced RSA and AES Cryptographic Provider' `
-KeyExportPolicy Exportable `
-KeyUsage DigitalSignature `
-CertStoreLocation 'Cert:\CurrentUser\My' `
-HashAlgorithm SHA256 `
-NotAfter (Get-Date).AddYears(5) `
-FriendlyName 'EverythingToJpeg Dev'
Write-Host "Thumbprint: $($cert.Thumbprint)"
Write-Host "Exporting PFX: $OutputPfx"
Export-PfxCertificate -Cert $cert -FilePath $OutputPfx -Password $Password | Out-Null
Write-Host ''
Write-Host '--- 다음 단계 ---'
Write-Host " 1. 이 PFX 파일을 5대 PC 각각에 복사"
Write-Host " 2. 각 PC에서 관리자 PowerShell로:"
Write-Host ' Import-PfxCertificate -CertStoreLocation "Cert:\LocalMachine\TrustedPeople" -FilePath <경로>.pfx -Password (Read-Host -AsSecureString)'
Write-Host ' 3. MSIX 빌드 시 BuildMsix.ps1 -CertThumbprint ' + $cert.Thumbprint
Write-Host ''
Write-Host "PFX는 비밀이므로 절대 git에 커밋하지 마세요. (.gitignore에 *.pfx 추가됨)"

View file

@ -0,0 +1,56 @@
#Requires -Version 5.1
# packaging/Assets/ 의 placeholder 아이콘들을 생성한다.
# 추후 진짜 로고로 교체.
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
$assetsDir = Join-Path $PSScriptRoot 'Assets'
if (-not (Test-Path $assetsDir)) { New-Item -ItemType Directory -Path $assetsDir | Out-Null }
function New-LogoPng {
param(
[int]$Width,
[int]$Height,
[string]$Path,
[string]$Label = ''
)
$bmp = New-Object System.Drawing.Bitmap($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::ClearTypeGridFit
$rect = New-Object System.Drawing.Rectangle(0, 0, $Width, $Height)
$brush = New-Object System.Drawing.Drawing2D.LinearGradientBrush(
$rect,
[System.Drawing.Color]::FromArgb(0xFF, 0x3B, 0x82, 0xF6),
[System.Drawing.Color]::FromArgb(0xFF, 0x1E, 0x40, 0xAF),
[System.Drawing.Drawing2D.LinearGradientMode]::Diagonal)
$g.FillRectangle($brush, $rect)
if ($Label) {
$fontSize = [Math]::Max(8, [Math]::Min($Width, $Height) / 5)
$font = New-Object System.Drawing.Font('Segoe UI', $fontSize, [System.Drawing.FontStyle]::Bold)
$textBrush = [System.Drawing.Brushes]::White
$sf = New-Object System.Drawing.StringFormat
$sf.Alignment = [System.Drawing.StringAlignment]::Center
$sf.LineAlignment = [System.Drawing.StringAlignment]::Center
$rectF = New-Object System.Drawing.RectangleF(0, 0, [float]$Width, [float]$Height)
$g.DrawString($Label, $font, $textBrush, $rectF, $sf)
$font.Dispose()
$sf.Dispose()
}
$g.Dispose()
$bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
$brush.Dispose()
Write-Host " [+] $Path ($Width x $Height)"
}
Write-Host 'Generating placeholder logos…'
New-LogoPng -Width 50 -Height 50 -Path (Join-Path $assetsDir 'StoreLogo.png') -Label 'E2J'
New-LogoPng -Width 44 -Height 44 -Path (Join-Path $assetsDir 'Square44x44Logo.png') -Label 'E2J'
New-LogoPng -Width 150 -Height 150 -Path (Join-Path $assetsDir 'Square150x150Logo.png')-Label 'E2J'
New-LogoPng -Width 310 -Height 150 -Path (Join-Path $assetsDir 'Wide310x150Logo.png') -Label 'EverythingToJpeg'
Write-Host 'Done.'

View file

@ -0,0 +1,41 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
# 5대 PC에서 MSIX 사이드로드 설치 — 1회 셋업 스크립트.
# 사용법:
# PowerShell (관리자) > .\Install-EverythingToJpeg.ps1 -PfxPath .\EverythingToJpeg-DevCert.pfx -MsixPath .\EverythingToJpeg.msix
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string]$PfxPath,
[Parameter(Mandatory)] [string]$MsixPath,
[securestring]$PfxPassword
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path $PfxPath)) { throw "PFX 파일을 찾을 수 없습니다: $PfxPath" }
if (-not (Test-Path $MsixPath)) { throw "MSIX 파일을 찾을 수 없습니다: $MsixPath" }
if (-not $PfxPassword) {
$PfxPassword = Read-Host -AsSecureString -Prompt 'PFX 비밀번호'
}
Write-Host '[1/3] 인증서를 LocalMachine\TrustedPeople에 임포트…'
$importResult = Import-PfxCertificate `
-CertStoreLocation 'Cert:\LocalMachine\TrustedPeople' `
-FilePath $PfxPath `
-Password $PfxPassword
Write-Host " Thumbprint: $($importResult.Thumbprint)"
Write-Host '[2/3] 인증서를 LocalMachine\Root에도 임포트 (체인 신뢰)…'
Import-PfxCertificate `
-CertStoreLocation 'Cert:\LocalMachine\Root' `
-FilePath $PfxPath `
-Password $PfxPassword | Out-Null
Write-Host '[3/3] MSIX 패키지 설치…'
Add-AppxPackage -Path $MsixPath -ForceApplicationShutdown
Write-Host ''
Write-Host '✅ 설치 완료. Win11 메인 우클릭 메뉴에 "JPEG로 빠른 변환" / "JPEG로 변환…" 항목이 보일 겁니다.'
Write-Host ' (탐색기 재시작이 필요할 수 있음: 작업 관리자 → "Windows 탐색기" 다시 시작)'

View file

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
IgnorableNamespaces="uap rescap desktop desktop4 desktop5 com">
<Identity
Name="EverythingToJpeg.YunChan"
Publisher="CN=EverythingToJpegDev"
Version="1.0.0.0"
ProcessorArchitecture="x64" />
<Properties>
<DisplayName>EverythingToJpeg</DisplayName>
<PublisherDisplayName>YunChan</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Resources>
<Resource Language="ko-KR" />
<Resource Language="en-US" />
</Resources>
<Applications>
<Application Id="EverythingToJpeg"
Executable="EverythingToJpeg.exe"
EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="EverythingToJpeg"
Description="모든 파일을 JPEG로 변환"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
</uap:VisualElements>
<Extensions>
<!-- COM Surrogate Server: hosts the two IExplorerCommand handlers -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="EverythingToJpeg Shell Extension">
<com:Class Id="801B2DD3-632C-4731-9510-AEAE09345264"
Path="EverythingToJpeg.Shell.dll"
ThreadingModel="STA" />
<com:Class Id="CEBA1DB7-9175-4DF6-A362-490DEA49B598"
Path="EverythingToJpeg.Shell.dll"
ThreadingModel="STA" />
</com:SurrogateServer>
</com:ComServer>
</com:Extension>
<!-- File Explorer context menu registrations -->
<desktop4:Extension Category="windows.fileExplorerContextMenus">
<desktop4:FileExplorerContextMenus>
<!-- 이미지 -->
<desktop5:ItemType Type=".png">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".jpg">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".jpeg">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".gif">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".bmp">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".tif">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".tiff">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".webp">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".avif">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".psd">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<!-- HEIC/HEIF -->
<desktop5:ItemType Type=".heic">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".heif">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<!-- RAW -->
<desktop5:ItemType Type=".dng">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".nef">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".cr2">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".cr3">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".arw">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".raf">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".orf">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".rw2">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".srw">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".pef">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<!-- PDF -->
<desktop5:ItemType Type=".pdf">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<!-- DOCX -->
<desktop5:ItemType Type=".docx">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
<desktop5:ItemType Type=".doc">
<desktop5:Verb Id="Quick" Clsid="801B2DD3-632C-4731-9510-AEAE09345264"/>
<desktop5:Verb Id="Dialog" Clsid="CEBA1DB7-9175-4DF6-A362-490DEA49B598"/>
</desktop5:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View file

@ -1,25 +1,87 @@
# Phase 2 — MSIX 패키징 (placeholder)
# Phase 2 — MSIX 패키징
이 폴더는 향후 IExplorerCommand 셸 익스텐션 + MSIX Sparse Package 작업을 위한 자리입니다. 현재 구현되지 않았습니다.
Win11 메인 우클릭 메뉴에 "JPEG로 빠른 변환" / "JPEG로 변환…"을 띄우는 정공법.
## 다음 단계 체크리스트
## 구성
- [ ] `EverythingToJpeg.Shell` C++/WinRT 또는 C#(WinRT projection) 프로젝트 생성 → `IExplorerCommand` 구현
- [ ] `Package.appxmanifest` 작성 — `<uap3:Extension Category="windows.fileExplorerContextMenus">` 사용
- [ ] Windows Application Packaging Project (.wapproj) 생성 — App + Shell DLL 묶기
- [ ] 자체 서명 인증서 생성 스크립트:
```powershell
New-SelfSignedCertificate -Type CodeSigningCert `
-Subject "CN=EverythingToJpegDev" `
-KeyAlgorithm RSA -KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My"
```
- [ ] MakeAppx + SignTool로 MSIX 빌드 + 서명
- [ ] 5대 PC에 인증서를 `Cert:\LocalMachine\TrustedPeople`에 임포트
- [ ] GitHub Actions: 태그 푸시 시 unsigned MSIX 자동 빌드 + Release 첨부
```
packaging/
├── Package.appxmanifest — IExplorerCommand 등록 (com:Class + desktop4:FileExplorerContextMenus)
├── Assets/ — 앱 아이콘 (placeholder, GenerateAssets.ps1로 생성)
├── GenerateAssets.ps1 — placeholder PNG 일괄 생성
├── CreateDevCert.ps1 — 자체 서명 코드사이닝 인증서 생성 + PFX export
├── BuildMsix.ps1 — .NET publish + C++ DLL 빌드 + makeappx + (선택) signtool
└── Install-EverythingToJpeg.ps1 — 5대 PC 1회 설치 스크립트
```
## 참고
C++ Shell DLL은 `src/EverythingToJpeg.Shell/` 에 있고 `BuildMsix.ps1` 안에서 자동 빌드됩니다.
- [PowerToys 컨텍스트 메뉴 개발 문서](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/common/context-menus.md)
- [IExplorerCommand C# 예제](https://github.com/cjee21/IExplorerCommand-Examples)
- [Microsoft: Sparse package 등록](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps)
## 1회: 자체 서명 인증서 만들기
```powershell
cd packaging
.\CreateDevCert.ps1
# Subject 기본값: CN=EverythingToJpegDev (Package.appxmanifest의 Publisher와 일치)
# 비밀번호 입력 → EverythingToJpeg-DevCert.pfx 생성
```
출력된 Thumbprint를 `BuildMsix.ps1 -CertThumbprint <값>` 으로 사용하거나, PFX 파일을 5대 PC에 복사해서 설치 시 사용합니다.
## 빌드
### 미서명 (Phase 1 그대로 사용 가능, 메인 메뉴 노출은 안 됨)
```powershell
.\BuildMsix.ps1
# 산출: packaging/dist/EverythingToJpeg-x64.msix
```
### 서명
```powershell
# 방법 1: PFX 사용
.\BuildMsix.ps1 -Sign -PfxPath .\EverythingToJpeg-DevCert.pfx
# 방법 2: 인증서 저장소의 Thumbprint
.\BuildMsix.ps1 -Sign -CertThumbprint AABBCCDD...
```
## 5대 PC 설치 (관리자 PowerShell)
```powershell
.\Install-EverythingToJpeg.ps1 `
-PfxPath .\EverythingToJpeg-DevCert.pfx `
-MsixPath .\EverythingToJpeg-x64.msix
```
스크립트가 자동으로:
1. PFX를 `LocalMachine\TrustedPeople` 에 임포트
2. PFX를 `LocalMachine\Root` 에도 임포트 (체인 신뢰)
3. `Add-AppxPackage` 로 MSIX 사이드로드
설치 후 PNG/JPG/HEIC/PDF/DOCX 등을 우클릭하면 **메인 메뉴에 직접** "JPEG로 빠른 변환" / "JPEG로 변환…"이 보입니다.
## 미서명 사이드로드 (Phase 2 임시 사용)
자체 서명 만들기조차 귀찮을 때:
```powershell
# 개발자 모드 켜기: 설정 → 개인 정보 및 보안 → 개발자용 → 켜기
Add-AppxPackage -AllowUnsigned -Path .\EverythingToJpeg-x64.msix
```
> Win11 24H2부터 `-AllowUnsigned` 지원. 이전 버전은 자체 서명 권장.
## CI/CD
`.github/workflows/release.yml` — 태그 푸시(`v1.0.0` 등) 시 자동:
1. .NET / MSBuild 셋업
2. `BuildMsix.ps1` 실행 (미서명)
3. GitHub Release 생성 + MSIX 첨부
서명까지 자동화하려면 GitHub Secrets에 `PFX_BASE64`, `PFX_PASSWORD`를 등록하고 워크플로에 단계 추가 (별도 보안 검토 후).
## 트러블슈팅
| 증상 | 원인 / 해결 |
|---|---|
| `Add-AppxPackage`: "신뢰할 수 없는 인증서" | PFX를 `LocalMachine\TrustedPeople`에 임포트했는지 확인 (Install 스크립트 자동 수행) |
| 메뉴가 안 뜸 | 탐색기 재시작: 작업관리자 → "Windows 탐색기" 다시 시작 |
| Publisher 불일치 오류 | `Package.appxmanifest``Publisher=` 와 인증서 `Subject` 가 정확히 일치해야 함 |
| `App identity required` | MSIX 패키지로 설치된 경우에만 IExplorerCommand 작동. portable EXE는 Phase 1 레지스트리 방식 사용 |