Files
SheetMe/tools/publish.ps1
T
MsystechandClaude Fable 5 71862c2986 배포 준비 — 자격증명 분리, 산출물 축소, 로깅·전역 예외
[자격증명] 접속 문자열 우선순위를 환경변수 > appsettings.Development.json > appsettings.json >
ServerInfo ini 로 세웠다. 병원 설치본은 설정 없이 [000]bin 의 MSYSTECH_ServerInfo.ini 를
자동으로 찾아 붙는다(레거시와 동일한 MD5 → 3DES-ECB 복호).
- ServerInfoReader 를 [200]SheetMe 에서 복사 이식(vendoring — 두 저장소가 분리돼 있어
  ProjectReference 불가, 레거시 암복호 규약은 변할 이유가 없는 고정 자산). 헤더에 동기화 의무 명시.
- 실측 검증: ini 566라인/병원 엔트리 76개, CurrentServer=029.BSGH 복호 후 실제 접속 성공.
- SaveMode 기본값을 File 로 되돌렸다. ini 의 CurrentServer 가 운영 병원 DB 를 가리키므로
  DB 쓰기는 명시적으로 켠 단말에서만 활성화되어야 한다.
- ConfigService 로 단일 소유화 — ConfigLoader.Load() 11회 호출이 ini 파싱 + 3DES 복호를
  매번 반복하던 것을 1회로. 죽어 있던 Designer:GridSize/SnapThreshold 를 SnapEngine 에 배선하고,
  쓰이지 않던 His:Provider 와 DefaultPaperWidth/Height 는 제거했다(후자는 설정이 아니라
  레거시 패리티 상수다).

[산출물] 8.6MB/34파일 → 7.2MB/26파일.
- EmrDataContext 제거 — M.Framework.DBAccess/TableFramework/M.MW.Data.EMR 의 유일한 소비처였는데
  그 클래스가 어디서도 인스턴스화되지 않았다. DB 접근은 전부 raw Oracle 클라이언트를 쓴다.
- Microsoft.Web.WebView2 는 ExcludeAssets="runtime" — M.Framework.WPF 전이 의존일 뿐
  소스 참조 0건. [000]bin 과의 이름 충돌 3건도 함께 사라진다.
- Production.pubxml(win-x64, FDD, SatelliteResourceLanguages=ko). RID 를 csproj 가 아니라
  pubxml 에 둔 이유는 csproj 에 넣으면 dotnet build/test 까지 RID 별 복원을 타기 때문이다.
- tools/publish.ps1 — 비밀값 하드 게이트 + [000]bin 충돌 경고 + SHA256 매니페스트 + zip.
  게이트는 역방향으로 검증했다(appsettings.json 에 실접속 정보를 넣고 실행 → 정상 차단).
- nuget.config 신설 — 사내 피드가 개발자 개인 OneDrive 경로라 다른 머신에서 복원이 불가능했다.
  %MSYS_NUGET_FEED% 환경변수로 받게 해 최소한 실패 원인이 드러나게 했다.

[배포 규약] docs/DEPLOYMENT.md.
[000]bin 최상위 평면 복사를 금지한다 — 실측 결과 Oracle.ManagedDataAccess.dll 이 겹치고
(신규 .NET Core 5,434KB ↔ 기존 .NET FW 4,602KB), 덮으면 그 폴더의 레거시 EXE 187개가
전부 Oracle 접속 불능이 된다. [000]bin\SheetMe\ 하위 폴더에 둔다 — Information/Log/
OCR서식생성기/SpreadDesign 등 기존 앱들과 같은 방식이다.
FDD 로 배포한다: [000]bin\OCR서식생성기 가 이미 net10.0 + WindowsDesktop.App 10.0.0 을
요구하며 운영 중이라 런타임 존재가 확인된다. 없는 단말이 나오면 -SelfContained 한 번이면 된다.

[로깅] AppLog — LogManager 배선. 모든 호출을 try/catch 로 감싸 로깅 실패가 업무를 막지 않게 했다.
- Redact 필수 — 접속 문자열을 값으로 들고 다니므로 예외 메시지에 자격증명이 섞일 수 있다.
  기록 직전 1회 통과시킨다.
- LogLevel 은 열거형이 아니라 문자열 속성이라 오타를 컴파일러가 못 잡고, 잘못된 값이면
  FIXED 만 남고 나머지가 조용히 사라진다. LogType 열거값의 이름으로만 지정하게 했다.
- 문서에 있는 HandleShutdown 은 6.0.0 DLL 에 실제로는 없어(XML 문서가 앞서 있음) 쓰지 않는다.
  대신 기록이 비동기 배치라 스모크에서 짧게 폴링해 확인한다.
- 로그 경로는 실행 폴더\logs\Designer, 쓰기 불가 시 %LocalAppData% 폴백(쓰기 프로브까지 확인).

[전역 예외] Dispatcher/AppDomain/TaskScheduler 3종을 진단 분기보다 앞에 등록했다.
UI 예외는 기록 후 계속 진행한다(편집 중 문서를 예외 하나로 잃지 않게) — 단 10초 내 5회면
무한 팝업 루프이므로 강제 종료한다.
DialogService.ShowError 도입 — 우리가 던진 안내성 예외는 메시지를 그대로 보여주고, 그 외는
일반화 문구 + 오류 코드만 노출한다(코드가 로그 줄머리와 같아 전화 한 통으로 특정된다).
ex.ToString() 전문을 그대로 띄우던 2곳을 정리했다.
로그인/권한거부/DB저장은 감사 이벤트(FIXED)로 남긴다.

