param([string]$Source,[string]$Destination,[string]$Expected,[string]$Report) $ErrorActionPreference='Stop' if(Test-Path -LiteralPath $Destination){throw '검증 대상 파일이 이미 있습니다.'} Copy-Item -LiteralPath $Source -Destination $Destination function Invoke-ExcelCall([scriptblock]$Action){for($attempt=0;$attempt -lt 30;$attempt++){try{return & $Action}catch{if($_.Exception.HResult -notin @(-2147418111,-2147417846)){throw};Start-Sleep -Milliseconds 300}};throw 'Excel COM 호출 대기시간 초과'} $excel=$null;$book=$null try{ $excel=New-Object -ComObject Excel.Application Invoke-ExcelCall {$excel.Visible=$false;$excel.DisplayAlerts=$false;$excel.AskToUpdateLinks=$false;$excel.AutomationSecurity=3} $book=Invoke-ExcelCall {$excel.Workbooks.Open($Destination,0,$false)} Invoke-ExcelCall {$excel.CalculateFullRebuild()} $checks=Get-Content -LiteralPath $Expected -Raw -Encoding utf8 | ConvertFrom-Json $errors=@();$count=0 foreach($item in $checks){$sheet=Invoke-ExcelCall {$book.Worksheets.Item($item.sheet)};$v=Invoke-ExcelCall {$sheet.Range($item.cell).Value2};$count++;if($v -isnot [double] -and $v -isnot [int]){$errors+="$($item.sheet)!$($item.cell): $v"}elseif([Math]::Abs([double]$v-$item.value) -gt 1e-8*[Math]::Max(1,[Math]::Abs($item.value))){$errors+="$($item.sheet)!$($item.cell): $v != $($item.value)"};[void][Runtime.InteropServices.Marshal]::ReleaseComObject($sheet)} Invoke-ExcelCall {$book.Save()} @{engine='Microsoft Excel';checked=$count;differences=$errors;worksheets=$book.Worksheets.Count}|ConvertTo-Json -Depth 4|Set-Content -LiteralPath $Report -Encoding utf8 }finally{if($book){try{Invoke-ExcelCall {$book.Close($false)}}catch{};[void][Runtime.InteropServices.Marshal]::ReleaseComObject($book)};if($excel){try{Invoke-ExcelCall {$excel.Quit()}}catch{};[void][Runtime.InteropServices.Marshal]::ReleaseComObject($excel)};[GC]::Collect();[GC]::WaitForPendingFinalizers()}