검증: 테스트 70/70, edit-smoke 실패 0(마스킹 5건 + 로그 배선 1건 추가), 왕복 1,271건
diff 0/예외 0, db-save-smoke 제자리 갱신 통과. publish.ps1 정방향/역방향 모두 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 19:23:27 +09:00

100 lines
4.3 KiB
PowerShell

#requires -Version 5.1
<#
.SYNOPSIS
병원 배포용 산출물 생성 — 비밀값 하드 게이트 + SHA256 매니페스트 + zip.
.DESCRIPTION
산출물에 실접속 정보가 섞이면 60여 개 병원 배포본에 자격증명이 평문으로 나간다.
.gitignore 는 커밋만 막을 뿐 퍼블리시 산출물은 못 막으므로, 여기서 실패시켜 차단한다.
.EXAMPLE
pwsh tools/publish.ps1
pwsh tools/publish.ps1 -SelfContained # .NET 10 Desktop 런타임이 없는 단말용
#>
[CmdletBinding()]
param(
[string]$OutputRoot = "$PSScriptRoot\..\publish",
[switch]$SelfContained
)
$ErrorActionPreference = 'Stop'
$repo = Resolve-Path "$PSScriptRoot\.."
$project = Join-Path $repo 'src\SheetMe.Designer\SheetMe.Designer.csproj'
$stage = Join-Path $OutputRoot 'SheetMe'
Write-Host "== SheetMe 배포 산출물 생성 ==" -ForegroundColor Cyan
if (Test-Path $stage) { Remove-Item -Recurse -Force -LiteralPath $stage }
New-Item -ItemType Directory -Force -Path $stage | Out-Null
$args = @(
'publish', $project,
'-c', 'Release',
'-p:PublishProfile=Production',
'-o', $stage,
'--nologo'
)
if ($SelfContained) { $args += @('-p:SelfContained=true') }
& dotnet @args
if ($LASTEXITCODE -ne 0) { throw "퍼블리시 실패 (exit $LASTEXITCODE)" }
# ---- 하드 게이트: 비밀값이 산출물에 섞였으면 여기서 중단 ----
Write-Host "`n-- 비밀값 검사 --" -ForegroundColor Cyan
$violations = @()
$devConfig = Join-Path $stage 'appsettings.Development.json'
if (Test-Path $devConfig) { $violations += "appsettings.Development.json 이 산출물에 포함됨" }
Get-ChildItem -LiteralPath $stage -Recurse -File -Include *.json, *.config, *.ini, *.txt |
ForEach-Object {
$text = Get-Content -LiteralPath $_.FullName -Raw -ErrorAction SilentlyContinue
if ($null -eq $text) { return }
# 플레이스홀더(__PASSWORD__)는 통과, 실제 값이 채워진 것만 잡는다
if ($text -match 'Password\s*=\s*(?!__)[^;"\s]{1,}') {
$violations += "$($_.Name): Password= 에 실제 값이 들어 있음"
}
if ($text -match 'User\s*Id\s*=\s*(?!__)[^;"\s]{1,}') {
$violations += "$($_.Name): User Id= 에 실제 값이 들어 있음"
}
}
if ($violations.Count -gt 0) {
Write-Host "산출물에 자격증명이 포함되어 있습니다:" -ForegroundColor Red
$violations | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
throw "비밀값 게이트 실패 — 배포를 중단합니다."
}
Write-Host " OK — 자격증명 없음(접속 정보는 [000]bin 의 ServerInfo ini 에서 읽습니다)" -ForegroundColor Green
# ---- 충돌 경고: [000]bin 평면 배치 금지 확인용 ----
$binRoot = 'C:\MsystechHIS_Ver.2\[000]Bin'
if (Test-Path -LiteralPath $binRoot) {
$existing = @{}
Get-ChildItem -LiteralPath $binRoot -File -Filter *.dll | ForEach-Object { $existing[$_.Name] = $true }
$clash = Get-ChildItem -LiteralPath $stage -File -Filter *.dll | Where-Object { $existing.ContainsKey($_.Name) }
if ($clash) {
Write-Host "`n-- 참고: [000]bin 최상위와 이름이 겹치는 DLL $($clash.Count)건 --" -ForegroundColor Yellow
$clash | ForEach-Object { Write-Host " $($_.Name)" -ForegroundColor Yellow }
Write-Host " → 반드시 [000]bin\SheetMe\ 하위 폴더에 배치하세요(평면 복사 금지)." -ForegroundColor Yellow
}
}
# ---- 매니페스트 ----
$manifest = Join-Path $stage 'MANIFEST.sha256'
Get-ChildItem -LiteralPath $stage -Recurse -File |
Sort-Object FullName |
ForEach-Object {
"{0} {1}" -f (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash,
$_.FullName.Substring($stage.Length + 1)
} | Set-Content -LiteralPath $manifest -Encoding utf8
$files = Get-ChildItem -LiteralPath $stage -Recurse -File
Write-Host ("`n산출물: {0}개 파일 / {1:N1} MB" -f $files.Count, (($files | Measure-Object Length -Sum).Sum / 1MB))
# ---- zip ----
$zip = Join-Path $OutputRoot 'SheetMe.zip'
if (Test-Path $zip) { Remove-Item -Force -LiteralPath $zip }
Compress-Archive -Path $stage -DestinationPath $zip
Write-Host "패키지: $zip" -ForegroundColor Green
Write-Host "`n배포 절차는 docs/DEPLOYMENT.md 를 따르세요." -ForegroundColor Cyan