Initial commit: EG-BIM installer plans and design concepts

This commit is contained in:
jychoi4 committed 2026-10-19 09:24:15 +09:00
commit 54f0cfe360
25 files changed
+4251

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
* text=auto
*.md text eol=lf
*.json text eol=lf
*.html text eol=lf
+17
View File
@@ -0,0 +1,17 @@
# Local connection settings and credentials
.mcp.json
.env
.env.*
!.env.example
*.pfx
*.p12
*.key
# Build dependencies and temporary files
node_modules/
target/
__pycache__/
*.pyc
*.log
.DS_Store
Thumbs.db
+16
View File
@@ -0,0 +1,16 @@
# EG-BIM Network Setup
EG-BIM의 Windows 설치기 전환, 네트워크 배포 및 버전별 패치 관리를 위한 계획과 설계 저장소입니다.
## 문서
- [설치 및 배포 계획](docs/plan/README.md)
- [Rust + Tauri 제작 프로그램 설계](docs/code-design/README.md)
- [설치기 디자인 미리보기](output/egbim-installer-preview.html): 파일을 브라우저에서 열어 확인합니다.
- [설치기 디자인 원본](output/egbim-installer-concepts.html)
현재는 계획·설계와 인터랙티브 시안 단계입니다. 시안은 실제 설치나 시스템 변경을 수행하지 않습니다.
## 로컬 설정
Gitea 연결과 인증 정보를 포함하는 `.mcp.json`은 로컬에서 관리하며 Git에 포함하지 않습니다. 인증 토큰과 서명 인증서도 커밋하지 않습니다.
+77
View File
@@ -0,0 +1,77 @@
# Rust + Tauri 구조와 모듈
## 실행 구조
```mermaid
flowchart TD
UI[Tauri 웹 UI] --> IPC[타입이 지정된 Tauri Command]
IPC --> APP[Rust 작업 서비스]
CLI[CI용 CLI] --> APP
APP --> DOMAIN[프로젝트 검증 · 파일 해석 · 버전/패치 정책]
APP --> ADAPTER[도구와 저장소 어댑터]
ADAPTER --> WIX[WiX · Burn]
ADAPTER --> SIGN[SignTool · 서명 공급자]
ADAPTER --> STORE[작업 디렉터리 · 불변 릴리스 저장소]
APP --> REPORT[진행 이벤트 · 검증 보고서]
```
화면은 설정 편집과 결과 표시만 담당한다. 파일 수집, GUID 할당, 패치 판정, 외부 프로세스 실행, 서명, 최종 상태 변경은 모두 Rust에서 처리한다. CLI도 같은 서비스를 호출하므로 화면 없이 CI에서 동일한 검증을 수행한다.
## 제안 저장소 구조
```text
Cargo.toml # Cargo workspace
Cargo.lock
rust-toolchain.toml
apps/
desktop/
src/ # TypeScript + React 화면
src-tauri/ # Tauri Command, 이벤트, capabilities
cli/ # package-builder 명령
crates/
package-domain/ # 타입과 순수 검증/비교 규칙
package-engine/ # 작업 흐름, 취소, 상태, 포트 정의
package-infra/ # 파일 I/O, WiX, 서명, Windows 프로세스
templates/wix/ # 버전 관리되는 설치 정의 템플릿
schemas/ # 프로젝트/매니페스트/IPC JSON Schema
tests/fixtures/ # 작은 가상 제품과 기준 MSI 정의
docs/
```
의존 방향은 UI/CLI → engine → domain이다. infra는 engine이 정의한 포트를 구현하고 실행 시작점에서 주입한다. domain은 Tauri·네트워크·프로세스에 의존하지 않는다. Rust 공통 의존성은 workspace에서 관리하고 `Cargo.lock`을 저장한다. [Cargo workspaces](https://doc.rust-lang.org/cargo/reference/workspaces.html)
## 핵심 서비스와 포트
| 구성 | 책임 |
| --- | --- |
| ProjectService | 열기·저장·스키마 마이그레이션·편집 revision 관리 |
| ManifestResolver | 파일 규칙을 정렬된 실제 파일 목록으로 확장 |
| IdentityPlanner | 기존 identity lock을 기준으로 컴포넌트와 제품 식별자 계획 |
| ReleasePlanner / PatchPlanner | 실행 전 검사, 차이 보고, 출력과 적용 경로 확정 |
| InstallFlowCompiler | 단계 의존성과 설치 모드 검증, MSI/Burn 실행 정의로 변환 |
| InstallerUiCompiler | UI 템플릿·리소스·바인딩 검증, 백엔드 기능 지원 검사 |
| BuildCoordinator | 작업 잠금, 단계 실행, 체크포인트, 취소, 실패 기록 |
| WixBackend | 설치 정의 생성, MSI/MSP/Burn 빌드, 결과 메타데이터 추출 |
| Signer | 서명 정책 적용, 타임스탬프, 실제 서명 검증 결과 반환 |
| ArtifactRepository | 기준 릴리스 조회, 콘텐츠 검증, 불변 결과 등록 |
| ProcessRunner | 허용 도구 실행, 로그 스트리밍, 시간 제한, 자식 종료 |
포트는 외부 도구를 가짜 구현으로 바꿀 수 있게 한다. 실제 Rust trait의 비동기 형태는 구현 시 확정하되, 모든 작업은 `BuildContext`, 취소 토큰, 타입이 지정된 요청과 결과를 받는다. 도구 종료 코드만 성공 기준으로 삼지 않는다.
후보 라이브러리는 `serde/serde_json`, `uuid`, `sha2`, `quick-xml`, `tokio`, `clap`, `tracing`이다. 도입 시 유지보수 상태·라이선스·Windows 지원을 확인하고 버전을 고정한다. WiX CLI와 확장, Windows SDK SignTool도 정확한 버전·파일 해시를 `toolchain.lock.json`에 기록한다. WiX 세대가 다른 명령 구문을 혼용하지 않는다.
## 작업 수명과 장애 복구
생성 작업은 UI 스레드 밖에서 실행한다. 최초 버전은 한 프로세스에서 빌드 한 건씩 수행하며, CLI와 GUI 간에도 프로젝트/출력 저장소에 OS 파일 잠금을 사용한다. 프로젝트 편집은 가능하지만 실행 중인 작업은 시작 시점 스냅샷을 사용한다.
작업 기록은 `job.json`, 단계별 구조화 로그와 체크포인트 파일로 저장한다. 임시 파일 작성 후 동일 볼륨 내 교체로 저장하며 프로세스 종료 후 다시 읽어 마지막 단계를 복원한다. 기존 작업을 무조건 이어서 실행하지 않고 입력·도구·서명 완료 파일의 해시를 재검사한다. 확정 결과를 변경하는 재개는 금지한다.
GUI 종료 시 실행 중 작업을 안내하고 취소를 요청할 수 있다. 프로세스 중단 후의 작업은 `Interrupted`로 복구한다. 최초 버전은 GUI를 닫아도 계속 도는 서비스가 아니다. 무인 장시간 실행은 CLI를 사용한다.
## 권한 경계
생성기는 일반 사용자 권한으로 동작한다. 대상 HKLM 레지스트리를 편집하는 것은 생성된 설치기의 역할이다. 빌드 중 사용자 PC에 MSI/MSP를 설치하지 않는다.
웹 UI에는 임의 셸 실행·임의 파일 쓰기 API를 노출하지 않는다. Rust가 허용한 프로젝트와 소스 루트 안에서 요청을 검증한다. 외부 도구는 절대 경로와 인자 배열로 실행하며 `.bat`, `cmd /c`, PowerShell 문자열 조합을 허용하지 않는다. Windows Job Object로 자식 프로세스 수명을 관리하고 실패·취소 시 남은 프로세스를 정리한다. [Rust Command](https://doc.rust-lang.org/std/process/struct.Command.html)
Tauri의 로컬 번들 UI만 사용하고 원격 페이지에 IPC 권한을 주지 않는다. 사용자 입력·로그는 HTML로 렌더링하지 않으며 CSP와 capabilities를 제한한다. 사용자 정의 Command는 등록만 하면 기본적으로 모든 앱 창에서 호출할 수 있으므로 `AppManifest::commands`와 명시적 권한을 함께 구성한다. capability가 Rust 내부의 경로 검증을 대신하지 않는다. [Tauri capabilities](https://v2.tauri.app/security/capabilities/)
+58
View File
@@ -0,0 +1,58 @@
# 프로젝트와 데이터 모델
## 설정과 결과를 분리하는 파일 구조
```text
project/
package-project.json # 사람이 편집하는 제품/패키지 설정
package-project.local.json # PC별 소스 경로, 도구/서명 프로필 참조
identity.lock.json # 제품 계열 및 컴포넌트 식별자 이력
toolchain.lock.json # 승인한 도구/템플릿 버전
.work/jobs/<jobId>/ # 스냅샷, 스테이징, 생성 WXS, 로그
releases/<releaseId>/
release.json # 확정된 메타데이터
input.snapshot.json # 설정과 도구, 소스 정보
files.resolved.json # 실제 파일 목록, 원본/서명 후 해시
identity.snapshot.json
artifacts/ # 배포 가능한 파일
baseline/ # MSI, wixpdb, signed payload, 설치 정의
reports/ # 정적 검사와 빌드 당시 검증
patches/<patchId>/ # 기준→목표 패치와 동일 종류의 기록
attestations/<artifactDigest>/ # 이후 VM 시험 결과; 아티팩트는 수정 안 함
```
프로젝트·identity lock·toolchain lock은 형상 관리한다. local 파일, 자격 증명, 대용량 릴리스와 작업 폴더는 제외한다. 릴리스 저장소는 백업 가능한 별도 위치를 지정할 수 있다. 인증서 개인 키나 토큰은 어떤 JSON에도 넣지 않고 OS/CI 자격 증명 저장소의 참조만 사용한다.
## 주요 데이터
| 타입 | 핵심 필드와 규칙 |
| --- | --- |
| PackageProject | schemaVersion, projectId, product, sources, files, registry, outputs, prerequisites, signing, patchPolicy, installationFlow, installerUi |
| InstallationFlow / InstallerUi | 단계 의존성·모드·조건, 테마/페이지/바인딩. [상세 계약](09-install-flow-and-ui.md) 참조 |
| ProductSpec | productId, displayName, manufacturer, architecture, language, installScope, version, engineVersion, upgradeCode, installRoot |
| FileRule | 고유 id, source alias, include/exclude 패턴, destinationDirectory, required |
| ResolvedFile | source-relative path, target-relative path, 크기, SHA-256, PE 정보, componentId/Guid, signingPolicy |
| RegistryEntry | id, hive, view, key, name, typed value, component, 설치/제거 정책 |
| IdentityLock | 제품 계열, ProductCode, 컴포넌트 GUID/키 경로, 삭제된 식별자 tombstone |
| ReleaseRecord | releaseId, buildId, 설정 해시, 소스 커밋(선택), 실제 MSI/Burn 식별자, artifacts, 호환성, 서명/도구 정보 |
| PatchRecord | patchId/PatchCode, 기준 상태, 목표 releaseId, family/sequence, 필요한 원본·패치, 전체 대체 경로 |
| Artifact | artifactId, kind, 상대 경로, u64 크기, 최종 SHA-256, dependsOn, 서명 결과 |
| ValidationReport | reportId, 대상 해시, 검사 항목, pass/fail/notRun, 환경, 도구 버전 |
시각은 UTC로 저장하고 화면에서 현지 시간으로 표시한다. 파일 크기는 Rust `u64`; JavaScript 정밀도 손실을 막기 위해 IPC의 바이트 수는 십진 문자열로 표현한다. 경로는 Rust `PathBuf`로 다루고 프로젝트 이식 경로는 `/`로 정규화한다. 직렬화 불가능한 파일명은 손실 변환하지 않고 오류로 표시한다.
## 프로젝트 예제와 검증
[egbim.package-project.json](examples/egbim.package-project.json)은 필드 형식을 보여 주는 **초안**이다. 제조사, 실제 스테이징 경로, 필수 런타임은 확인되지 않았으며 예제만으로 정식 패키지를 생성할 수 없다. `sources.alias`는 local 설정에서 실제 폴더로 연결한다. 레지스트리 예제도 실제 제품에서 요구하는 설정으로 확인해야 한다.
`schemaVersion`은 앱 버전과 분리한다. 알 수 없는 상위 스키마 버전과 알 수 없는 필드는 거부한다. 마이그레이션은 원본 백업과 변경 보고서를 만든 후 새 파일로 저장한다. JSON Schema와 Rust 검증이 일치하는지 계약 테스트를 수행한다.
버전·GUID 문자열 형식, 참조 id 존재 여부, 중복 대상 경로, 필수 파일 누락, hive와 설치 범위의 모순은 Rust에서 검사한다. 출력 경로는 소스 루트 밖이어야 한다. 검증 결과는 `severity`, `code`, `fieldPath`, `message`, `remediation`으로 반환하여 화면에서 해당 설정으로 이동하게 한다.
`release.json`은 고정된 프로젝트 설정을 복사하는 것으로 끝내지 않는다. 빌드된 MSI·MSP·EXE에서 읽은 실제 제품 코드·버전과 파일 정보를 기록하고 기대값과 대조한다. 메타데이터 자신의 해시를 자기 안에 넣지 않는다. 메타데이터 서명을 사용할 경우 별도 서명 파일을 생성한다.
## 편집·빌드 동시성
프로젝트 저장은 `expectedRevision`을 받아 변경 충돌을 검사한다. 빌드 계획에는 프로젝트 스냅샷 해시, 기준 릴리스 해시, 도구 해시와 identity lock revision을 포함한다. 계획 후 입력이 바뀌면 이전 계획을 실행하지 않고 다시 계산한다.
`releaseId`와 `buildId`는 각각 불변 릴리스와 작업 시도를 구분한다. 실패한 빌드는 이력에 남지만 배포 가능한 릴리스가 아니다. 입력이 같아도 타임스탬프 서명 때문에 재빌드 결과가 바이트 단위로 같을 것이라고 보장하지 않는다. 정확한 재배포는 보존한 최종 아티팩트를 사용한다.
+43
View File
@@ -0,0 +1,43 @@
# 파일 목록, 설치 위치와 레지스트리
## 파일 목록 편집과 해석
파일/폴더 추가, 상대 경로 유지, 포함/제외 패턴, 필수 파일 지정, 대상 폴더 변경을 지원한다. 화면에는 최종 배포 경로, 크기, PE 여부, 서명 상태, 직전 릴리스 대비 추가·수정·삭제를 표시한다. 패턴만 저장하되 빌드마다 확장한 실제 목록도 보존한다.
규칙의 처리 순서는 소스 alias 해석 → include 합집합 → exclude 제거 → 필수 파일 검사 → 대상 경로 계산 → 충돌 검사다. 같은 대상에 둘 이상 매핑되면 뒤의 규칙으로 덮어쓰지 않고 실패한다. 결과는 정규화된 대상 경로 기준으로 정렬한다.
Windows 대소문자 충돌, `..`, 절대 대상 경로, 예약 이름, ADS, 끝의 공백/점과 지원하지 않는 경로 길이를 검사한다. junction/symlink 등 reparse point는 최초 버전에서 거부한다. 출력·작업 폴더가 입력에 포함되는 순환 수집도 차단한다. 원본은 읽기만 하고 실제 파일 복사본을 스테이징한다. 하드링크로 복사하여 원본을 함께 서명하는 방식은 금지한다.
배포 범위는 실제 빌드 결과와 필요한 리소스로 한정한다. PDB, 소스, 테스트, 로그, 비밀 설정 제외는 프로젝트에서 명시하며, 이름이 `.config`라는 이유로 런타임 필수 파일까지 제외하지 않는다. 최초 입력 루트는 `D:\egbim\repo\14.1` 전체 대신 검증된 배포 스테이징 폴더로 설정한다.
## 설치 위치
| 디렉터리 id | 기본 의미 |
| --- | --- |
| INSTALLROOT | Windows의 64비트 Program Files 아래 `Baron` |
| APPDIR | INSTALLROOT 아래 `EG-BIM` |
| COMMONDATADIR | ProgramData 아래 제품 공용 데이터; 필요한 경우만 정의 |
드라이브 문자 `C:`는 하드코딩하지 않는다. 기존 설치가 있으면 탐지한 경로를 우선하며 신규 설치는 기본 경로 또는 검증된 사용자 지정 경로를 사용한다. 경로 설정은 WiX 디렉터리와 MSI 속성으로 변환한다. `ProgramFiles64Folder`와 설치 범위는 x64/perMachine에 맞춘다. [WiX Directory](https://docs.firegiant.com/wix/schema/wxs/directory/)
앱이 수정하는 CUI·도면·사용자 설정을 Program Files의 공급자 파일과 섞지 않는다. 공용/사용자 데이터로 옮기는 작업은 제품 변경과 마이그레이션 설계가 선행되어야 한다. 생성기가 파일 소유권을 추정해 자동 이동·삭제하지 않는다.
## MSI 컴포넌트 규칙
기본적으로 파일 하나에 컴포넌트 하나를 할당하고 해당 파일을 key path로 지정한다. 레지스트리 전용 컴포넌트도 별도로 만든다. 컴포넌트의 안정적인 식별 기준은 대상 경로·아키텍처·설치 범위·리소스 종류이며 소스 절대 경로·파일 해시·정렬 순번을 GUID 생성 기준으로 쓰지 않는다.
처음 할당한 WiX id와 Component GUID는 `identity.lock.json`에 보존한다. 경로/키 경로/비트수/소유권이 바뀌면 기존 GUID를 기계적으로 재사용하지 않고 호환성 검토 대상으로 분류한다. 삭제된 식별자는 tombstone으로 남겨 무관한 리소스에 재사용하지 않는다. 동일 프로젝트 동시 빌드가 서로 다른 GUID를 확정하지 못하도록 잠근다.
기존 ISM의 컴포넌트 GUID를 그대로 가져온다고 패치 호환성이 확보되지는 않는다. 최초 전환은 전체 업그레이드로 검증하고 이후 WiX 기준 릴리스부터 패치 계열을 관리한다.
## 레지스트리 편집
입력 필드는 `hive`, 32/64비트 `view`, `key`, 값 이름(기본값 포함), 타입, 값, 설치 조건, 제거 정책이다. 초기 지원 타입은 string, expandableString, integer, binary, multiString이며 DWORD 범위·바이너리 형식·문자열 배열을 타입별로 검증한다. 지원하지 않는 타입은 자동 문자열 변환하지 않는다. [WiX RegistryValue](https://docs.firegiant.com/wix/schema/wxs/registryvalue/)
문자열에는 `literal`과 `msiFormatted`를 구분한다. literal의 대괄호 등은 MSI 형식으로 오인되지 않게 변환하고, formatted는 `[APPDIR]` 같은 허용 속성만 참조한다. XML은 라이브러리로 직렬화하여 사용자 값을 원시 WXS에 삽입하지 않는다.
perMachine 기본은 HKLM이다. 파일 연결/COM 등록은 HKCR의 병합 뷰를 무작정 수정하지 않고 실제 소유 hive와 뷰를 명시한다. HKCU는 관리자 권한 설치를 실행한 계정과 실제 앱 사용자가 다를 수 있으므로 최초 버전의 perMachine 프로젝트에서는 사용자별 기본값 초기화를 앱 첫 실행 작업으로 분리한다. HKCU 설치 기능은 별도 구현·시험 없이는 차단한다.
제거 시 기본 정책은 생성한 값만 제거하는 것이다. 공유 키 전체나 `Software\Baron` 전체 삭제는 허용하지 않는다. 사용자 수정 값과 기존 값 보존 정책은 MSI repair/upgrade 동작까지 검증한 템플릿만 제공하며 UI 체크박스 하나로 보장한다고 표시하지 않는다.
설정 미리보기는 빌드 PC 레지스트리를 바꾸지 않는다. `.reg` 파일이나 ISM을 가져오는 기능은 향후 파서로 변환·검토하고 실행하지 않는다. 설치 과정 편집기는 표준 기능과 등록된 핸들러의 추가·삭제·조건 설정을 지원한다. 임의 셸 스크립트 입력은 최초 범위에서 제외하고, COM/서비스/CustomAction에는 소유권·롤백·권한 계약을 요구한다. [설치 과정과 UI 설계](09-install-flow-and-ui.md)
@@ -0,0 +1,46 @@
# 버전과 GUID 관리
## 서로 다른 버전의 구분
| 값 | 목적 |
| --- | --- |
| productVersion | MSI의 3자리 버전. 예: `1.14.0` |
| displayVersion | 사용자 표시용 버전. 사전 배포 표기 허용 |
| engineVersion | 제품이 사용하는 엔진 버전. 현재 대상 `14.1` |
| PE FileVersion / ProductVersion | 실제 실행 파일에 들어 있는 버전 |
| releaseId / buildId | 릴리스/빌드 시도의 고유 식별자 |
| project schemaVersion | 생성기 설정 파일 형식의 버전 |
MSI ProductVersion은 처음 세 필드만 비교하며 최대값은 각각 255, 255, 65535다. 네 번째 필드만 바꾼 릴리스를 새로운 MSI 업데이트로 처리하지 않는다. [Microsoft ProductVersion](https://learn.microsoft.com/en-us/windows/win32/msi/productversion)
앱 버전과 엔진 `14.1`을 동일시하지 않는다. 기존 `.ism`의 `1.13.1.0`도 MSI 비교 관점은 `1.13.1`이다. 기존 업데이터는 실제 Grimi.exe 버전을 참조하므로 생성 전에 목표 릴리스와 EXE 버전의 매핑을 검사한다. 이미 빌드한 PE의 버전을 생성기가 임의 수정하지 않는다. 불일치는 제품 재빌드 또는 명시적으로 승인된 버전 매핑으로 해결한다.
## 식별자 수명
| 식별자 | 정책 |
| --- | --- |
| productId | 업데이트 서비스 제품 키. 기존 `E901`; MSI GUID와 별개 |
| UpgradeCode | 동일 제품 업그레이드 계열에서 유지 |
| ProductCode | 최초/major upgrade 시 신규; minor upgrade 패치 계열에서는 유지 |
| PackageCode | 내용이 바뀐 MSI마다 신규; 같은 확정 파일 재배포는 유지 |
| Component GUID | 같은 컴포넌트 규칙을 지키는 동안 유지 |
| PatchCode | 새 MSP마다 신규; 동일 파일 재배포는 유지 |
| Patch family / sequence | 같은 제품 패치 순서와 대체 관계를 명시적으로 관리 |
| Bundle UpgradeCode | 번들 업그레이드 계열에서 유지; MSI UpgradeCode와 별개 |
| Bundle id / version | 실제 Burn 출력에서 추출하고 탐지·등록 정책에 맞게 관리 |
Major/Minor는 앱 버전의 첫째·둘째 숫자를 의미하지 않고 Windows Installer의 업데이트 종류다. 변경된 MSI는 새 PackageCode를 가져야 하며 ProductCode 유지 여부는 업데이트 종류에 따른다. [Windows Installer 업데이트 구분](https://learn.microsoft.com/en-us/windows/win32/msi/patching-and-upgrades)
## EG-BIM 초기 전환
기존 ISM에서 확인한 UpgradeCode는 `{E6381427-A8BC-4506-A428-CE860BDA119F}`, ProductCode는 `{FBEE188C-7D40-4CBD-80DA-89919B116F57}`이다. 이것은 확인한 파일의 값이며 모든 실제 설치본이 동일하다는 보장은 없다. 출시된 설치 파일들의 식별자·언어·아키텍처를 수집해 전환 목록을 확정한다.
초기 설계는 기존 UpgradeCode 유지와 새 ProductCode를 사용하는 전체 전환이다. 기존 제거 CustomAction이 데이터까지 지울 수 있으므로 GUID 연계만으로 전환을 허용하지 않는다. 백업 위치, 실제 삭제 범위, 제거 순서와 복원 시험이 통과해야 전환 경로를 활성화한다. [기존 전환 계획](../plan/02-installer-migration.md)
## 편집과 자동 할당
화면에서 표시명·버전·UpgradeCode를 설정할 수 있지만, 첫 정식 릴리스 이후 계열 식별자는 잠근다. 새 제품/계열 생성은 기존 프로젝트를 덮어쓰지 않고 명시적인 새 계열 작업으로 처리한다. ProductCode/PackageCode/PatchCode는 엔진이 계획 단계에서 할당하고 화면에는 변경 이유를 보여 준다.
정식 빌드의 GUID를 매번 무작위로 모두 재생성하지 않는다. 실행 전에 identity lock revision과 계획된 값을 예약한다. 실패한 작업의 예약 이력은 남기고, 재개 시 동일 바이트 산출물은 재사용한다. 내용을 바꿔 다시 만드는 MSI/MSP는 새 패키지 식별자를 할당한다.
동일 버전으로 서로 다른 파일을 정식 배포하는 것은 기본 금지다. 시험 빌드는 별도 draft buildId로 구분하되 배포 등록을 허용하지 않는다. 사용자 그룹은 버전/GUID를 바꾸는 이유가 아니다. 개발자 테스트를 통과한 동일 파일을 다음 그룹으로 승격한다.
+68
View File
@@ -0,0 +1,68 @@
# 전체 릴리스 생성 파이프라인
## 작업 단계와 상태
```text
Draft → Planned → Running → Built → ArtifactVerified → Finalized
└→ Failed / Cancelled / Interrupted
Finalized + 별도 설치 시험 증빙 → DeploymentReady
```
`Finalized`는 아티팩트가 변경 불가능하게 보관됐다는 뜻이다. 설치 시험과 배포 등록까지 성공한 상태는 아니다. `DeploymentReady` 판정은 후속 검증 증빙을 참조하며 기존 릴리스 JSON이나 바이너리를 고치지 않는다. 시험 전 결과는 패치 후보 비교에 사용할 수 있지만 정식 배포 기준으로 승격할 수 없다.
| 단계 | 처리와 실패 조건 |
| --- | --- |
| 사전 검사 | 프로젝트 스키마, 확정 필드, 도구/확장 버전, 서명 접근, 출력 잠금, 디스크 여유 확인 |
| 계획 | 파일 목록과 차이, 설치 기능, GUID, 출력 프로필, 전체 설치·업그레이드 경로 결정 |
| 과정/UI 변환 | 모드별 단계 의존성, 핸들러 계약, UI 백엔드 지원을 검증하고 MSI/Burn/테마 정의로 변환; 미지원 기능은 차단 |
| 스냅샷 | 설정·identity lock·도구 정보 고정, 실제 파일 복사, 크기와 원본 해시 기록 |
| 입력 검증 | 필수 파일, PE 버전/아키텍처, 의존 런타임, 예상한 입력 해시와 복사본 일치 검사 |
| 내부 파일 서명 | 자체 PE 서명과 타임스탬프, 외부 PE 검증, 서명 후 해시 고정 |
| WiX 생성 | 디렉터리·파일·컴포넌트·레지스트리·업그레이드 정의를 결정적 순서로 생성 |
| MSI 생성 | 분할 CAB 포함 출력, 설치 테이블/식별자 검증, 정적 MSI 검증 |
| MSI 서명 | 외부 CAB 서명 및 MSI에 서명 정보 반영이 필요한 순서 처리, 최종 MSI 서명 |
| Burn 생성 | 확정 MSI와 런타임으로 네트워크/오프라인 번들 생성, 엔진·최종 EXE 서명 |
| 산출물 검증 | 도구 결과뿐 아니라 실제 파일의 식별자·서명·크기·의존 관계 검사 |
| 확정 | 모든 최종 해시 계산, 보고서/매니페스트 작성, 동일 볼륨 임시 디렉터리를 불변 경로로 이동 |
입력 원본은 빌드 전에 변경을 멈춘 스테이징 폴더를 사용한다. 복사 중 변경이 감지되면 실패하고 다시 스냅샷을 만든다. 서명 이후 파일 수정이 필요하면 영향받은 단계부터 새 작업으로 실행한다. 진행률은 파일 수·처리 바이트와 단계로 표시하고 외부 도구가 제공하지 않는 정확한 남은 시간을 추정값으로 단정하지 않는다.
디스크 여유는 원본 스테이징, 서명 복사본, 압축 전후 파일, 임시 파일, 기준 버전과 최종 결과를 포함해 계산한다. 정확히 예측할 수 없는 부분은 보수적인 여유량을 적용한다. 수 GB 파일을 메모리에 통째로 적재하지 않고 스트림으로 복사·해시한다.
## 대용량 출력 프로필
| 프로필 | 산출물과 정책 |
| --- | --- |
| offline-layout | Setup.exe + EG-BIM.msi + Data*.cab + prerequisites. 인터넷 없이 설치 가능한 전체 폴더 |
| network | WebSetup.exe + 원격 MSI/CAB/런타임. 모든 payload에 불변 URL·해시·크기 연결 |
| store-singlefile | 필수 요소를 포함한 단일 오프라인 EXE 또는 적합한 MSI. 크기·서명·Store 검증 실패 시 생성 불가 |
총 설치 용량이 2GB를 넘는 경우 외부 CAB를 분할한다. 기본 압축 전 CAB 예산은 **512MiB 제안값**이며 파일별 크기와 실제 생성 CAB를 검사한다. 이것은 Windows Installer의 고정 제한값이 아니다. WiX MediaTemplate의 미디어 분할 설정과 단일 대형 파일 처리 설정은 서로 다르다. 도구 버전별 지원을 검증하여 매핑한다. [WiX MediaTemplate](https://docs.firegiant.com/wix/schema/wxs/mediatemplate/)
전체 용량 분할이 단일 초대형 파일이나 EXE 형식의 제한까지 해결하지는 않는다. 최초 버전은 개별 파일 2GiB 이상을 보수적으로 차단하고 해당 파일의 별도 패키징 설계를 요구한다. 이 기준은 도구가 지원할 수 있는 최대값에 대한 단정이 아니라 초기 지원 범위다. 큰 파일 분할 기능을 추가할 때는 설치·복구·MSP까지 시험한다.
서명할 PE 파일은 4GB 이상에서 서명 문제가 있으므로 단일 EXE에 모든 내용을 무조건 포함하지 않는다. 4GB 미만도 선택한 Burn 버전·컨테이너·압축 방식에서 성공한다는 보장은 없어 실제 빌드와 서명 검증으로 판정한다. [Microsoft 대형 파일 서명 문제](https://learn.microsoft.com/en-us/windows/msix/package/signing-known-issues)
Store의 MSI/EXE 제출은 독립 실행 가능한 오프라인 설치 패키지를 요구한다. 네트워크 stub이나 별도 CAB URL 목록이 단일 제출 파일을 대신한다고 가정하지 않는다. `store-singlefile`이 성립하지 않으면 Store 준비 상태를 실패로 보고하고 MSIX/제품 분할 등 별도 경로를 검토한다. [Store 패키지 요건](https://learn.microsoft.com/en-us/windows/apps/publish/publish-your-app/msi/app-package-requirements)
## 서명 정책
Rust/WiX는 서명 인증서를 발급하지 않는다. `Signer` 어댑터가 SignTool과 사전 구성된 인증서 또는 Azure Artifact Signing 프로필을 호출한다. 프로젝트에는 프로필 id만 저장한다. 인증서/서비스 계약은 별도이며 가격과 SmartScreen 설명은 [서명 계획](../plan/06-code-signing-and-smartscreen.md)을 따른다.
자체 EXE/DLL뿐 아니라 IRX 등 확장자가 다른 PE도 헤더로 식별한다. 정책은 `sign-owned`, `verify-vendor`, `not-applicable`로 구분하고 파일 소유권과 허용 서명자를 명시한다. 타사 서명을 지우거나 모든 파일을 자사 서명으로 덮어쓰지 않는다. 서명이 필요한 외부 파일이 미서명인 경우 명시적인 공급자 조치가 없으면 정식 빌드를 차단한다.
내부 PE 서명 → CAB/MSI의 필요한 서명 정보 반영 및 최종 서명 → Burn 엔진 분리·서명·재결합 → 최종 번들 서명 → 최종 해시 순서를 적용한다. 단계별 정확한 명령은 고정된 WiX 버전 어댑터에 캡슐화한다. MSP도 생성 후 서명한다. [WiX 서명 절차](https://docs.firegiant.com/wix/tools/signing/)
인증서 체인·정책·타임스탬프를 검증하고 서명자, 검증 환경, 검증 시각을 기록한다. 일시적인 서명 서비스 오류는 제한적으로 재시도하되 완료된 파일을 재서명하여 해시를 바꾸지 않는다. 토큰·PIN·개인 키·전체 인증 응답은 로그에 남기지 않는다.
## 전체 설치와 Minor Upgrade
목표 릴리스마다 신규 설치 가능한 전체 MSI를 만든다. 같은 ProductCode를 유지하는 minor upgrade를 기존 설치에 적용하는 것은 단순히 새 MSI를 실행하는 것과 다르므로 별도의 `upgradeRoute`를 제공한다. MVP에서는 해당 기준에 MSP를 적용하는 경로를 우선한다.
MSP가 없는 상태에서 전체 MSI를 대체 업데이트로 사용하려면 해당 버전에 맞는 reinstall/업데이트 동작과 Burn 탐지를 검증한다. 미검증 시 `fallbackArtifactId`만 넣어 실행 가능하다고 표시하지 않고 `FullUpgradeRouteUnavailable`로 차단한다. major upgrade 대상과 minor upgrade 대상의 실행 계획을 분리한다.
## 확정·내보내기
동일 releaseId의 출력 폴더가 있으면 덮어쓰지 않는다. 확정 전 실패한 폴더는 격리하고 명시적인 정리 작업으로 제거한다. 기본 보존 대상은 최종 MSI/CAB/EXE, 서명된 실제 payload, wixpdb, 생성 WXS, 잠금/스냅샷, 로그와 검증 보고서다. 지원 중인 패치가 참조하는 기준은 자동 정리에서 제외한다.
배포 내보내기는 artifacts와 의존 관계, 최종 SHA-256, 설치/업그레이드 경로, 지원 OS·런타임, 패치 기준을 매니페스트로 만든다. offline-layout의 전체 설치 대체 경로는 단일 MSI가 아니라 필요한 CAB·런타임까지 포함한 artifact graph를 가리킨다. 업로드 URL에는 releaseId를 포함한다. payload URL 계획은 Burn 생성 전에 고정하고, 그룹 공개는 별도 배포 시스템에서 수행한다.
+56
View File
@@ -0,0 +1,56 @@
# 기준 버전별 패치 생성
## 입력 계약
패치는 이전 릴리스의 폴더와 새 폴더만 비교해 만드는 ZIP이 아니다. 보존한 기준 MSI/빌드 정보와 목표 전체 릴리스를 사용해 WiX MSP를 생성한다. WiX는 기준·업데이트 입력으로 MSI 또는 wixpdb와 관련 payload를 사용한다. 정확한 도구 호출은 버전별 백엔드가 담당한다. [WiX Windows Installer patches](https://docs.firegiant.com/wix/tools/patches/)
필수 입력은 `baseStateId`, `targetReleaseId`, 패치 정책, 고정 도구 프로필이다. baseState에는 기준 releaseId, 실제 ProductCode/PackageCode, ProductVersion, 언어, 아키텍처, 설치 범위, 기존 적용 PatchCode 집합, 관리 파일 상태를 포함한다. 목표는 이미 확정한 전체 릴리스의 동일한 서명 완료 파일을 사용한다. 패치 생성 중 목표 PE를 다시 서명하거나 재빌드하지 않는다.
기준 ProductVersion 문자열만 같은 임의 MSI는 허용하지 않는다. 등록된 기준 아티팩트 해시와 도구/설치 정의를 검증한다. 사용자 변경 데이터는 관리 파일 해시 비교에서 제외한다.
## 호환성 판정
| 검사 | 패치 생성 정책 |
| --- | --- |
| 제품 계열·ProductCode·언어·아키텍처·설치 범위 | 같은 패치 계열인지 확인; 불일치 시 전체 업그레이드 필요 |
| 목표 버전 | 기준보다 높은 MSI 버전; 네 번째 필드 변경만으로는 허용하지 않음 |
| GUID/key path/대상 경로 | MSI 컴포넌트 규칙 위반 시 차단 |
| 파일 추가 | 허용되는 새 컴포넌트/feature 구조인지 검사 후 지원 |
| 파일 삭제·이동·컴포넌트 제거 | 최초 버전은 전체 업그레이드 대상으로 분류; 자동 MSP 생성 금지 |
| 파일 버전 | PE 버전 역행, 내용 변경 없이 버전만 조작, 내용 변경인데 파일 버전 동일 등의 교체 규칙 검사 |
| 레지스트리 변경 | 지원 템플릿 내 변경과 제거/복구 동작 검증 |
| 설치 과정·UI 변경 | Component/Feature/CustomAction/조건 변경을 호환성 검사에 포함. 번들 UI 변경은 MSP만으로 배포되지 않으므로 새 번들 경로도 검증 |
| 필수 런타임·드라이버·서비스 구조 변경 | 최초 버전은 전체 업그레이드 대상으로 분류 |
| 기준 파일·wixpdb 등 누락 | 생성 중단; 현재 폴더로 기준을 임의 복원하지 않음 |
판정 결과는 `PatchEligible`, `FullUpgradeRequired`, `InvalidInput`로 구분한다. `FullUpgradeRequired`가 나오면 위반 항목과 새 제품 계열/ProductCode 검토 필요성을 표시한다. 사용자가 패치 생성을 요청했다고 자동으로 GUID를 바꾸거나 다른 형태의 패키지를 성공 결과로 내놓지 않는다.
이 기준은 초기 지원 범위를 보수적으로 정의한 것이다. 모든 파일 삭제·런타임 변경이 Windows Installer에서 원천적으로 불가능하다는 뜻은 아니다. 지원 범위를 넓히려면 템플릿과 시험 매트릭스를 함께 확장한다.
## 생성 흐름
1. 기준과 목표의 서명·해시·식별자 및 잠금 스냅샷을 읽는다.
2. 파일/레지스트리/컴포넌트 차이와 호환성 보고서를 생성한다.
3. PatchCode와 patch family/sequence, 대체 관계를 예약한다.
4. 기준→목표 패치 정의를 생성하고 WiX로 MSP를 만든다.
5. MSP를 서명하고 실제 적용 대상·순서·식별자를 기대값과 비교한다.
6. 크기·SHA-256·서명·기준 상태·목표·전체 대체 경로를 기록하고 불변 패치로 보관한다.
7. 각 선언된 기준 상태의 VM 시험 결과를 연결하여 배포 가능 여부를 판정한다.
초기에는 **기준 상태 하나에서 목표 하나로 가는 MSP 하나**를 기본으로 한다. 여러 기준을 선택하면 작업을 여러 개 만든다. 실제로 여러 기준을 지원하는 단일 MSP와 무제한 패치 체인은 후속 범위다. 패치가 전체 설치보다 커지거나 이점이 작으면 결과에 표시하고 배포 정책에서 전체 경로를 선택할 수 있게 한다.
새로 설치한 1.14.1과 1.14.0에 패치를 적용해 도달한 1.14.1은 표시 버전이 같아도 기준 MSI/패치 이력이 다를 수 있다. 별도 baseState로 기록하여 다음 패치를 각각 시험한다. 불필요한 모든 조합을 지원하지 않고 지원 상태 목록을 명시한다.
## 실행기와 Burn의 계약
패키지 생성기는 `applyRoute`를 출력하고 실제 적용은 기존 업데이터의 연계 작업이다. MSI가 관리하는 파일을 Rust 업데이터가 직접 덮어쓰지 않는다. 실행기는 설치 식별자와 패치 상태를 조회하고 승인된 MSP 적용 명령을 사용한다.
최초 구현의 시험용 경로는 Windows Installer를 통한 직접 MSP 적용으로 둘 수 있다. 정식 배포 전에는 Burn으로 설치한 제품에서 패치 후 번들 표시 버전, ARP, repair, uninstall, 다음 전체 업데이트가 일관적인지 확인해야 한다. 필요 시 패치용 Burn 번들을 추가한다. 이 검증이 끝나기 전에는 패치 파일 생성 성공만으로 운영 배포를 허용하지 않는다.
패치 실패 뒤에는 rollback 완료·재부팅 필요·현재 설치 상태를 확인한다. 상태가 불명확하면 전체 설치를 즉시 덮어 실행하지 않는다. 전체 대체 경로는 동일 목표 릴리스이며 해당 source state에서 검증된 업그레이드 동작을 가져야 한다. [전체 릴리스의 업그레이드 경로](05-release-pipeline.md)
## 검증·보존 기준
기준 설치 → 패치 적용 → 목표 신규 설치와 공급자 관리 파일/레지스트리/제품 등록 비교 → 실행 → repair → 제거 → 다음 패치 순서를 검증한다. MSI 파일 버전 규칙과 수정된 비버전 파일 때문에 파일이 실제로 교체되지 않는 경우까지 검사한다. 재부팅 코드와 MSI source cache 누락도 시험한다.
패치 제거 지원은 기본 꺼짐이며, 가능하게 작성하고 데이터/다음 업데이트까지 검증한 패치만 활성화한다. 일반 장애 대응은 더 높은 버전의 수정 릴리스를 우선한다. 기준과 목표의 전체 패키지, 원본 서명 payload, 패치 순서 정보와 검증 증빙은 지원 기간 동안 함께 보존한다. [운영 패치 정책](../plan/08-version-patching.md)
+70
View File
@@ -0,0 +1,70 @@
# Tauri 화면과 명령 인터페이스
## 화면 구성
| 화면 | 입력/행동 | 결과 |
| --- | --- | --- |
| 프로젝트 | 생성·열기·저장, 제품명/제조사/엔진, 소스 alias 연결 | 현재 revision, 미확정 필드 |
| 파일 목록 | 파일/폴더 추가, include/exclude, 대상 폴더, 서명 정책 | 실제 설치 트리, 용량, 누락·충돌·변경 파일 |
| 설치 설정 | 기본/기존 경로, perMachine, x64, 필수 런타임 | 설치 디렉터리·사전 조건 미리보기 |
| 설치 과정 | 단계 추가·삭제·활성화·순서·조건, 핸들러 설정 | 모드별 실행 계획, 의존성·필수 단계 검사 |
| 설치 UI | 테마·로고·색상·문구, 지원되는 페이지·입력 항목 편집 | 모드별 미리보기, 선택한 BA 백엔드의 지원 범위 |
| 레지스트리 | hive/view/key/name/type/value 편집 | 형식 검증, 생성/수정/삭제 차이 |
| 버전과 식별자 | 목표 버전, 전체 전환/동일 계열 선택 | 자동 할당 GUID와 변경 이유, 기존 계열 잠금 |
| 릴리스 생성 | 출력 프로필, 서명 프로필, 생성 계획 | 파일/용량/도구/설치 경로 요약과 오류 |
| 패치 생성 | 목표 릴리스, 기준 상태 복수 선택 | 기준별 호환 여부·차이·대체 경로 |
| 작업/이력 | 실행·취소·실패 작업 재검사, 로그 보기 | 단계 진행률, 실패 원인, 결과 폴더 |
| 릴리스 상세 | 파일·서명·검증 증빙, 내보내기 | 확정 결과와 배포 준비 상태 |
파일 수가 수천 개이므로 트리·표는 가상화하고 정렬·검색·필터를 제공한다. 큰 목록은 Rust 쪽에서 페이지 단위로 조회하며 매번 전체 파일 목록을 IPC로 보내지 않는다. 단일 EXE로 만들 수 없는 용량은 생성 시작 전에 가능한 출력 방식을 설명한다.
일반 흐름은 프로젝트 열기 → 설정 수정 → 계획 미리보기 → 빌드 실행 → 결과 확인이다. 패치는 목표 전체 릴리스를 먼저 만들고 기준 릴리스를 선택한다. 설정이 달라졌으면 예전 계획을 실행할 수 없으며 차이를 다시 계산한다.
미서명 개발 결과에는 `개발용 · 배포 불가`를 표시한다. 서명 완료와 설치 시험 완료를 별도 항목으로 표시하여 성공 아이콘 하나로 뭉뚱그리지 않는다. 업로드/그룹 승격 버튼은 초기 구현 범위에 넣지 않고 배포 시스템용 파일 내보내기를 제공한다.
## Tauri IPC 계약
아래 이름은 구현할 Command 제안이며 현재 존재하는 API가 아니다.
| Command | 요청 → 응답 |
| --- | --- |
| open_project | 허용된 파일 선택 결과 → ProjectHandle, revision, 요약 |
| save_project | handle, expectedRevision, DTO → 새 revision |
| resolve_files | handle, revision → 목록 snapshotId, 요약 |
| list_resolved_files | snapshotId, cursor, filter → page, nextCursor |
| validate_project | handle, revision → ValidationReport |
| preview_install_flow | handle, revision, installMode, mockState → 시스템 변경 없는 실행 계획 |
| preview_installer_ui | handle, revision, backend, installMode → 미리보기 모델과 미지원 기능 |
| plan_release | handle, revision, outputProfiles → planId, 입력 해시, 변경/경고 |
| plan_patch | targetReleaseId, baseStateIds → 기준별 planId 또는 차단 이유 |
| start_build | planId, clientRequestId → jobId |
| get_job / cancel_job | jobId → 작업 상태 / 취소 접수 |
| list_releases / get_release | 조회 조건 / releaseId → 이력 / 상세 |
| export_release | releaseId, 허용된 목적지 → export 결과 |
서버 측 세션의 ProjectHandle과 planId를 사용하고 WebView에서 전달한 절대 경로를 그대로 신뢰하지 않는다. `clientRequestId`는 중복 클릭/재전송을 같은 jobId로 연결한다. 계획 데이터·권한·프로젝트 revision은 Rust가 재검증한다.
작업 이벤트는 `jobId`, 단조 증가 `sequence`, `stage`, `status`, `completedBytes`, `totalBytes`, `messageCode`를 포함한다. WebView가 끊겼다가 돌아오면 `get_job`과 마지막 sequence 이후 기록을 조회한다. 이벤트만이 유일한 상태 저장소가 되지 않는다. 로그는 크기 제한·회전·비밀값 제거를 적용한다.
오류는 `code`, `stage`, `message`, `details`, `retryable`, `logRef` 형태다. 예: `MissingRequiredFile`, `TargetPathCollision`, `IdentityConflict`, `PatchIncompatible`, `PayloadTooLarge`, `SigningFailed`, `ToolchainMismatch`, `FullUpgradeRouteUnavailable`. 화면은 원인과 바꿔야 할 설정을 보여 주고 외부 도구 원문은 상세 로그에서 확인한다.
## CLI 계약
다음은 구현 예정인 명령 형식이다. 현재 실행 가능한 명령이 아니다.
```powershell
package-builder validate --project .\package-project.json --format json
package-builder plan release --project .\package-project.json --profile offline-layout
package-builder build --plan-id <planId>
package-builder plan patch --target <releaseId> --base-state <baseStateId>
package-builder job status --id <jobId> --format json
package-builder export --release <releaseId> --output D:\exports\egbim
```
CI용 `release`/`patch` 편의 명령은 같은 엔진에서 plan+build를 연속 호출하도록 추가한다. 비대화식 실행은 미확정 설정을 질문으로 보류하지 않고 실패 코드로 종료한다. JSON 결과는 stdout, 진행 로그는 stderr로 분리하고 비밀값을 두 출력 모두에서 제거한다.
제안 종료 코드는 0 성공, 2 입력/검증 오류, 3 호환 불가, 4 도구 실행 실패, 5 서명/무결성 실패, 6 작업 잠금 충돌, 130 취소다. 이것은 생성기 종료 코드이며 MSI의 0/3010/1641 등 설치 결과 코드와 혼동하지 않는다. 명령 성공은 해당 명령의 완료만 의미하고 일반 사용자 배포 성공을 의미하지 않는다.
## 생성 프로그램 자체 배포
Tauri 앱 자체의 설치 패키지와 버전은 EG-BIM의 프로젝트/제품 GUID와 분리한다. 개발 머신의 WebView2, WiX, SDK 및 서명 연동 도구를 검사하는 환경 진단 화면을 제공한다. 필요한 외부 도구를 자동 다운로드하거나 번들에 재배포하는 기능은 라이선스·고정 버전 검토 후 추가하며, 최초 구현은 관리자가 준비한 도구 프로필을 사용한다.
@@ -0,0 +1,58 @@
# 검증 계획과 구현 순서
## 자동 검증
| 계층 | 검증할 핵심 동작 |
| --- | --- |
| 도메인 | MSI 버전 경계, GUID 수명, component 이동/삭제 판정, 지원 기준 상태 비교 |
| 프로젝트 계약 | JSON Schema와 Rust 역직렬화 일치, 알 수 없는 필드, 마이그레이션, revision 충돌 |
| 파일 해석 | 포함/제외, 필수 파일 누락, 대소문자 충돌, 경로 탈출/reparse point, 원본 무변경 |
| 레지스트리 | 타입 범위, 32/64비트, XML/MSI 문자열 escaping, 지원하지 않는 HKCU 정책 차단 |
| 작업 엔진 | 가짜 WiX/Signer를 통한 단계 실패, 취소, 중복 요청, 체크포인트, 잠금 충돌 |
| 실제 도구 통합 | 작은 샘플 제품으로 MSI/EXE/MSP 생성, 실제 식별자와 서명/해시 추출 |
| 대용량 | 총 2GB 초과와 여러 CAB, 크기 합산 오버플로, 메모리 사용, 개별 제한 초과 실패 |
| UI/IPC | 계획 이후 설정 변경 차단, 권한 없는 Command, 입력/로그의 HTML 실행 방지 |
| 설치 과정/UI | 의존 단계 삭제·순환·조건 오류 차단, 대화형/무인 결과 일치, patch/repair Feature 유지, CustomAction 복구, WebView2 없는 PC |
CAB 시험에는 압축이 매우 잘 되는 0바이트 반복 데이터만 쓰지 않는다. 실제 압축률에 가까운 fixture를 사용해 산출물 크기와 분할 경계를 검증한다. 개발용 인증서 시험과 공인 서명 서비스 시험을 구분한다. 개발용 인증서 통과를 공인 서명이나 SmartScreen 평판 통과로 표시하지 않는다.
## Windows VM 설치 시험
실제 설치 시험은 생성기/개발자의 업무 PC가 아닌 스냅샷 복구 가능한 VM에서 수행한다. 각 결과에는 사용한 최종 아티팩트 해시, OS, 계정/권한, 기준 상태, 실행 로그와 결과를 연결한다.
| 시나리오 | 합격 조건 |
| --- | --- |
| 신규 설치 | 기본/사용자 지정 경로, 파일·레지스트리·런타임·실행 정상 |
| 기존 InstallShield 전환 | 실제 배포본별 탐지, 설정·CUI·도면 보존, 기존 삭제 스크립트 영향 확인 |
| 오프라인 전체 설치 | 네트워크 차단 상태에서 CAB와 런타임을 모두 로컬로 해석 |
| 네트워크 설치 | 단절·재시도·캐시·해시 불일치 처리, 불완전 다운로드 미적용 |
| 패치 | 선언한 기준 상태 각각에서 목표 신규 설치와 공급자 관리 상태 일치 |
| 패치 불가/전체 대체 | 잘못된 ProductCode·언어·패치 이력 차단, 검증된 전체 업그레이드 실행 |
| 파일 버전/수정 | 동일 PE 버전의 변경 파일, 수정된 비버전 파일, 누락 파일의 결과 검증 |
| 실패/재부팅 | 앱 실행 중, 파일 잠김, 디스크 부족, 3010/1641, rollback와 복구 판정 |
| 설치 후 유지보수 | Burn 표시와 MSI 상태, repair, uninstall, 다음 패치·전체 업그레이드 |
| Store 출력 | 단일 파일, 무다운로드, silent 경로, 서명, 버전별 고정 URL 조건 |
사용자 데이터 보존은 해시가 반드시 동일해야 하는 공급자 파일과 별도 판정한다. 저장된 백업이 있다는 것만으로 복구 성공이라고 하지 않고 실제 복원과 앱 재실행까지 확인한다.
## 구현 순서
| 단계 | 구현 내용 | 완료 기준 |
| --- | --- | --- |
| 1. 기준 확정 | 실제 배포 payload, ISM 대조, 런타임, 출시본 GUID, 도구 버전 | 출처가 기록된 첫 프로젝트와 전환 위험 목록 |
| 2. Rust 엔진/CLI | 스키마, 파일 해석, identity lock, 검증/계획 | fixture에서 안정적인 목록·GUID와 설명 가능한 오류 |
| 3. 전체 릴리스 | WXS 생성, MSI/Burn, 분할 CAB, 서명 어댑터 | 작은 제품과 2GB 초과 제품의 전체 설치 시험 통과 |
| 4. 버전별 패치 | 기준 저장소, diff, 호환성, MSP, 순서/이력 | 최소 두 기준 상태에서 동일 목표 도달·repair·제거 통과 |
| 5. Tauri UI | 프로젝트/파일/레지스트리/릴리스/패치/작업 화면 | CLI와 같은 계획·결과, 긴 작업 중 UI 응답 유지 |
| 6. EG-BIM 전환 | 실제 파일/레지스트리/백업, 기존 업데이터 연계 | 실제 IS 출시본→WiX→후속 MSP 경로 시험 통과 |
| 7. 배포 연결 | 내보내기, 외부 증빙, 업로드 계약, Store 가능성 | 불변 파일과 기준 정보가 배포 시스템에 연결 |
버전별 패치는 필수 범위이며 최초 제품 완성에서 제외하지 않는다. 다만 작은 시험 제품으로 엔진을 검증한 뒤 실제 EG-BIM 데이터를 적용한다. UI 작업은 엔진 계약이 안정화된 후 병행할 수 있다.
설치 과정/화면 편집 요구는 [09 설계](09-install-flow-and-ui.md)를 따른다. 단계 2~3에 과정 검증과 기본 테마 출력을, 단계 5에 과정/UI 편집기를 추가한다. 자유로운 페이지/분기 지원은 Rust/Burn 연결 실험 후 사용자 정의 BA로 구현하며 기본 테마 편집만으로 해당 요구를 완료 처리하지 않는다.
## 정식 배포를 막는 미확정 사항
실제 설치용 소스 폴더, 제조사 표시명, 런타임 목록, 서명 프로필, 기존 설치본 목록과 파괴적 제거 동작, 업데이터의 MSI/MSP 실행 계약, Burn과 패치의 유지보수 동작은 아직 확정되지 않았다. 초안 설정으로 편집·차이 계산·개발 패키지 검증을 진행할 수 있지만 정식 릴리스의 배포 준비 상태는 부여하지 않는다.
이 문서 작성 단계에서는 설계 문서의 경로·예제 JSON·문서 간 일관성만 검사한다. Rust 컴파일, WiX 빌드, 실제 서명, 설치·패치·Store 심사는 수행하지 않았다.
+122
View File
@@ -0,0 +1,122 @@
# 설치 과정 편집과 설치 UI 커스터마이징
## 목표
패키지 제작자가 설치 단계를 추가·삭제·비활성화·재배치하고, 최종 사용자가 보는 설치 화면도 변경할 수 있게 한다. 제작자가 정하는 실행 정책과 설치 사용자가 선택하는 옵션은 구분한다.
- **설치 과정 편집기**: 환경 검사, 런타임, 제품 설치, 설정 이전, 바로가기, 완료 후 실행 등 작업을 설정한다.
- **설치 UI 편집기**: 로고·색상·문구·약관·입력 항목·화면 순서를 설정하고 모드별 미리보기를 제공한다.
화면을 숨기는 것과 설치 작업을 제거하는 것은 별개다. 설치 폴더 화면을 숨겨도 경로 탐지·검증은 수행한다. 무인 설치에도 같은 설치 규칙을 적용한다.
## 실행 구조
```mermaid
flowchart TD
A[Tauri 제작 프로그램] --> F[설치 과정 정의]
A --> U[UI 정의와 리소스]
F --> C[Rust 검증 및 변환]
U --> C
C --> M[MSI 컴포넌트 · Feature · Action]
C --> B[Burn Chain · 조건 · BA 리소스]
B --> S[최종 Setup.exe]
M --> S
S --> E[설치 PC의 Burn / Windows Installer]
```
제작 프로그램의 Tauri 화면과 배포되는 Setup.exe의 화면은 별개다. 설정을 WiX/Burn 정의와 UI 리소스로 변환하여 서명된 산출물에 포함한다. 이미 서명한 EXE의 테마나 작업을 외부 JSON으로 바꾸지 않고 새 빌드·서명·검증을 거친다.
## 단계 카탈로그
| 작업 | 구현 위치 | 편집 규칙 |
| --- | --- | --- |
| OS·공간·기존 제품 검사 | Burn 탐지/조건; 필요한 항목은 MSI에도 검사 | 제품 필수 조건은 잠금 |
| 런타임 설치 | Burn Chain 패키지 | 앱 의존성이 없어졌을 때 제거 가능 |
| 파일·레지스트리 설치 | MSI Component/Feature와 표준 Action | 리소스 그룹 편집; MSI 표준 실행 순서는 임의 이동하지 않음 |
| 바로가기·파일 연결 | MSI 표준 정의/검증된 확장 | 선택 기능으로 추가/제거, 기존 설치 영향 검사 |
| 기존 설정 백업·이전 | 검증된 마이그레이션 핸들러 | 해당 전환의 필수 단계이면 삭제 차단 |
| 앱/서비스 종료·시작 | MSI 표준 기능/검증된 확장 우선 | 데이터 저장, 종료 실패, 재시작 정책 필요 |
| 완료 후 앱 실행 | BA의 설치 성공 후 작업 | 대화형 설치에서 선택, 원래 사용자 권한으로 실행 |
| 추가 업무 작업 | 버전이 고정된 사용자 정의 핸들러 | 권한·입력·설치 모드·실패 복구 계약 필요 |
카드 목록에서 추가/삭제/활성화, 조건 설정, 가능한 범위의 순서 이동을 제공한다. 내부 모델은 의존 관계를 가진 그래프다. 카드의 파일·레지스트리 설정은 실제 리소스 정의를 참조하고, MSI 밖 스크립트로 중복 실행하지 않는다.
`StepDefinition`은 `id`, `kind`, `enabled`, `appliesTo`, `phase`, `dependsOn`, `condition`, `configRef`, `handlerRef`를 가진다. 필수 여부·권한·지원 phase·실패 정책은 신뢰된 핸들러 카탈로그에서 정한다. 제작자가 필수 여부를 바꿔 보호 동작을 우회하지 못하게 한다.
`appliesTo`는 install, upgrade, patch, repair, uninstall을 구분한다. phase는 detect, beforeApply, managedInstall, afterSuccess로 제한한다. 이것이 MSI Sequence 번호를 직접 의미하지는 않는다. MSI 내부 작업이 필요한 핸들러는 별도의 스케줄 계약을 가진다.
조건은 all/any/not/equals/isInstalled/featureSelected 같은 타입이 지정된 표현식으로 저장한다. 임의 JavaScript/PowerShell을 평가하지 않으며 변수 타입·평가 시점을 검사한다.
## 편집 검증
1. 삭제 단계의 의존 작업, 그래프 순환, phase 역전, 지원하지 않는 모드를 빌드 전에 검출한다.
2. 필수 런타임·무결성 검증·제품 설치·필수 백업은 임의로 삭제할 수 없다.
3. 재실행 가능한 작업에는 멱등성 또는 완료 마커가 필요하다. 마커는 제품·버전·설치 시도에 연결한다.
4. 단계 삭제가 기존 PC의 자원 삭제를 뜻하지는 않는다. 유지/제거/이전 정책을 명시하고 Component/Feature/CustomAction 변경을 패치 호환성 검사에 전달한다.
5. 모드별 실행 계획을 각각 보여 준다. 완료 후 앱 실행은 uninstall·무인 패치에서 제외한다.
설치에 필수인 변경을 afterSuccess에 놓아 실패 시 복구할 수 없는 구성을 허용하지 않는다. 완료 후 선택 작업 실패는 이미 성공한 설치 트랜잭션 실패와 구분해 보고한다.
## 사용자 정의 작업
표준 MSI 기능과 검증된 WiX 확장을 우선한다. 추가 업무 작업은 개발자가 작성한 Rust 실행 파일/DLL 등의 핸들러를 등록하는 인터페이스로 지원한다. MSI ABI 또는 외부 프로세스 프로토콜과 수명 관리 어댑터가 필요하며, 임의 EXE를 곧바로 MSI CustomAction으로 넣는 기능은 아니다.
핸들러에는 버전·해시·서명자·입력 스키마·실행 계정·시간 제한·종료 코드·재부팅 동작·rollback/복구 방법을 등록한다. 제작자는 등록된 핸들러를 추가/삭제하고 매개변수를 편집한다. 셸 명령 문자열을 입력해 관리자 작업으로 실행하는 기능은 초기 범위에서 제외한다.
시스템을 변경하는 MSI CustomAction은 적합한 deferred 실행과 rollback 작업을 설계하고 필요한 데이터를 제한된 CustomActionData로 전달한다. 즉시 실행 Action에서 시스템을 변경하거나 변경이 자동 복구된다고 가정하지 않는다. [Microsoft Custom Actions](https://learn.microsoft.com/en-us/windows/win32/msi/about-custom-actions), [Rollback Custom Actions](https://learn.microsoft.com/en-us/windows/win32/msi/rollback-custom-actions)
Burn 체인 전체를 무조건 하나의 원자적 트랜잭션으로 표시하지 않는다. 런타임 EXE·외부 설정 이전은 별도의 복구 한계가 있다. MSI rollback, 보상 작업, 수동 복구 필요 상태를 구분한다. 기존 InstallShield 제거 스크립트에 의한 데이터 삭제도 별도 백업·복원 범위다.
## UI 출력 방식
| 백엔드 | 지원 목적 | 제약 |
| --- | --- | --- |
| `wixstdba-theme` | 로고, 이미지, 문구, 약관, 글꼴/배치 등 외형 수정 | WixStdBA의 페이지·컨트롤·동작 계약 안에서 변경 |
| `custom-burn-ba` | 추가 입력 화면, 기능 선택 마법사, 조건별 분기, 자유로운 배치 | 별도 BA 프로그램과 Burn 연결 구현·검증 필요 |
| MSI 자체 UI | MSI 직접 실행 시 기본 화면 | Burn UI와 별도이며 중복 입력/표시 방지 필요 |
WixStdBA는 테마 XML과 리소스로 화면을 교체할 수 있지만 동작까지 제한 없이 바꾸는 UI 프레임워크는 아니다. 공식 페이지 계약을 지키는 테마를 생성한다. [WixStdBA 테마](https://docs.firegiant.com/wix/tools/burn/wixstdba/)
첫 구현은 기본 테마 편집으로 브랜드·문구·배치를 제공한다. **임의의 페이지 추가/삭제·분기까지 필요한 프로젝트는 사용자 정의 BA로 처리한다.** 미지원 기능은 생략하지 않고 `UnsupportedInstallerUiCapability`로 표시한다. 자유로운 화면 요구를 기본 테마만으로 완료했다고 표시하지 않는다.
UI 편집기는 템플릿 선택, 리소스 등록, 문구/다국어, 허용 컨트롤 배치, 모드별 미리보기로 구성한다. 자유 배치 캔버스에 앞서 템플릿+속성 패널을 구현한다. 색상 등의 토큰은 백엔드에서 실제 지원하는 속성에만 매핑한다. 웹 CSS를 native 테마에 그대로 적용한다고 가정하지 않는다.
## Rust/Tauri 사용자 정의 설치 UI
제작 프로그램과 별도로 `installer-ui` 실행 프로그램과 `burn-bridge` 계층을 설계한다. 설치 화면에는 제작기의 파일 편집·도구 실행 API를 포함하지 않는다. UI는 입력·탐지·진행률을 표시하고 Rust 어댑터가 Burn의 Detect → Plan → Apply 및 취소/오류/재부팅 이벤트를 연결한다. UI가 별도로 msiexec를 실행하여 Burn과 경쟁하게 만들지 않는다.
WiX는 out-of-process BA와 native API를 제공하지만 Rust/Tauri용 완성 SDK를 제공한다는 뜻은 아니다. 고정한 WiX 버전의 native ABI/콜백에 대한 Rust FFI 연결을 작은 실험으로 먼저 검증한다. 필요한 native 어댑터의 범위·유지보수 비용도 이때 확정한다. [WiX out-of-process BA](https://docs.firegiant.com/wix/whatsnew/oopbas/)
Tauri 설치 UI는 WebView2 없는 PC에서도 시작할 수 있어야 한다. 지원되는 native 선행 BA/오프라인 UI 런타임 등 부트스트랩 경로를 검증한다. UI가 뜬 다음에야 UI 런타임을 설치하는 순환 구조를 피한다. 오프라인/Store 출력은 UI 런타임도 다운로드 없이 준비하고 전체 용량에 포함한다.
UI는 일반 권한을 유지하고 시스템 변경은 Burn/MSI의 적합한 권한 경로로 수행한다. 입력·경로·Feature 선택을 Rust에서 재검증한다. 원격 페이지나 임의 JS에 관리자 실행 기능을 연결하지 않는다. Windows UAC 보안 화면은 커스터마이징 대상이 아니다.
## 데이터 계약과 무인 설치
`PackageProject`에 `installationFlow`와 `installerUi`를 추가한다. 기존 초안 예제에서 생략한 경우 버전이 고정된 기본 과정/테마를 적용하고 빌드 스냅샷에는 전체 정의와 리소스 해시를 저장한다.
| 정의 | 필드 |
| --- | --- |
| InstallationFlow | schemaVersion, templateId/version, steps, variables, features |
| InstallerUi | backend, templateId/version, assets, strings/locales, pages, bindings |
| UiPage | id, kind, appliesTo, visibleWhen, controls, transitions |
| UiBinding | controlId, variableRef, type, validator, silentDefault 또는 requiredInput |
| HandlerDescriptor | id/version, capability set, inputSchema, execution/rollback contract |
| CompiledInstallPlan | 모드별 단계, MSI/Burn 매핑, UI 지원 검사, 정의/리소스 해시 |
페이지 전환과 작업 의존성은 다른 그래프다. 다음 버튼을 여러 번 눌러도 설치는 중복 실행되지 않는다. 적용 시작 후 선택값과 계획을 고정한다. MSI 속성 입력은 허용 목록·타입·권한을 검사하며 비밀번호/토큰을 일반 속성·명령줄·로그에 넣지 않는다.
Feature 선택은 MSI 모델에 연결하고 repair/patch에서 설치된 선택 상태를 유지한다. 패치가 미설치 선택 기능을 임의 추가하지 않게 한다. 무인 설치는 같은 검증을 사용하며 필수 입력이 기본값이나 명령줄로 제공되지 않으면 명확히 실패한다. 약관 화면 숨김을 동의로 처리하지 않고 무인 배포의 동의 전달 규칙을 정의한다. 작업을 특정 화면 버튼에서만 실행하지 않는다.
## 검증과 구현 순서
단계 카탈로그·검증 → 과정 편집기 → 기본 테마 편집 → Rust/Burn 연결 실험 → 사용자 정의 BA·페이지 편집 순서로 구현한다. 모두 기존 전체 릴리스·MSP와 같은 스냅샷·서명·검증 절차를 적용한다.
- 의존 단계 삭제, 순환, 모드/조건 오류의 사전 검출.
- 대화형·숨김·무인 모드에서 관리 파일/레지스트리 결과의 동등성.
- optional Feature, patch/repair 선택 상태 유지, 제거와 rollback.
- DPI·키보드·스크린 리더·다국어·취소·실패·재부팅 화면.
- 핸들러 실패/시간 초과/재부팅 재개, WebView2 없는 PC와 오프라인 실행.
- 테마/핸들러 변경 후 새 빌드 해시·서명·증빙 연결.
편집기 미리보기는 시스템 변경 없이 동작한다. 미리보기와 최종 native UI는 다를 수 있어 실제 번들의 VM 화면 시험을 완료 기준으로 둔다. 현재는 설계만 작성했으며 BA/FFI 구현·설치 시험은 수행하지 않았다.
+45
View File
@@ -0,0 +1,45 @@
# 설치 패키지 자동화 생성 프로그램 설계
작성 기준: 2026-10-16. 대상 제품: EG-BIM/Baron. 구현 언어는 **Rust**, 데스크톱 화면은 **Tauri 기반 웹 UI**로 확정한다. 이 문서들은 구현 전 설계이며 실행 가능한 프로그램이나 검증된 설치 패키지는 아직 없다.
프로그램은 설치 파일 목록, 설치 위치, 레지스트리, 버전·GUID를 프로젝트로 관리하고, 전체 릴리스와 기준 버전별 패치를 생성한다. GUI와 CI용 CLI가 같은 Rust 엔진을 사용한다. 패키지 생성 백엔드는 기존 검토를 이어 **WiX MSI/MSP + Burn EXE**를 기준으로 설계한다.
## 문서 구성
| 문서 | 설계 내용 |
| --- | --- |
| [01 구조와 모듈](01-architecture.md) | Tauri, Rust 공통 엔진, CLI, 작업 실행과 저장 구조 |
| [02 프로젝트와 데이터 모델](02-project-model.md) | 설정 파일, 파일 목록, 릴리스 기록, 예제 JSON |
| [03 파일·설치 위치·레지스트리](03-files-and-registry.md) | 파일 수집, 컴포넌트, 설치 경로, 레지스트리 작성 규칙 |
| [04 버전과 GUID](04-version-and-identity.md) | ProductCode, UpgradeCode, PackageCode, 패치 계열 |
| [05 릴리스 생성](05-release-pipeline.md) | 스냅샷, WiX 생성, 서명, 대용량 분할, 산출물 확정 |
| [06 패치 생성](06-patch-pipeline.md) | 기준 선택, 호환성 판정, MSP 생성, 전체 설치 전환 |
| [07 화면과 명령 인터페이스](07-ui-and-commands.md) | 화면별 기능, Tauri IPC, 작업 이벤트, CLI |
| [08 검증과 구현 순서](08-validation-and-roadmap.md) | 자동 검증, Windows VM 시험, 단계별 완료 기준 |
| [09 설치 과정과 UI 편집](09-install-flow-and-ui.md) | 단계 추가·삭제·순서·조건, UI 테마, 사용자 정의 Burn 화면 |
| [프로젝트 예제](examples/egbim.package-project.json) | 설계용 초기 프로젝트 설정 |
## 범위와 결정
| 구분 | 내용 |
| --- | --- |
| 필수 | 파일·폴더 등록, 포함/제외, 설치 경로, 레지스트리, 버전/GUID, 전체 릴리스, 버전별 MSP |
| 필수 | PE·MSI·MSP·Burn 서명 연동, 2GB 초과 전체 용량 대응, 기준 릴리스 보존, 검증 보고서 |
| 필수 | 제작자의 설치 과정 추가·삭제·조건 편집, 설치 UI 커스터마이징; 표준 테마와 사용자 정의 BA의 지원 범위 구분 |
| 확정 | Windows 대상, InstallShield 중단, Rust + Tauri |
| 설계 제안 | Tauri 2, TypeScript + React + Vite, JSON 프로젝트, WiX/Burn 백엔드 |
| 연계 | 업로드용 메타데이터 내보내기, 개발자→1차→2차→사용자 배포에 동일 아티팩트 사용 |
| 별도 시스템 | 배포 서버·그룹 승인, EG-BIM의 실제 자동 업데이트 실행부, Store 제출 |
| 후속 검토 | MSIX 출력, 클라우드 업로드 어댑터, 기존 ISM의 자동 가져오기 |
Tauri는 **생성 프로그램의 화면과 데스크톱 실행 환경**이다. Tauri가 자체 앱을 배포하는 번들러와 EG-BIM 패키지를 만드는 엔진은 별개다. EG-BIM을 Tauri 앱으로 바꾸거나, Tauri 업데이터를 기존 제품에 바로 적용한다는 뜻이 아니다.
기존 설치 기본 경로 `C:\Program Files\Baron\EG-BIM`을 유지한다. 실제 설치 파일 목록은 별도로 확정해야 하며 `D:\egbim\repo\14.1` 전체를 배포 파일로 취급하지 않는다. InstallShield 제거 스크립트가 사용자 데이터를 지울 수 있으므로 최초 전환에는 백업·복원 검증이 필요하다. [기존 현황](../plan/01-requirements-and-current-state.md), [설치기 전환](../plan/02-installer-migration.md)
전체 설치 결과는 단일 EXE로 한정하지 않는다. 대용량 기본 출력은 `Setup.exe + MSI + 분할 CAB + 필수 런타임` 레이아웃이며, 네트워크용 EXE는 필요한 데이터를 내려받는다. Store용 단일 오프라인 설치 파일은 용량과 제출 요건을 통과한 경우에만 생성한다.
## 완료된 설계와 남은 확정 사항
문서의 타입·명령·파일 형식은 구현 계약 제안이다. 도구의 정확한 버전, 제조사 표시명, 실제 배포 소스 폴더, 필수 런타임 목록, 서명 프로필, 지원 기준 릴리스는 첫 구현 단계에서 확정한다. 값이 비어 있거나 검증되지 않은 프로젝트는 편집·미리보기만 허용하고 정식 릴리스 확정은 차단한다.
기존 운영 정책은 [배포 계획](../plan/README.md)을 따른다. 이 설계에서는 생성 프로그램의 책임과 출력 계약을 구체화한다.
@@ -0,0 +1,82 @@
{
"schemaVersion": 1,
"projectId": "egbim-windows-x64",
"status": "draft",
"product": {
"productId": "E901",
"displayName": "EG-BIM",
"manufacturer": null,
"architecture": "x64",
"language": 1042,
"installScope": "perMachine",
"version": "1.14.0",
"displayVersion": "1.14.0",
"engineVersion": "14.1",
"upgradeCode": "{E6381427-A8BC-4506-A428-CE860BDA119F}",
"installRoot": {
"base": "ProgramFiles64Folder",
"relativePath": "Baron",
"appDirectory": "EG-BIM",
"preserveExistingPath": true,
"allowUserSelection": true
}
},
"sources": [
{ "alias": "appPayload", "binding": "local" }
],
"files": [
{
"id": "application-payload",
"source": "appPayload",
"include": ["**/*"],
"exclude": ["**/*.pdb", "**/*.log", "**/.git/**"],
"destinationDirectory": "APPDIR",
"required": ["Grimi.exe"]
}
],
"registry": [
{
"id": "example-install-path",
"hive": "HKLM",
"view": "registry64",
"key": "Software\\Baron\\EG-BIM",
"name": "InstallPath",
"value": { "type": "string", "mode": "msiFormatted", "data": "[APPDIR]" },
"component": "registry-install-path",
"uninstall": "removeOwnedValue",
"reviewed": false
}
],
"identity": {
"lockFile": "identity.lock.json",
"initialMigration": "majorUpgrade",
"legacyProducts": [
{
"productCode": "{FBEE188C-7D40-4CBD-80DA-89919B116F57}",
"productVersion": "1.13.1",
"migrationValidated": false
}
]
},
"prerequisites": { "reviewed": false, "items": [] },
"outputs": {
"profiles": ["offline-layout"],
"cabUncompressedBudgetBytes": 536870912,
"singleFileRejectAtBytes": 2147483648,
"networkBaseUrl": null,
"storeSingleFileEnabled": false
},
"signing": {
"profileRef": null,
"ownershipPolicyRef": null,
"requiredForRelease": true
},
"patchPolicy": {
"enabled": true,
"mode": "oneMspPerBaseState",
"supportedBaseStateIds": [],
"allowFileRemoval": false,
"allowUninstall": false,
"requireValidatedFullUpgradeRoute": true
}
}
@@ -0,0 +1,59 @@
# 요구사항과 기존 프로그램 현황
EG-BIM의 기존 설치 기반을 보존하면서 네트워크 배포를 추가한다. 설치 도구 변경과 앱 구조 변경을 한 번에 강제하지 않는 방향으로 검토한다.
## 확정 요구사항
| 항목 | 내용 |
| --- | --- |
| 운영 환경 | Windows 데스크톱 프로그램 |
| 대상 소스 | `D:\egbim\repo\14.1` |
| 기존 배포 | InstallShield `.ism`에서 생성한 EXE |
| 기존 설치 위치 | `C:\Program Files\Baron` 하위 |
| 설치 도구 | 향후 InstallShield 사용 중단 |
| 패키지 운영 | 버전별 관리 및 배포용 업로드 |
| 업데이트 | 현재 설치 버전에 맞는 변경분 패치와 전체 설치 경로 모두 지원 |
| 배포 순서 | 개발자 테스터 → 1차 테스터 → 2차 테스터 → 일반 사용자 |
| 추가 검토 | Microsoft Store 배포 |
네트워크 설치는 파일 전달 방식이고 MSI·EXE·MSIX는 설치 형식이다. 네트워크 설치에 MSI가 필수인 것은 아니다. 현재 프로젝트에 MSI를 권고하는 이유는 기존 MSI 제품과의 업그레이드 연속성이다.
## 저장소 확인 결과
| 항목 | 확인 내용 | 영향 |
| --- | --- | --- |
| 설치 프로젝트 | `InstallShield/src/EG-BIM_V1.13.1.ism` | 새 설치 정의를 작성하기 위한 참고 자료 |
| 설치 엔진 | MSI Property·Directory·Upgrade 테이블과 사용자 지정 작업 존재 | 기존 제품 식별자를 활용한 전환 검토 가능 |
| 설치 범위 | `ALLUSERS=1` | PC 전체 설치, 적용 단계의 관리자 권한 필요 |
| 설치 경로 | `ProgramFiles64Folder\Baron`, 앱 하위 폴더 `EG-BIM` | 기본 실행 위치는 `Baron\EG-BIM\Grimi.exe` 구성 |
| 제품 버전 | `.ism`의 `1.13.1.0`, `version.config`의 `1.13.1` | 엔진 버전 `14.1`과 제품 버전 분리 |
| 제품 식별자 | UpgradeCode `{E6381427-A8BC-4506-A428-CE860BDA119F}` | 과거 배포본에서도 일관적인지 확인 필요 |
| 앱 구성 | IntelliCAD 기반 네이티브 코드, .NET 8/WPF, C++/CLI 모듈 | 네이티브·관리형 런타임 및 플러그인 호환성 검증 |
| 선행 조건 | .NET 8 Desktop Runtime, VC++ 재배포 패키지, 웹 설치형 .NET Framework 등의 항목 등록 | 실제 필요한 런타임과 포함 조건을 정리해야 함 |
| 서비스 | 검토한 `.ism`의 ServiceInstall·ServiceControl 테이블은 비어 있음 | 다른 실행 파일이나 사용자 지정 작업의 서비스·드라이버 사용 여부는 별도 확인 |
`.ism`의 File 테이블은 4,701행, Registry 테이블은 379행이다. 이는 설치 정의의 규모이며 최종 설치되는 고유 파일 수나 현재 필요한 항목 수와 동일하다고 단정하지 않는다.
## 기존 업데이트와 설정 보존
`Pet.Entry`는 `BaronSoftware.Update` 2.0.0을 참조한다. 업데이트 옵션에는 제품 코드 `E901`, 표시 버전, 다운로드 폴더, 테스트 모드와 채널이 들어간다. 채널은 현재 로그인 사용자의 `IsCenter()`·`IsFamily()` 결과에 따라 `Center`, `Family`, `External` 중 하나를 선택한다.
새 4개 그룹을 이 값들과 자동으로 대응시키지 않는다. 기존 채널의 업무상 의미와 서버의 인증 방식을 확인한 뒤 매핑한다. 앱의 `E901` 코드와 MSI의 GUID ProductCode는 서로 다른 식별자다.
제품 버전은 호스트 실행 파일 `Grimi.exe`의 FILEVERSION에서 읽는다. 사용자 레지스트리와 CUI를 백업·복원하는 코드도 있으며, 엔진 버전에 종속된 일부 설정은 복원에서 제외한다. 오프라인 라이선스 사용자는 현재 진입 코드에서 자동 업데이트를 건너뛰므로 별도 배포 경로가 필요하다.
## 참고 파일
아래 경로는 모두 대상 소스 루트 `D:\egbim\repo\14.1` 기준이다.
- `InstallShield/src/EG-BIM_V1.13.1.ism`
- `InstallShield/src/Script Files/Setup.rul`
- `InstallShield/version.config`
- `InstallShield/관리지침.md`
- `Source/Module/Pet.Entry/Pet.Entry.csproj`
- `Source/Module/Pet.Entry/Global.cs`
- `Source/Module/Pet.Entry/Entry.cs`
- `Source/Module/Pet.Entry/Services/UpdateService.cs`
- `Source/Module/Pet.Entry/Services/UserDataBackup/UserDataLocalBackupService.cs`
- `Source/Module/Nyng.Interop.Interface/Nyng.Interop.Interface.vcxproj`
- `CoWork/Export/IrxPackages/IrxInstaller/README.md`
+56
View File
@@ -0,0 +1,56 @@
# InstallShield 대체와 기존 설치본 전환
권고안은 WiX로 MSI를 작성하고 Burn으로 필수 런타임과 MSI를 묶은 전체 설치 EXE를 만드는 것이다. 사용자에게는 기존처럼 EXE 설치 파일을 제공할 수 있다. WiX 채택과 도구 버전·이용 조건은 구현 시작 전에 확정한다.
## 대체 도구 검토
| 선택지 | 장점 | 이 프로젝트의 주요 과제 |
| --- | --- | --- |
| WiX MSI + Burn EXE | MSI 업그레이드 체계 활용, 설치 정의의 코드 관리, 런타임 묶음 설치 | 기존 구성요소·레지스트리·사용자 지정 작업 이관 |
| Inno Setup EXE | 스크립트 기반 제작, 저장소에 IRX SDK 설치 경험 존재 | 기존 MSI 제거 및 새 설치 실패 복구를 별도 설계 |
| MSIX | Store 관리 업데이트와 패키지 배포 기능 | 설치 경로, CAD 연동, 데이터 저장, 자체 업데이트 구조 검증 |
WiX와 Burn의 기능은 [WiX 공식 설명](https://docs.firegiant.com/wix/)을 참고한다. IRX SDK 설치기는 본 프로그램과 설치 범위·목적이 다르므로 그대로 재사용한다고 가정하지 않는다.
## 산출물과 설치 경로
- `EG-BIM.msi`: 앱 파일, 레지스트리, 바로가기 등 설치 정의.
- `Setup.exe`와 MSI·분할 CAB·필수 런타임 폴더: 대용량 제품의 기본 전체 설치 레이아웃. 단일 파일이 가능한 경우에만 `FullSetup.exe`로 묶는다.
- `Patch.msp`: 지원하는 기준 버전에서 목표 버전으로 갱신하는 Windows Installer 패치. 세부 정책은 [버전별 패치](08-version-patching.md)를 따른다.
- `WebSetup.exe`: 서버의 고정된 MSI·CAB·런타임을 받아 설치하는 네트워크 진입점. 사용자별 목표 릴리스 선택은 배포 서버/업데이터와 연계한다.
총 용량이 2GB를 넘을 때는 외부 CAB 분할을 사용하며 단일 CAB·개별 파일·서명 EXE의 한계를 별도로 검사한다. 자세한 출력 프로필과 제한 처리는 [릴리스 생성 설계](../code-design/05-release-pipeline.md)를 따른다.
신규 설치 기본 경로는 `%ProgramFiles%\Baron\EG-BIM`으로 하고 기존 설치본은 실제 등록 경로를 탐지해 유지한다. 사용자 지정 경로를 무조건 기본 경로로 변경하지 않는다. 설치 경로와 사용자 데이터 경로는 분리하며 일반 사용자에게 프로그램 폴더의 광범위한 쓰기 권한을 부여하지 않는다.
## 기존 제거 작업의 위험
`Setup.rul`의 `CleanAfterUninstall`은 앱 폴더 일부, AppData의 설정·도구 폴더, 공용 데이터 일부, 사용자·시스템 레지스트리를 삭제한다. CAD 파일 확장자 `.dwg`, `.dxf` 등의 키도 삭제 대상이다.
`.ism`의 `UnintallAction`은 `REMOVE="ALL"` 조건으로 연결되어 있어 업그레이드 중 구버전 제거에서도 실행될 수 있다. 새 설치기에서 제거 동작을 개선하더라도 이미 설치된 구버전의 제거 동작이 자동으로 바뀌지는 않는다.
현재 설정의 RemoveExistingProducts는 InstallInitialize보다 앞에 있다. 새 설치기는 기존 제거와 신규 설치의 실행 순서 및 실패 복구를 다시 설계한다. WiX의 `afterInstallInitialize` 방식은 검토 후보지만 사용자 지정 파일·레지스트리 삭제까지 자동 복구된다고 가정하지 않는다. [WiX MajorUpgrade 동작](https://docs.firegiant.com/wix/schema/wxs/majorupgrade/)
## 전환 절차
1. 지원할 기존 버전별 ProductCode·UpgradeCode·설치 경로·필수 런타임을 수집한다.
2. 실행 중인 앱과 관련 도구를 확인하고, 작업 저장 후 종료하도록 안내한다.
3. 사용자 설정·CUI·사용자 추가 리소스를 제거 대상 밖에 백업한다. 백업 성공을 검증하기 전에는 기존 제품을 제거하지 않는다.
4. 새 설치 파일과 런타임을 모두 확보하고 서명·해시·디스크 공간을 검증한다.
5. 검증된 업그레이드 순서로 기존 제품 제거와 새 제품 설치를 수행한다.
6. 호환되는 사용자 설정을 복원하고, 도면 열기·저장·출력·인증·플러그인 실행을 확인한다.
7. 설치 완료 후 상태 확인까지 성공한 경우에만 백업 정리를 허용한다.
현재 백업·복원 코드는 일부 예외를 내부에서 처리하고 성공 여부를 호출자에게 명확히 전달하지 않는다. 전환용 경로에는 결과 상태와 실패 로그를 추가한다. 백업을 다운로드 임시 폴더 정리와 함께 삭제하지 않도록 수명도 분리한다.
PC 전체 설치와 HKCU·AppData의 사용자별 데이터를 구분한다. 다른 관리자 계정으로 UAC를 승인한 경우에도 원래 사용자의 설정을 다뤄야 하며, 여러 사용자의 프로필은 각 소유자와 복원 시점을 정의한다.
## MSI 식별자와 복구
기존 UpgradeCode를 계승하는 Major Upgrade를 우선 검토하되, 실제 배포본의 아키텍처·언어·제품 식별자와 함께 시험한다. 새 Major Upgrade의 ProductCode와 패키지 식별자는 적절히 갱신한다. GUID 복사만으로 호환성이 보장되지는 않는다.
위 Major Upgrade 권고는 최초 설치기 전환과 설치 구조가 달라지는 업데이트에 적용한다. 이후 모든 릴리스의 ProductCode를 변경하지 않는다. 같은 제품 계열의 MSP Minor Upgrade는 ProductCode와 구성요소 규칙을 유지하며 ProductVersion을 올리는 방향으로 설계한다. 전체 설치 파일의 갱신 방식도 해당 패치 계열과 일치시킨다. [Windows Installer 업데이트 구분](https://learn.microsoft.com/en-us/windows/win32/msi/patching-and-upgrades)
Windows Installer는 제품 버전 비교에 앞의 세 필드를 사용한다. 네 번째 숫자만 올려 새 업데이트를 구분하는 설계를 피하고, 제품 버전과 빌드 식별자의 매핑을 정한다. [ProductVersion 규칙](https://learn.microsoft.com/en-us/windows/win32/msi/productversion)
설치 중 실패 복구와 설치 완료 후 이전 릴리스 복귀는 별도 기능이다. 데이터 형식 변경으로 복귀가 불가능하면 더 높은 제품 버전의 수정 릴리스를 배포한다. 새 빌드에서 InstallShield 사용을 중단하더라도 최초 전환 시 기존 설치본의 등록된 제거 엔진을 호출할 수는 있다.
+67
View File
@@ -0,0 +1,67 @@
# 버전별 패키지 관리와 업로드
패키지는 등록 후 변경하지 않고 배포 정책만 변경한다. 같은 릴리스를 개발자 테스트부터 일반 배포까지 사용하며, 수정이 필요하면 새 릴리스를 만든다.
## 버전 및 관리 정보
| 정보 | 목적 |
| --- | --- |
| productId | 제품 식별. 기존 업데이트 제품 코드 `E901`과의 관계 정의 |
| productVersion | 사용자 표시와 업그레이드 판정에 사용할 제품 버전 |
| engineVersion | IntelliCAD 등 엔진 버전. 현재 대상은 `14.1` |
| releaseId / buildId | 고유 릴리스·빌드 식별자와 소스 커밋 연결 |
| platform / architecture | Windows 및 x64 등 대상 플랫폼 |
| artifacts | 파일 종류, 저장 경로, 크기, 최종 SHA-256, 서명 검증 결과 |
| patchTargets | 패치별 허용 기준 릴리스·제품 식별자·패치 이력·목표 릴리스 |
| fallbackArtifactId | 패치 적용 불가 또는 복구 후 사용할 동일 목표 버전의 전체 설치 파일 |
| compatibility | 지원 OS, 필수 런타임, 최소 업데이터 버전, 허용 업그레이드 경로 |
| recovery | 복구 가능한 릴리스와 데이터 호환 조건 |
예시 저장 경로는 다음과 같다. 값은 운영 체계를 설명하기 위한 예시다.
```text
/packages/egbim/1.13.2/build-001/windows-x64/FullSetup.exe
/packages/egbim/1.13.2/build-001/windows-x64/EG-BIM.msi
/patches/egbim/release-001/to-release-002/windows-x64/Patch.msp
/manifests/egbim/release-001.json
```
같은 URL의 바이너리를 교체하지 않는다. 패키지는 장기 캐시가 가능하고, 현재 그룹별 대상 버전을 반환하는 응답은 사용자 간 공유 캐시로 섞이지 않도록 한다.
위 `FullSetup.exe`는 단일 파일 출력이 가능한 경우의 예시다. 대용량 패키지는 MSI·분할 CAB·런타임을 각각 아티팩트로 등록하고 의존 관계와 전체 설치 실행 경로를 함께 저장한다. `fallbackArtifactId`도 필요한 모든 파일을 참조하는 설치 레이아웃/매니페스트로 연결해야 한다. 생성 프로그램의 데이터와 출력 계약은 [Rust + Tauri 설계](../code-design/README.md)를 따른다.
## 업로드와 공개 절차
| 단계 | 처리 | 실패 시 결과 |
| --- | --- | --- |
| 빌드 | 버전 반영, 난독화, 파일 서명, 패키징, 패키지 서명 | 업로드 중단 |
| 임시 업로드 | 필요하면 분할·재개 지원, 공개 저장 경로와 격리 | 전송 재시도 |
| 서버 검증 | 최종 해시·크기·서명·필수 파일·중복 릴리스 확인 | 공개 불가 |
| 설치 검증 | 신규 설치·지원 기준별 패치·전체 설치 전환·실행 확인 | 승인 불가 |
| 릴리스 등록 | 검증한 아티팩트와 메타데이터를 고정 | 그룹 공개 전 상태 유지 |
| 그룹 공개 | 승인된 그룹의 대상 릴리스를 원자적으로 변경 | 기존 정책 유지 |
권장 상태는 `Uploaded → Verified → Ready`이며 검증 실패는 `Rejected`로 관리한다. 그룹 배포 상태는 패키지 상태와 별도로 둔다. Ready는 모든 사용자에게 공개됐다는 뜻이 아니다.
CI 업로드를 기본으로 하되 관리자 수동 업로드도 같은 검증을 거친다. 업로드 권한과 일반 사용자 배포 승인 권한을 분리하고, 누가 어떤 릴리스를 어느 그룹에 공개했는지 기록한다. 동시에 두 배포가 변경될 때는 정책 버전 비교로 충돌을 검출한다.
## 서버 역할
| 구성 | 역할 |
| --- | --- |
| 패키지 저장소 | 서명된 파일과 불변 메타데이터 보관 |
| 배포 API | 인증, 장치·그룹 판정, 대상 버전 및 다운로드 권한 반환 |
| 관리 DB | 릴리스, 그룹, 배포 정책, 승인 이력 |
| 결과 수집 | 다운로드·설치·실행 상태와 오류 집계 |
인터넷 배포는 오브젝트 저장소와 CDN, 사내망 배포는 내부 HTTPS 서버를 후보로 둔다. 특정 클라우드 사업자는 아직 정하지 않았다. 비공개 테스트 패키지는 실제 다운로드 경로에도 접근 통제를 적용한다.
버전별 패치는 최초 필수 범위에 포함한다. 릴리스마다 전체 설치 파일을 유지하고, 지원하는 기준 버전에는 변경분 패치를 함께 제공한다. 서버는 그룹의 목표 릴리스를 먼저 결정한 뒤 설치 상태에 맞는 패치 또는 전체 설치 파일을 선택한다. 적용 범위와 복구 정책은 [버전별 패치](08-version-patching.md)를 따른다.
## 무결성과 보존
해시는 전송 중 변조·손상을 검출하지만 서버의 메타데이터와 해시가 함께 바뀌는 공격까지 막지는 못한다. 패키지의 Authenticode 서명과 배포 메타데이터 서명을 별도로 검토한다. 메타데이터에는 버전·만료·허용된 업그레이드 경로와 키 교체 정책이 필요하다. [TUF 메타데이터 설계](https://theupdateframework.io/docs/metadata/)
현재 배포 대상, 복구 대상, 진행 중인 설치가 참조하는 패키지는 삭제하지 않는다. Store에 등록한 URL도 해당 버전의 배포 수명 동안 유지한다. 보존 기간은 사용자 접속 주기와 저장 비용을 확인한 뒤 확정한다.
패치 생성·검증에 필요한 기준 및 목표 MSI, 서명 완료 원본 파일, WiX 설치 정의와 빌드 정보도 해당 기준 버전의 지원 기간 동안 보관한다. 후속 패치가 의존하는 이전 패치·원본을 삭제하지 않는다. 패키지 등록 정보에는 지원하는 각 기준에서 목표 상태로 도달하는 검증 결과를 연결한다.
+66
View File
@@ -0,0 +1,66 @@
# 사용자 그룹별 4단계 배포
하나의 릴리스를 네 그룹에 순차적으로 공개한다. 초기에는 담당자가 관측 결과를 확인한 후 다음 단계를 승인하는 방식을 권고한다.
## 그룹 구성
| 단계 | 대상 | 주요 확인 사항 | 관찰 기간 예시 |
| --- | --- | --- | --- |
| 1 | 개발자 테스터 | 설치, 실행, 핵심 기능, 복구 | 1일 |
| 2 | 1차 테스터 | 내부 실제 업무, 기존 데이터 호환 | 1~2일 |
| 3 | 2차 테스터 | 다양한 OS·장비·권한·네트워크 환경 | 2~3일 |
| 4 | 일반 사용자 | 확대 배포 안정성 및 서버 부하 | 단계적 확대 |
기간은 초기 제안이며 승인 기준으로 확정된 값이 아니다. EG-BIM의 사용 빈도를 고려해 최소 설치 수, 실제 실행 수, 관찰 시간을 함께 정한다. 2차 테스터는 단순 인원 확대보다 대표성 있는 환경 구성이 중요하다.
## 대상 결정
서버는 인증된 사용자·등록 장치·제품·설치 버전·환경 정보를 바탕으로 대상 릴리스를 반환한다. 클라이언트가 보낸 그룹 문자열만으로 테스트 버전을 허용하지 않는다.
PC 전체 설치이므로 장치별 유효 그룹을 하나로 결정한다. 여러 사용자의 그룹이 충돌하면 적용할 우선순위와 관리자의 장치 고정 정책을 정의한다. 최초 설치는 앱 로그인 전이므로 로그인·장치 등록·만료되는 등록 코드 중 식별 수단을 준비한다.
기존 `Center`, `Family`, `External`, 테스트 모드는 업무상 의미를 확인한 뒤 새 그룹으로 매핑한다. 테스트 모드를 개발자 그룹과 자동으로 동일시하지 않는다. 오프라인 라이선스에는 전체 설치 파일을 이용한 별도 경로를 둔다.
## 배포 정책
그룹별로 릴리스 ID, 시작 시각, 배포 비율, 상태, 승인 이력과 정책 버전을 관리한다. 일반 사용자 그룹 내에서 `5% → 20% → 50% → 100%`로 확대하는 방법은 선택 사항이다.
자체 비율 배포는 `장치 ID + 배포 ID`의 고정 해시로 대상을 결정한다. 배포 ID를 유지한 채 임계값만 올려 기존 선정 장치가 제외되지 않도록 한다. Store 자체 점진 배포의 대상 선정은 Microsoft가 관리하며 이 규칙과 다르다.
예시:
```text
개발자 테스터: release-002
1차 테스터: release-002
2차 테스터: release-001
일반 사용자: release-001
```
2차 테스터 승인 시 해당 정책만 release-002로 변경한다. 패키지를 다시 빌드하거나 재서명하지 않는다.
하나의 목표 릴리스는 전체 설치 파일과 기준별 패치로 구성될 수 있다. 각 장치에는 현재 설치 상태에 맞는 아티팩트를 제공하되 목표 버전은 그룹 정책을 따른다. 같은 패치를 단계마다 다시 생성하지 않으며 지원 기준별 패치와 전체 설치 경로를 모두 검증한 결과로 승격한다. 특정 패치만 중지한 경우에는 해당 목표 릴리스의 전체 설치가 별도로 승인되어 있을 때만 전환하고, 릴리스 전체 중지라면 어느 경로로도 우회하지 않는다.
## 승인과 중지
- 최소 설치·실행 수 및 관찰 기간을 충족해야 한다.
- 설치 성공률, 설치 후 실행, 도면 작업 등 핵심 기능을 확인한다.
- 기존 버전 대비 비정상 종료·오류 증가가 허용 범위 안에 있어야 한다.
- 실행 불가·데이터 손상 등 중대 장애가 있으면 다음 단계 승격을 막는다.
수치 기준과 담당자는 구현 전 확정한다. 설치 성공률의 분모는 설치를 실제 시작한 건수 등으로 명시하고, 다운로드 실패·사용자 취소·재부팅 대기·미응답을 별도로 집계한다. 미응답 장치를 성공으로 취급하지 않는다.
패치 ID와 기준→목표 릴리스별 성공률, 실패 후 전체 설치 전환 횟수도 별도 집계한다. 전체 설치로 복구된 장치를 패치 자체의 성공으로 기록하지 않는다.
배포 중지 후에는 새 설치 허가를 차단한다. 클라이언트는 다운로드 이후 설치 직전에 중지 상태를 다시 확인한다. 이미 시작한 설치를 강제 종료하지 않으며, 오프라인 장치의 중지 반영 시점에는 한계가 있다. 설치 완료 장치의 복구는 별도 정책으로 실행한다.
## 클라이언트 진행 상태
```text
대상 조회 → 호환성 확인 → 다운로드 → 서명·해시 검증
→ 설치 허가 재확인 → 설정 백업 → 앱 종료·설치
→ 재부팅 필요 처리 → 실행 상태 확인 → 결과 보고
```
동일 설치 시도의 상태 보고는 중복 전송을 허용하되 서버에서 중복 집계하지 않는다. 네트워크 단절 시 결과를 보관해 재전송하고, 서버 장애만으로 기존 앱 사용을 차단하지 않는다. 필수 보안 업데이트의 실행 제한은 별도 정책으로 결정한다.
배포 링 운영의 참고 자료는 [Microsoft 배포 계획 가이드](https://learn.microsoft.com/en-us/windows/deployment/update/create-deployment-plan)다.
+50
View File
@@ -0,0 +1,50 @@
# Microsoft Store 배포 방식
현재 EG-BIM에는 자체 업데이터와 PC 전체 설치 구조가 있으므로 MSI·EXE 등록 경로를 우선 검토한다. Store 배포에 MSIX가 반드시 필요한 것은 아니다.
## 두 경로의 차이
| 항목 | MSI·EXE 등록 | MSIX 제출 |
| --- | --- | --- |
| 파일 보관 | 자체 HTTPS 저장소 | Microsoft 호스팅 |
| 서명 | 게시자가 설치 파일과 포함 PE 파일 서명 | Store가 패키지 서명 처리 |
| 업데이트 | 자체 업데이터 담당 | Store 관리 |
| 지정 테스터 배포 | 자체 서버에서 구현 | Package Flights 지원 |
| 비율 배포 | 자체 서버에서 구현 | Store 점진 업데이트 지원 |
| 현재 앱 적용 | 기존 경로와 설치 기능을 유지하는 방향 | 경로·레지스트리·CAD 연동·업데이터 검증 필요 |
[Microsoft 배포 방식 비교](https://learn.microsoft.com/en-us/windows/apps/distribute-through-store/how-to-distribute-your-win32-app-through-microsoft-store)
## MSI 및 EXE 제출 조건
1. 버전별 고정 HTTPS 다운로드 URL을 제출한다.
2. 제출한 URL의 파일은 이후 덮어쓰지 않는다. 새 파일은 새 URL로 등록한다.
3. 무인 설치를 지원한다. 설치 UI는 표시하지 않지만 UAC 표시는 허용된다.
4. 설치 중 본체나 런타임을 추가 다운로드하지 않는 전체 설치 파일이어야 한다.
5. 설치 파일과 포함된 PE 파일에 신뢰 가능한 코드 서명이 있어야 한다.
Store용은 필요한 오프라인 런타임과 MSI를 포함한 단일 `FullSetup.exe` 등 제출 요건을 만족하는 파일로 만든다. 대용량으로 단일 파일의 생성·서명 제한을 넘으면 이 경로를 사용 가능하다고 간주하지 않는다. 자체 배포용 분할 CAB 폴더나 작은 `WebSetup.exe`가 그대로 Store 제출을 대신하는 것은 아니다. 현재 `.ism`의 웹 설치형 선행 조건은 그대로 이관하지 않고 실제 의존성을 정리한다. [공식 패키지 요구사항](https://learn.microsoft.com/en-us/windows/apps/publish/publish-your-app/msi/app-package-requirements), [출력 프로필과 용량 검사](../code-design/05-release-pipeline.md)
## 자체 배포와 함께 운영
자체 사이트와 Store에서 같은 검증 완료 전체 설치 파일을 사용할 수 있다. Store에는 일반 사용자에게 공개할 릴리스만 등록한다. 최초 설치 후의 업데이트는 자체 서버가 그룹별로 제어한다.
Store MSI·EXE 등록은 자체 서버의 4단계 배포 정책을 대신하지 않는다. 자체 서버의 배포 중지와 Store의 새 사용자 설치 중지는 별도 조치다. 두 경로의 공개 상태와 설치 패키지 버전을 함께 기록해야 한다.
Store 등록·심사 시점과 자체 배포 시점을 조정하고, Store의 오래된 전체 설치 파일로 시작한 사용자가 최신 허용 릴리스로 이동하는 경로도 검증한다.
Store의 신규 설치용 등록 파일은 항상 전체 MSI·EXE로 유지한다. MSP를 신규 설치 파일 대신 제출하지 않는다. 설치 이후 자체 업데이터는 [버전별 패치 정책](08-version-patching.md)에 따라 호환되는 MSP를 적용할 수 있으며, 해당 버전의 전체 설치본과 패치 적용 결과가 같은 관리 상태가 되는지 검증한다.
## MSIX로 전환할 경우
Package Flights에 개발자·1차·2차 테스트 그룹을 구성하고 정식 제출을 일반 사용자 단계에 연결할 수 있다. Microsoft 계정으로 테스터를 등록하고 Flight도 인증 절차를 거친다. 초기 앱 제출 게시 후 Flight를 구성하는 흐름을 고려한다. [Package Flights](https://learn.microsoft.com/en-us/windows/apps/publish/package-flights)
Store 점진 배포는 기존 사용자의 업데이트에 대한 비율 제어다. 최초 설치에도 같은 비율 제한이 적용된다고 가정하지 않는다. 중지는 이미 업데이트된 앱을 이전 버전으로 되돌리지 않으므로 수정 릴리스를 준비해야 한다. [점진 배포](https://learn.microsoft.com/en-us/windows/apps/publish/gradual-package-rollout)
MSIX 전환 시에는 고정 설치 경로, 설치 위치 쓰기, 외부 CAD·플러그인·COM 연동, 사용자 설정 이동, 런타임을 검증한다. 현재 구조만으로 불가능하다고 판정한 것은 아니다. [MSIX 준비 사항](https://learn.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-prepare)
## SmartScreen과 설치 경로
Microsoft 공식 안내는 Store를 통한 MSI·EXE 설치 과정에는 SmartScreen 경고가 없고 UAC는 나타날 수 있다고 구분한다. 이는 Microsoft가 해당 EXE를 재서명한다는 의미가 아니다. Store에서 제공하는 MSIX 패키지 서명 처리와 다르다.
같은 EXE를 자체 웹사이트에서 내려받으면 일반 다운로드의 SmartScreen 평판 판단을 받는다. Store 등록만으로 자체 배포 URL까지 경고 면제가 되지는 않는다. Store 설치 후 자체 업데이터가 내려받는 파일도 별도의 서명·검증이 필요하며 Store 설치 경로의 효과를 그대로 가정하지 않는다. [코드 서명 및 배포 경로 비교](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/code-signing-options)
@@ -0,0 +1,79 @@
# 코드 서명 서비스와 SmartScreen
WiX는 설치 패키지 생성 도구다. 회사 명의의 코드 서명 인증서 또는 서명 서비스는 별도로 준비해야 한다. WiX 빌드에서 서명 도구를 호출해 자동화할 수 있다.
## 역할과 서비스 선택
| 구성 | 역할 |
| --- | --- |
| WiX·Burn | MSI·설치 EXE 생성 |
| 코드 서명 인증서 또는 서비스 | 검증된 게시자 신원과 서명용 키 제공 |
| SignTool 등 | 인증서·서비스를 이용한 서명 및 검증 |
기존 회사 인증서가 있다면 명의·유효기간·키 접근 방식·빌드 연동 가능 여부를 먼저 확인한다. 새로 도입한다면 Azure Artifact Signing의 Public Trust를 우선 검토한다. 다른 인증기관의 코드 서명 인증서도 선택 가능하다.
Azure Artifact Signing은 Microsoft가 운영하며 이전 이름은 Trusted Signing이다. 게시자에는 신원 확인을 받은 회사의 법인명이 사용된다. 제품을 Microsoft Corporation 명의로 서명해 주는 서비스가 아니다. 이 서비스를 사용하면 해당 서명을 위해 다른 인증기관 인증서를 별도로 구매할 필요는 없다. [서비스 소개](https://azure.microsoft.com/en-us/products/artifact-signing), [게시자 명의 FAQ](https://learn.microsoft.com/en-us/azure/artifact-signing/faq)
한국 소재 조직은 현재 Public Trust 신청 대상이다. 개인 개발자는 미국·캐나다 제한이 있다. 일반 공개 배포에는 Public Trust를 사용하며 Public Trust Test나 Private Trust를 대신 사용하지 않는다. 가입 자격은 서비스의 [현재 설정 안내](https://learn.microsoft.com/en-us/azure/artifact-signing/quickstart)에서 확인한다. 일부 Windows 배포 개요 문서의 지역 목록보다 이 서비스 전용 안내를 우선 참고한다.
## 요금
2026-10-16에 확인한 공식 공개 요금은 다음과 같다.
| 요금제 | 계정당 월 기본료 | 월 포함 서명 | 초과 서명 |
| --- | --- | --- | --- |
| Basic | US$9.99 | 5,000회 | 회당 US$0.005 |
| Premium | US$99.99 | 100,000회 | 회당 US$0.005 |
원화 청구액은 적용 환율·세금·계약에 따라 달라진다. 별도 빌드·저장소·CDN 비용은 이 표에 포함하지 않는다. [공식 요금표](https://learn.microsoft.com/en-us/azure/artifact-signing/how-to-change-sku)
과금 단위는 사용자 설치 횟수가 아니라 서명 횟수다. 300개 파일을 각각 한 번 서명하면 약 300회이고, 여기에 MSI·Burn 엔진·최종 설치 파일 및 재서명 횟수를 더한다. 완성된 동일 패키지를 1만 명이 설치해도 그 이유로 추가 서명은 발생하지 않는다. Basic으로 시작하되 실제 파일 수와 릴리스 횟수를 측정한다.
## 서명 대상
| 대상 | 처리 |
| --- | --- |
| 자체 제작 EXE·DLL | 최종 빌드 파일을 회사 명의로 서명 |
| IRX 등 기타 확장자 | 실제 PE 모듈인지 확인하고 지원되는 서명·검증 경로 적용 |
| 이미 유효하게 서명된 자체 파일 | 게시자·무결성 확인 후 유지 가능 |
| 외부 DLL·런타임 | 공급자 서명을 검증·보존, 미서명 파일은 공급자 배포본과 서명·재배포 조건 검토 |
| MSI | 패키지 생성 후 서명 |
| MSP | 패치 생성 후 서명·타임스탬프, 적용 후 목표 PE 파일의 서명도 검증 |
| Burn 설치 EXE | 내부 엔진과 최종 번들 각각 서명 |
| 네트워크 설치기·업데이터 | 각각 서명 |
이미 존재하는 EXE도 재빌드 없이 서명할 수 있다. 서명은 파일 바이트와 해시를 바꾸므로 최종 해시는 서명 후 계산한다. .NET의 `.snk` 강력한 이름 서명은 Authenticode 코드 서명을 대체하지 않는다.
Store MSI·EXE 제출은 설치 파일과 포함된 모든 PE 파일의 서명을 요구하므로 설치 EXE만 서명해서는 충분하지 않다. [Store 패키지 요구사항](https://learn.microsoft.com/en-us/windows/apps/publish/publish-your-app/msi/app-package-requirements)
## 빌드 순서
```text
버전 반영·빌드·난독화 완료
→ 배포 폴더에 파일 수집
→ 기존 서명 검사 및 필요한 PE 파일 서명·타임스탬프
→ 검증
→ MSI 생성·서명·타임스탬프
→ Burn 번들 생성
→ Burn 엔진 서명·재결합·최종 EXE 서명·타임스탬프
→ 최종 검증·해시 계산
→ 업로드
```
서명 후 버전 정보 변경이나 난독화를 수행하지 않는다. SHA-256과 RFC 3161 타임스탬프를 적용하고, 타임스탬프 실패도 배포 실패로 처리한다. 유효기간 내 서명 증거는 인증서 만료 후 검증에 필요하지만 인증서 폐기 등의 조건도 영향을 준다. [SignTool](https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool), [WiX 서명 연동](https://docs.firegiant.com/wix/tools/signing/)
패치는 보관된 기준 패키지와 서명이 완료된 목표 파일로 구성된 업데이트 패키지를 입력으로 생성한다. MSP에도 별도로 서명하고 최종 해시를 계산한다. 적용 후 설치되는 자체 PE 파일은 목표 릴리스의 서명·해시와 일치해야 한다. 사용자 PC에 서명 키를 배포하거나 클라이언트에서 재서명하지 않는다. MSP 및 패치용 EXE의 서명 횟수도 요금 산정에 포함한다.
키는 저장소·배포 패키지·사용자 PC에 넣지 않는다. 보호된 서명 서비스 또는 장치를 사용하고 배포 빌드에 필요한 권한만 부여한다. Azure는 인증서가 갱신되므로 업데이터에서 인증서 지문 하나를 영구 고정하지 않는다. 체인·코드 서명 용도·배포자 식별·갱신 정책을 함께 설계한다. [인증서 수명 관리](https://learn.microsoft.com/en-us/azure/artifact-signing/concept-certificate-management)
## SmartScreen의 한계
Azure Artifact Signing을 사용해도 SmartScreen 경고가 발생할 수 있다. SmartScreen은 게시자 평판과 해당 파일 해시의 평판을 함께 평가한다. 외부 인증서로 서명했어도 새 파일의 평판이 부족하면 경고가 나올 수 있으며, Azure 서명도 즉시 신뢰를 보장하지 않는다. EV 인증서 역시 현재 경고 면제를 보장하지 않는다. [Microsoft SmartScreen 설명](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/smartscreen-reputation)
| 배포 경로 | 기대할 수 있는 동작 |
| --- | --- |
| 자체 웹 + Azure 서명 | 유효한 게시자 확인, 초기 SmartScreen 경고 가능 |
| 자체 웹 + 외부 OV·EV 서명 | 유효한 게시자 확인, 초기 SmartScreen 경고 가능 |
| Store에서 직접 설치 | 공식 안내상 Store 설치 중 SmartScreen 경고 없음, UAC는 가능 |
Azure 선택의 근거는 비용과 자동화 편의성이다. SmartScreen 경고 제거를 구매 효과로 약속하지 않는다. 일반 사용자의 설치 마찰을 줄이려면 Store 경로를 함께 운영한다. UAC, SmartScreen, 백신 탐지는 서로 다른 기능이므로 경고 종류를 구분해 수집한다. [배포 경로 비교](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/code-signing-options)
@@ -0,0 +1,65 @@
# 구현 순서와 검증 계획
첫 검증 목표는 기존 InstallShield 설치본을 새 설치본으로 전환하면서 사용자 설정과 CAD 연동을 보존하는 것이다. 이 경로를 검증한 뒤 서버의 단계 배포를 연결한다.
## 구현 전 결정할 항목
| 항목 | 현재 상태 | 필요한 결정·확인 |
| --- | --- | --- |
| 새 설치 도구 | WiX + Burn 권고 | 채택 여부, 지원 버전, 이용 조건 |
| 서명 | Azure Public Trust 우선 검토 | 기존 인증서, 법인 명의, 신청 자격, 서명량 |
| 기존 배포본 | 저장소의 1.13.1 설정 확인 | 실제 지원할 이전 EXE·MSI와 설치 로그 확보 |
| 업데이트 라이브러리 | NuGet 2.0.0 호출부 확인 | 명령 실행·권한·검증·복구 지원 범위 |
| 버전별 패치 | 필수 요구사항, MSP 우선 검토 | 지원 기준 버전, 누적 범위, 식별자·구성요소 규칙, 복구 절차 |
| 그룹 | 4단계 요구 확정 | 기존 채널과 매핑, 장치별 충돌 규칙 |
| 서버 | 구성 역할 제안 | 사내망·인터넷 범위, 사용자 수, 패키지 용량 |
| 승인 기준 | 관찰 기간 예시만 제시 | 최소 표본, 성공률, 장애 기준, 담당자 |
| Store | MSI·EXE 경로 우선 검토 | 계정·제품 등록, 서명·무인 설치·오프라인 조건 |
## 단계별 구현
1. **설치 목록 정리**: `.ism`에서 파일·레지스트리·바로가기·선행 조건·사용자 지정 작업을 추출하고 현재 앱에 필요한 항목을 판별한다. 실제 배포 대상 파일의 서명 목록도 만든다.
2. **새 전체 설치기**: WiX MSI와 Burn 전체 EXE를 구현하고 설치 경로·무인 설치·재부팅 상태·로그·런타임 설치를 검증한다.
3. **기존 설치본 전환**: 사용자별 백업, 기존 제거, 새 설치, 선택적 설정 복원, 실패 복구를 구현한다. 다른 CAD 제품과의 파일 연결도 확인한다.
4. **서명 자동화**: PE 파일부터 최종 EXE까지 서명·타임스탬프·검증을 빌드에 연결하고 최종 해시를 생성한다.
5. **패키지와 업로드**: 임시 업로드, 검증, 불변 릴리스 등록, 접근 통제, 감사 이력을 구현한다.
6. **버전별 패치**: 기준과 목표 패키지에서 MSP 생성, 지원 기준 검증, 서명, 패치 선택, 실패 상태 복구와 전체 설치 전환을 구현한다. 패치 지원을 위해 MSI 식별자·구성요소 규칙은 2단계부터 반영한다.
7. **4단계 배포**: 서버의 그룹 결정, 기존 업데이터 연동, 결과 수집, 수동 승격, 중지와 수정 릴리스를 연결한다.
8. **Store 제출 준비**: 전체 오프라인 설치, 모든 PE 서명, 무인 설치, 고정 URL, 정식 릴리스 공개 순서를 검증한다.
전체 설치와 버전별 패치는 최초 필수 범위다. 자체 바이너리 차분 엔진, 자동 승격, 복잡한 관리 화면은 측정 결과에 따라 확장한다. MSP와 별개인 독자 파일 교체 엔진은 우선 구현하지 않는다.
## 필수 검증 시나리오
| 분야 | 시나리오 | 통과 조건 |
| --- | --- | --- |
| 신규 설치 | 깨끗한 지원 Windows, 런타임 없는 상태 | 전체 EXE로 오프라인 설치·실행 가능 |
| 권한 | 일반 사용자, 관리자, 다른 계정으로 UAC 승인 | 필요한 단계만 승격, 원래 사용자 데이터 보존 |
| 기존 업그레이드 | 지원할 각 구버전, 사용자 지정 경로 | 제품 중복·경로 이동·설정 손실 없이 전환 |
| 버전별 패치 | 각 지원 기준→목표, 누적 적용, 중간 버전 생략 | 전체 설치와 동일한 목표 파일·설치 정보·기능 상태 |
| 패치 적용 거부 | 다른 기준, 아키텍처·제품 불일치, 변경·누락 파일 | 잘못된 패치 실행 금지, 승인된 전체 경로 선택 |
| 패치 이력 | 중복 적용, 이전 패치 존재, 설치 캐시 부족 | 이력 일관성, 재시도 안전성, 원본 필요 시 확보 또는 복구 |
| 패치 실패 | 적용 중 중단, 재부팅 대기, 롤백 실패 | 현재 상태 확인 후 복구, 불확실한 상태에서 전체 설치 강행 금지 |
| 패치와 설치 관리 | 패치 후 복구·제거·다음 업데이트, 전체 설치로 동일 버전 전환 | MSI 및 Burn 등록과 파일 상태 일치 |
| 사용자 데이터 | CUI, 레지스트리, 추가 리소스, 여러 사용자 | 소유자별 백업·복원, 엔진 종속 설정 제외 |
| CAD 공존 | 다른 CAD 설치 및 파일 연결 존재 | 타사 소유 연결·설정 훼손 없음 |
| 핵심 기능 | 인증, 도면 열기·저장·출력, 플러그인 | 기존 지원 기능 정상 동작 |
| 실패 복구 | 백업 실패, 설치 실패, 전원 중단, 디스크 부족 | 복구 가능한 상태와 로그·백업 보존 |
| 실행 중 업데이트 | 도면 편집 중, 파일 잠금, 중복 설치 요청 | 저장·종료 안내, 설치 직렬화, 작업 손실 방지 |
| 네트워크 | 중단·재개, 프록시, 만료 다운로드 URL | 재시도 가능, 불완전 파일 실행 금지 |
| 패키지 검증 | 손상·변조 파일, 다른 게시자, 만료 메타데이터 | 실행 차단 및 원인 기록 |
| 그룹 | 중복 소속, 조작된 채널, 비율 확대, 그룹 변경 | 서버 정책에 따른 일관된 대상 결정 |
| 배포 중지 | 다운로드 전·후 및 설치 시작 후 | 새 설치 차단 범위 명확, 진행 중 설치 강제 종료 없음 |
| 결과 집계 | 재전송, 중복 이벤트, 미응답, 재부팅 대기 | 중복 성공 집계 없음, 미응답을 성공 처리하지 않음 |
| Store | 전체 EXE 무인 설치·제거·수정 설치 | 제출 조건 및 자체 업데이트와의 호환성 충족 |
| 공개 다운로드 | 실제 HTTPS 다운로드 후 실행 | 서명·게시자·타임스탬프 확인, SmartScreen 결과 별도 기록 |
새 설치기의 성공 여부와 SmartScreen 경고 유무는 별도 평가한다. 서명 검증 통과만으로 SmartScreen 무경고를 합격 조건으로 보장하지 않는다.
## 완료 기준
지원 대상 구버전에서의 전환, 데이터 보존, 설치 실패 복구, 무인·오프라인 설치, 서명 검증이 통과해야 첫 개발자 그룹에 공개한다. 이후 각 그룹의 관측 기준을 통과하면 동일 패키지를 다음 그룹에 공개한다.
해당 릴리스가 지원한다고 선언한 모든 패치 기준의 적용 시험과 전체 설치 복구 경로가 통과해야 패치 지원 완료로 판단한다. 기준 버전 범위는 승인 전에 명시하고, 미검증 기준은 자동 패치 대상에서 제외한다. 상세 기준은 [버전별 패치](08-version-patching.md)를 따른다.
서버의 배포 중지와 Store 공개 상태 변경을 함께 다루는 운영 절차를 마련한다. 설치 완료 후 치명적 장애는 검증된 복구 릴리스 또는 높은 버전의 수정 릴리스로 대응한다. 일반 사용자의 다운그레이드를 임의로 허용하지 않는다.
+79
View File
@@ -0,0 +1,79 @@
# 버전별 패치와 복구
버전별 패치는 필수 요구사항이다. 현재 설치된 버전과 상태를 확인해 변경분만 적용할 수 있어야 하며, 신규 설치·최초 설치기 전환·패치 비호환 상태를 위한 전체 설치 패키지도 함께 제공한다.
## 권장 구현 방식
WiX가 생성하는 Windows Installer 패치인 MSP를 우선 검토한다. MSP는 기준 패키지와 업데이트 패키지의 차이를 담으며, 변경 내용에 따라 전체 설치보다 다운로드와 설치 부담을 줄일 수 있다. 모든 릴리스의 패치가 반드시 작아지는 것은 아니므로 생성 후 크기를 비교한다. [WiX 패치 지원](https://docs.firegiant.com/wix/tools/patches/)
MSI가 관리하는 파일을 별도 ZIP 업데이터로 덮어쓰는 방식은 우선 채택하지 않는다. 파일만 바꾸면 설치 복구·제거·버전 등록과 실제 파일이 어긋날 수 있다. MSI 관리 밖의 독립 콘텐츠가 있다면 소유 범위를 명확히 나눈 뒤 별도 방식으로 검토한다.
| 상황 | 기본 경로 |
| --- | --- |
| 미설치 PC | 전체 설치 |
| 기존 InstallShield 설치본에서 새 설치 체계로 전환 | 검증된 전체 설치 업그레이드 |
| 같은 제품 계열에서 패치 규칙을 만족하는 변경 | 해당 기준을 지원하는 MSP |
| 여러 버전을 건너뛰는 업데이트 | 기준을 명시한 누적·직접 패치, 없으면 전체 설치 |
| 제품 구조·아키텍처·필수 런타임 변경 등 패치 비호환 | 검증된 전체 설치 업그레이드 |
| 패치 대상 파일 변경·누락, 기준 또는 이력 불일치 | 패치 적용 차단 후 검증된 복구·전체 설치 경로 |
이 표의 최초 전환 정책은 프로젝트 권고안이다. 기존 InstallShield 패키지에 MSP를 적용하는 것이 기술적으로 항상 불가능하다는 뜻은 아니다.
## MSI 패치 계열 설계
최초 전환은 Major Upgrade로 검토하되, 이후 패치 가능한 계열은 ProductCode와 구성요소 식별·경로 규칙을 안정적으로 유지한다. 버전을 구분하는 패치는 Minor Upgrade를 우선 사용해 ProductVersion을 올리고, 변경된 MSI의 PackageCode는 갱신한다. 앱의 버전 명명 규칙과 Windows Installer의 Major·Minor 구분은 같은 의미가 아니다. [Windows Installer 업데이트 구분](https://learn.microsoft.com/en-us/windows/win32/msi/patching-and-upgrades)
새 WiX 설치 정의부터 패치 가능성을 고려한다. 파일 추가·삭제·이동, 구성요소 키 경로, 레지스트리 변경이 패치 규칙을 만족하는지 검사하고, 규칙에 맞지 않으면 전체 업그레이드로 전환한다. 기준 버전의 MSI, 서명된 실제 파일, 구성요소 정의, WiX 빌드 정보와 도구 버전을 보관한다.
Burn EXE를 통해 설치한 제품은 MSP 적용 후에도 번들 표시 버전·MSI 등록·복구·제거가 일관되도록 설계한다. 직접 MSP 실행 또는 패치용 번들 중 실행 방식을 정하고, 이후 전체 설치와의 탐지·업데이트 경로까지 시험한다. 전체 파일을 다시 실행하는 것만으로 같은 ProductCode의 Minor Upgrade나 복구가 자동 처리된다고 가정하지 않는다.
## 패키지와 메타데이터
전체 설치본과 여러 패치를 하나의 목표 릴리스에 연결한다. 패치 아티팩트마다 다음 정보를 관리한다.
| 필드 | 의미 |
| --- | --- |
| patchId / artifactId | 변경 불가능한 패치 식별자 |
| targetReleaseId | 적용 완료 시 도달할 목표 릴리스 |
| supportedBases | 허용 기준 릴리스·빌드·설치 정보의 명시적 목록 |
| installerIdentity | 제품 코드, 아키텍처, 언어, 설치 범위 |
| patchState | 필요한 기존 패치와 대체되는 패치, 적용 순서 |
| requirements | 최소 업데이터, OS·런타임 조건, 공간·재부팅 조건 |
| integrity | 패치 크기·해시·서명, 필요한 기준 파일 및 목표 파일 검증 정보 |
| fallbackArtifactId | 동일 목표 릴리스의 검증된 전체 설치 경로 |
표시 버전 문자열만으로 패치 가능 여부를 판정하지 않는다. 같은 버전에서 다시 빌드한 파일, 아키텍처, 적용된 패치 이력 등이 다를 수 있다. 클라이언트의 파일 검증은 공급자가 관리하는 대상에 한정하고 사용자 CUI·도면·사용자 설정은 기준 해시와 같아야 하는 파일에 포함하지 않는다.
## 선택 예시
아래 버전은 모두 전환 후 같은 패치 계열이라는 가정의 예시다. 실제 지원 여부를 의미하지 않는다.
| 현재 상태 | 그룹 목표 | 선택 |
| --- | --- | --- |
| 미설치 | 2.0.2 | 2.0.2 전체 설치 |
| 2.0.0, 기준 일치 | 2.0.2 | 2.0.0→2.0.2 누적 패치 |
| 2.0.1, 기준 일치 | 2.0.2 | 2.0.1→2.0.2 패치 |
| 2.0.1, 기준 불일치 | 2.0.2 | 검증된 복구·전체 설치 경로 |
| 2.0.2 | 2.0.2 | 정상 상태라면 작업 없음 |
| 2.0.3 | 2.0.2 | 일반 정책에서 다운그레이드하지 않음 |
초기에는 지원 기준에서 목표까지 한 번에 도달하는 패치를 우선한다. 누적 패치는 선언한 모든 기준과 패치 이력을 검증한다. 모든 과거 버전 쌍의 패치를 만들 필요는 없으며, 지원 범위 밖은 전체 설치로 연결한다. 지원 기준 범위와 보존 기간은 운영 규모에 맞춰 확정한다.
## 적용과 실패 처리
1. 인증된 그룹의 목표 릴리스를 결정하고 현재 설치 정보·패치 이력·기준 상태를 조회한다.
2. 검증된 적용 경로 중 패치를 선택한다. 호환 패치가 없거나 이점이 없으면 승인된 전체 경로를 선택한다.
3. 패치를 다운로드하고 서명·해시·호환 조건을 검증한다. 설치 직전 배포 중지 여부를 다시 확인한다.
4. 설정 백업 성공과 앱 종료를 확인하고 다른 설치 작업과 직렬화한다.
5. Windows Installer로 적용하고 종료 코드·재부팅 필요 여부·패치 등록 상태를 기록한다.
6. 목표 파일 및 자체 PE 서명, 설치 버전, 핵심 기능을 검증한 후 성공을 확정한다.
다운로드 실패는 원본 설치를 변경하지 않은 상태에서 재시도한다. 적용 실패는 먼저 롤백 완료·재부팅 대기·현재 설치 상태를 판별한다. 상태가 확인되고 전체 경로가 허용될 때만 전체 설치로 전환한다. 롤백 실패나 불확실한 상태에서는 로그·백업을 보존하고 복구 대상으로 표시하며 무조건 덮어쓰지 않는다.
패치 제거를 통한 이전 버전 복귀는 패치가 제거 가능하게 작성되고 데이터 호환성이 검증된 경우에만 허용한다. 그 외에는 더 높은 버전의 수정 릴리스를 사용한다. 클라이언트 캐시·원본 패키지가 필요한 복구·제거 경로도 시험한다.
## 배포와 완료 기준
지원 기준별 패치는 전체 설치본과 함께 4단계 배포를 통과한다. 동일 목표에 도달하더라도 각 패치 경로의 검증 결과가 필요하다. 전체 릴리스 중지는 패치와 전체 설치 모두에 적용한다.
패치 적용 후 새로 설치한 목표 버전과 공급자 관리 파일·레지스트리·등록 버전·기능 상태가 일치해야 한다. 사용자 데이터는 보존하며 동일 바이트 상태를 요구하지 않는다. 패치 후 복구·제거·다음 업데이트와 실패 복구를 [검증 계획](07-implementation-and-validation.md)에 포함한다.
+40
View File
@@ -0,0 +1,40 @@
# EG-BIM 네트워크 설치 및 배포 계획
기준일: 2026-10-16, Asia/Seoul. 대상 프로그램: `D:\egbim\repo\14.1`.
EG-BIM의 기존 Windows 설치 구조를 유지하면서 InstallShield를 대체하고, 버전별 패키지 관리와 4단계 그룹 배포를 지원한다. Microsoft Store 배포와 코드 서명도 함께 준비한다.
사용자가 확정한 사항은 Windows 데스크톱 앱 배포, InstallShield 사용 중단, 버전별 패키지 관리와 패치 지원, 업로드 체계, 개발자 테스터부터 일반 사용자까지의 4단계 배포다. Microsoft Store 배포는 검토 대상이다.
현재 권고안은 **WiX MSI + Burn 전체 설치 EXE + 자체 배포 서버 및 업데이터**다. Store에는 전체 설치 EXE를 등록하는 경로를 우선 검토한다. WiX 채택, Azure 서명 서비스 계약, 세부 운영 기준은 아직 확정되지 않았다.
## 문서 목록
생성 프로그램의 구현 설계는 [Rust + Tauri 설계 문서](../code-design/README.md)에 별도로 정리한다. 파일·레지스트리 편집, 버전/GUID, 전체 릴리스·MSP 생성, 대용량 분할 출력과 화면/CLI 계약을 다룬다.
| 문서 | 내용 |
| --- | --- |
| [01 요구사항과 기존 프로그램 현황](01-requirements-and-current-state.md) | 확정 요구사항, 저장소 확인 결과, 설치 경로와 기존 업데이트 구조 |
| [02 설치기 전환](02-installer-migration.md) | InstallShield 대체, WiX와 Burn, 구버전 마이그레이션과 복구 |
| [03 버전별 패키지와 업로드](03-package-and-upload.md) | 버전 체계, 불변 패키지, 저장소, 검증과 공개 절차 |
| [04 사용자 그룹별 단계 배포](04-staged-rollout.md) | 4개 그룹, 대상 결정, 승인 기준, 배포 중지와 관측 |
| [05 Microsoft Store 배포](05-microsoft-store.md) | MSI·EXE 및 MSIX 경로 비교, 제출 조건, 자체 배포와의 관계 |
| [06 코드 서명과 SmartScreen](06-code-signing-and-smartscreen.md) | 서명 대상과 순서, 서비스 비용, 게시자, SmartScreen 한계 |
| [07 구현 순서와 검증 계획](07-implementation-and-validation.md) | 선행 결정, 구현 단계, 마이그레이션 및 배포 시험 |
| [08 버전별 패치](08-version-patching.md) | 기준 버전별 패치 선택, MSP, 누적 패치, 실패 복구 및 전체 설치 전환 |
## 설계 원칙
1. 테스트부터 일반 배포까지 같은 서명 완료 패키지를 사용한다.
2. 파일 업로드와 사용자 공개를 별도 상태로 관리한다.
3. 기존 설치 경로와 사용자 설정을 보존한다.
4. 설치 대상 버전은 서버에서 인증된 사용자·장치와 정책으로 결정한다.
5. 배포 중지, 설치 중 실패 복구, 설치 완료 후 장애 복구를 구분한다.
6. 코드 서명과 SmartScreen 평판을 구분한다. Azure 서명도 경고 면제를 보장하지 않는다.
7. 버전별 패치를 필수 지원하며, 전체 설치 파일도 함께 유지한다. 적용 가능한 기준 버전과 설치 상태를 검증한 뒤 패치를 실행한다.
## 검토 범위
기존 코드와 `.ism`을 읽어 설치 및 업데이트 구조를 확인했다. 새 설치기 구현, 인증서 발급, 서명, Store 제출, 실제 설치·제거 시험은 수행하지 않았다. 특히 기존 `BaronSoftware.Update` NuGet 패키지는 호출부만 확인했으며 내부 구현을 검증한 상태가 아니다.
외부 서비스 가격과 Store 정책은 기준일에 확인한 공식 문서를 따른다. 계약·출시 시점에는 각 문서의 공식 링크에서 다시 확인한다.
+135
View File
@@ -0,0 +1,135 @@
<style>
#eg-preview { width:100%; }
#eg-preview .eg-app { --eg-bg:light-dark(#ffffff,#202329); --eg-rail:light-dark(#f5f7fa,#191d23); --eg-text:light-dark(#202c3b,#eff3f8); --eg-sub:light-dark(#637084,#aeb9c9); --eg-line:light-dark(#e1e6ed,#39414d); --eg-accent:light-dark(#2455d6,#96b5ff); --eg-on:light-dark(#ffffff,#14254d); --eg-soft:light-dark(#eef3ff,#27344d); --eg-green:light-dark(#257055,#85d7b6); color:var(--eg-text); background:var(--eg-bg); border:1px solid var(--eg-line); border-radius:12px; font:14px/1.6 'Segoe UI','Malgun Gothic',sans-serif; overflow-wrap:anywhere; box-shadow:0 8px 26px light-dark(#1324450c,#00000022); }
#eg-preview .eg-app * { box-sizing:border-box; }
#eg-preview .eg-titlebar { display:flex; align-items:center; gap:10px; min-height:52px; padding:12px 20px; border-bottom:1px solid var(--eg-line); padding-right:100px; }
#eg-preview .eg-mark { display:inline-grid; place-items:center; background:var(--eg-accent); color:var(--eg-on); width:27px; height:27px; border-radius:7px; font-size:15px; font-weight:600; flex-shrink:0; }
#eg-preview .eg-name { font-weight:600; letter-spacing:-.3px; }
#eg-preview .eg-demo { color:var(--eg-sub); font-size:11px; margin-left:4px; }
#eg-preview .eg-layout { display:grid; grid-template-columns:174px minmax(0,1fr); min-height:508px; }
#eg-preview .eg-side { background:var(--eg-rail); border-right:1px solid var(--eg-line); padding:28px 22px; display:flex; flex-direction:column; }
#eg-preview .eg-eyebrow { color:var(--eg-sub); font-size:11px; letter-spacing:1.6px; font-weight:600; }
#eg-preview .eg-side h2 { font-size:20px; margin:7px 0 28px; font-weight:600; letter-spacing:-.7px; }
#eg-preview .eg-steps { list-style:none; padding:0; margin:0; display:grid; gap:25px; }
#eg-preview .eg-steps li { display:flex; gap:10px; align-items:center; color:var(--eg-sub); font-size:12px; }
#eg-preview .eg-dot { display:inline-grid; place-items:center; width:24px; height:24px; border-radius:50%; border:1px solid var(--eg-line); flex-shrink:0; font-size:11px; }
#eg-preview .eg-steps li[aria-current=step] { color:var(--eg-accent); font-weight:600; }
#eg-preview .eg-steps li[aria-current=step] .eg-dot { background:var(--eg-accent); color:var(--eg-on); border-color:transparent; }
#eg-preview .eg-steps li.eg-past .eg-dot { color:var(--eg-green); background:var(--eg-bg); }
#eg-preview .eg-side-note { margin-top:auto; padding-top:32px; font-size:11px; color:var(--eg-sub); }
#eg-preview .eg-main { display:flex; flex-direction:column; min-width:0; }
#eg-preview .eg-content { padding:24px 30px 20px; flex:1; }
#eg-preview .eg-mode { display:flex; gap:4px; flex-wrap:wrap; margin-bottom:23px; }
#eg-preview .eg-mode button { background:var(--eg-rail); color:var(--eg-sub); border:0; padding:6px 12px; border-radius:5px; font:inherit; font-size:12px; }
#eg-preview .eg-mode button[aria-pressed=true] { background:var(--eg-soft); color:var(--eg-accent); font-weight:600; }
#eg-preview .eg-mode button:disabled { opacity:.6; }
#eg-preview .eg-heading { font-size:25px; line-height:1.4; font-weight:600; letter-spacing:-1px; margin:0 0 8px; }
#eg-preview .eg-description { color:var(--eg-sub); font-size:13px; margin:0 0 22px; }
#eg-preview .eg-box { border:1px solid var(--eg-line); border-radius:8px; padding:15px 17px; margin:14px 0; background:var(--eg-bg); }
#eg-preview .eg-box-top { display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap; margin-bottom:10px; }
#eg-preview .eg-box strong { font-weight:600; }
#eg-preview .eg-badge { font-size:11px; color:var(--eg-green); background:var(--eg-rail); padding:2px 8px; border-radius:4px; }
#eg-preview .eg-path { color:var(--eg-sub); font:12px/1.7 'Segoe UI','Malgun Gothic',sans-serif; overflow-wrap:anywhere; }
#eg-preview .eg-small { font-size:12px; color:var(--eg-sub); }
#eg-preview .eg-note { display:flex; gap:10px; padding:12px 14px; background:var(--eg-soft); color:var(--eg-accent); border-radius:7px; font-size:12px; margin-top:16px; }
#eg-preview .eg-note svg { flex-shrink:0; margin-top:2px; }
#eg-preview .eg-checks { list-style:none; padding:0; margin:12px 0 0; }
#eg-preview .eg-checks li { display:flex; justify-content:space-between; align-items:center; gap:12px; padding:8px 0; border-bottom:1px solid var(--eg-line); font-size:12px; }
#eg-preview .eg-checks li:last-child { border:0; }
#eg-preview .eg-status { color:var(--eg-green); font-size:11px; white-space:nowrap; }
#eg-preview .eg-footer { border-top:1px solid var(--eg-line); padding:15px 22px; display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
#eg-preview .eg-actions { display:flex; gap:8px; flex-wrap:wrap; margin-left:auto; }
#eg-preview .eg-button { display:inline-flex; align-items:center; justify-content:center; gap:8px; padding:8px 17px; border:1px solid var(--eg-line); background:var(--eg-bg); color:var(--eg-text); border-radius:6px; font:500 13px/1.5 'Segoe UI','Malgun Gothic',sans-serif; }
#eg-preview .eg-primary { border-color:transparent; background:var(--eg-accent); color:var(--eg-on); }
#eg-preview .eg-quiet { border-color:transparent; color:var(--eg-sub); padding-left:0; }
#eg-preview .eg-field { display:block; font-size:12px; color:var(--eg-sub); }
#eg-preview .eg-field input { display:block; width:100%; padding:10px; margin-top:6px; border:1px solid var(--eg-line); border-radius:5px; font:inherit; color:var(--eg-text); background:var(--eg-bg); }
#eg-preview .eg-agree { display:flex; align-items:flex-start; gap:8px; font-size:12px; margin-top:18px; }
#eg-preview input[type=checkbox] { accent-color:var(--eg-accent); margin:4px 0 0; }
#eg-preview .eg-meter { height:7px; border-radius:4px; background:var(--eg-rail); margin:17px 0 7px; overflow:hidden; }
#eg-preview .eg-meter span { display:block; height:100%; background:var(--eg-accent); }
#eg-preview .eg-progress-label { display:flex; justify-content:space-between; gap:10px; font-size:12px; color:var(--eg-sub); }
#eg-preview .eg-success { width:42px; height:42px; display:grid; place-items:center; color:var(--eg-green); background:var(--eg-rail); border-radius:50%; margin-bottom:18px; font-size:23px; }
#eg-preview .eg-alert { color:light-dark(#a43c27,#ffad98); font-size:12px; }
#eg-preview .eg-log { font-size:11px; color:var(--eg-sub); margin-top:13px; }
#eg-preview .eg-log summary { width:fit-content; }
#eg-preview .eg-log p { margin:8px 0; }
#eg-preview .eg-compact .eg-layout { display:flex; min-height:508px; }
#eg-preview .eg-compact .eg-main { width:100%; }
#eg-preview .eg-compact .eg-side { display:none; }
#eg-preview .eg-compact .eg-content { width:100%; max-width:530px; margin:auto; padding-top:25px; }
#eg-preview .eg-compact .eg-heading { font-size:29px; }
#eg-preview .eg-compact .eg-mode { margin-bottom:19px; }
#eg-preview .eg-compact .eg-footer { padding-left:30px; padding-right:30px; }
#eg-preview .eg-compact .eg-box { background:var(--eg-rail); border-color:transparent; }
#eg-preview .eg-compact .eg-badge { background:var(--eg-bg); }
#eg-preview .eg-mini-step { display:none; font-size:11px; color:var(--eg-accent); margin:0 0 7px; letter-spacing:.8px; }
#eg-preview .eg-compact .eg-mini-step { display:block; }
@media(max-width:580px) { #eg-preview .eg-layout { grid-template-columns:1fr; } #eg-preview .eg-side { padding:14px 18px; border-right:0; border-bottom:1px solid var(--eg-line); } #eg-preview .eg-side h2,#eg-preview .eg-side .eg-eyebrow,#eg-preview .eg-side-note { display:none; } #eg-preview .eg-steps { display:flex; justify-content:space-between; gap:7px; } #eg-preview .eg-steps li { gap:5px; font-size:11px; flex-direction:column; } #eg-preview .eg-content,#eg-preview .eg-compact .eg-content { padding:22px 18px; } #eg-preview .eg-heading,#eg-preview .eg-compact .eg-heading { font-size:23px; } #eg-preview .eg-footer,#eg-preview .eg-compact .eg-footer { padding:14px 18px; } #eg-preview .eg-titlebar { padding-left:16px; } }
@media(pointer:coarse) { #eg-preview button { min-height:44px; } #eg-preview .eg-field input { font-size:16px; } #eg-preview .eg-agree { min-height:44px; align-items:center; } }
</style>
<div id="eg-preview" class="viz-carousel viz-dotted-background" aria-label="EG-BIM 설치 화면 디자인" data-previous-label="이전 시안" data-next-label="다음 시안">
<section data-variant="단계형" aria-label="단계형 설치 디자인"><div id="eg-guided" class="eg-app" aria-label="단계형 설치 화면"></div></section>
<section data-variant="집중형" aria-label="집중형 설치 디자인" hidden><div id="eg-focused" class="eg-app eg-compact" aria-label="집중형 설치 화면"></div></section>
</div>
<script>
(() => {
const root = document.getElementById('eg-preview');
const definitions = [{id:'eg-guided', label:'단계형'}, {id:'eg-focused', label:'집중형'}];
const states = {};
const defaultPath = 'C:\\Program Files\\Baron\\EG-BIM';
const existingPath = 'D:\\Baron\\EG-BIM';
const escape = value => String(value).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const saved = window.openai?.widgetState?.privateContent?.installers || {};
const normalize = value => ({step:Number.isInteger(value?.step) && value.step>=0 && value.step<=3 ? value.step : 0, mode:value?.mode==='new'?'new':'upgrade', path:typeof value?.path==='string'?value.path:defaultPath, agreed:!!value?.agreed, launch:value?.launch!==false, error:false, finished:false});
definitions.forEach(d => states[d.id]=normalize(saved[d.id]));
function persist(id) {
const s=states[id];
window.openai?.setWidgetState?.({modelContent:{design:definitions.find(d=>d.id===id).label,mode:s.mode,screen:['설치 준비','설정 보호','설치 진행','완료'][s.step]},privateContent:{installers:states}})?.catch(()=>{});
}
function row(label,status='확인 완료') { return `<li><span>${label}</span><span class="eg-status">${status}</span></li>`; }
function content(s) {
const upgrade=s.mode==='upgrade';
if(s.step===0) return `<h1 class="eg-heading">${upgrade?'익숙한 작업 환경, 그대로.':'새로운 설계의 시작.'}</h1><p class="eg-description">${upgrade?'설정과 작업 환경을 보관한 뒤 EG-BIM을 업데이트합니다.':'EG-BIM을 설치하고 설계 작업을 시작하세요.'}</p>
${upgrade?`<div class="eg-box"><div class="eg-box-top"><strong>기존 EG-BIM을 찾았습니다</strong><span class="eg-badge">설치 경로 유지</span></div><div class="eg-path">${escape(existingPath)}</div><div class="eg-small">사용자 지정 설치 경로를 그대로 사용합니다.</div></div>`:`<div class="eg-box"><label class="eg-field">설치 위치<input data-path aria-label="설치 위치" value="${escape(s.path)}"></label><div class="eg-small">프로그램과 사용자 설정은 별도로 보관됩니다.</div></div>`}
<ul class="eg-checks">${row('Windows 환경 및 필수 구성요소')}${row('설치 파일 · 서명 · 무결성')}${row('저장 공간 및 실행 중인 프로그램')}</ul>
${upgrade?'<div class="eg-note"><i data-lucide="shield-check" aria-hidden="true"></i><span>다음 단계에서 사용자 설정과 추가 리소스를 백업합니다.</span></div>':'<label class="eg-agree"><input type="checkbox" data-agree '+(s.agreed?'checked':'')+'> <span>사용권 계약에 동의합니다. <span class="eg-small">(시안용 항목)</span></span></label><details class="eg-log"><summary class="cursor-interaction">사용권 계약 미리보기</summary><p>실제 설치 화면에는 승인된 사용권 계약 전문이 표시됩니다.</p></details>'}
${s.error?'<p class="eg-alert" role="alert">설치 위치를 입력하고 사용권 계약에 동의해 주세요.</p>':''}`;
if(s.step===1) return `<h1 class="eg-heading">${upgrade?'작업 환경을 안전하게 보관합니다.':'설치 준비가 끝났습니다.'}</h1><p class="eg-description">${upgrade?'백업 파일 확인이 끝난 후에만 기존 제품을 변경합니다.':'아래 구성을 확인하고 설치를 진행하세요.'}</p>
<div class="eg-box"><div class="eg-box-top"><strong>${upgrade?'사용자 설정 백업':'설치 구성 확인'}</strong><span class="eg-badge">${upgrade?'검증 완료':'준비 완료'}</span></div><ul class="eg-checks">${upgrade?row('환경설정 · 개인 도구 모음','보관됨')+row('CUI · 사용자 추가 리소스','보관됨')+row('백업 파일 읽기 및 무결성','정상'):row('EG-BIM 프로그램 파일','전체 설치')+row('필수 런타임','필요 시 설치')+row('설치 범위','이 PC의 모든 사용자')}</ul></div>
<div class="eg-small">${upgrade?'백업 위치 · 제거 대상과 분리':'설치 위치'}</div><div class="eg-path">${escape(upgrade?'D:\\EG-BIM-Backup\\migration-demo':s.path)}</div>
<div class="eg-note"><i data-lucide="shield-check" aria-hidden="true"></i><span>${upgrade?'설정 복원과 설치 후 확인이 끝날 때까지 백업을 유지합니다.':'설치에 필요한 경우 Windows에서 관리자 권한을 요청합니다.'}</span></div>`;
if(s.step===2) return `<h1 class="eg-heading">EG-BIM을 설치하고 있습니다.</h1><p class="eg-description">${upgrade?'프로그램을 업데이트한 뒤 호환되는 설정을 복원합니다.':'프로그램 파일과 필요한 구성요소를 설치합니다.'}</p><div class="eg-meter" role="progressbar" aria-label="설치 진행률 예시" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100"><span style="width:64%"></span></div><div class="eg-progress-label"><span>프로그램 파일 설치 중</span><span>64%</span></div><ul class="eg-checks">${row('설치 환경 및 패키지 확인','완료')}${row(upgrade?'사용자 설정 백업':'필수 런타임 확인','완료')}${row('EG-BIM 프로그램 설치','진행 중')}${row(upgrade?'설정 복원 및 설치 상태 확인':'설치 상태 확인','대기')}</ul><details class="eg-log"><summary class="cursor-interaction">진행 내역</summary><p>환경 검사 완료 → 파일 검증 완료 → ${upgrade?'백업 검증 완료 → ':''}프로그램 설치 중<br>화면 검토를 위한 예시 상태입니다.</p></details><div class="eg-note"><i data-lucide="info" aria-hidden="true"></i><span>설치가 끝날 때까지 이 창을 유지해 주세요.</span></div>`;
return `<div class="eg-success" aria-hidden="true">✓</div><h1 class="eg-heading">설치가 완료되었습니다.</h1><p class="eg-description">${upgrade?'사용자 설정을 복원했습니다. 이어서 작업을 시작하세요.':'EG-BIM에서 첫 번째 프로젝트를 시작해 보세요.'}</p><div class="eg-box"><ul class="eg-checks">${row('프로그램 설치','완료')}${upgrade?row('호환되는 사용자 설정 복원','완료'):''}${row('설치 상태 확인','완료')}</ul><div class="eg-path">${escape(upgrade?existingPath:s.path)}</div></div>${upgrade?'<p class="eg-small">백업은 유지됩니다. 도면 열기·저장·출력과 인증·플러그인 동작을 확인한 후 정리할 수 있습니다.</p>':''}<label class="eg-agree"><input type="checkbox" data-launch ${s.launch?'checked':''}><span>완료 후 EG-BIM 실행</span></label>${s.finished?'<p class="eg-small" role="status">미리보기가 끝났습니다. 실제 프로그램은 실행하지 않습니다.</p>':''}`;
}
function render(id) {
const app=document.getElementById(id),s=states[id];
const names=['설치 준비',s.mode==='upgrade'?'설정 백업':'구성 확인','설치 진행','완료'];
app.innerHTML=`<header class="eg-titlebar"><span class="eg-mark">E</span><span class="eg-name">EG-BIM <span class="eg-demo">설치 시안</span></span></header><div class="eg-layout"><aside class="eg-side"><div class="eg-eyebrow">BARON SOFTWARE</div><h2>${s.mode==='upgrade'?'업데이트':'새로 설치'}</h2><ol class="eg-steps">${names.map((n,i)=>`<li ${i===s.step?'aria-current="step"':''} class="${i<s.step?'eg-past':''}"><span class="eg-dot">${i<s.step?'✓':i+1}</span>${n}</li>`).join('')}</ol><div class="eg-side-note">당신의 설계 환경을<br>다음 버전으로.</div></aside><main class="eg-main"><div class="eg-content"><div class="eg-mode" aria-label="설치 상황 미리보기"><button class="cursor-interaction" data-mode="upgrade" aria-pressed="${s.mode==='upgrade'}" ${s.step>0?'disabled':''}>기존 설치본 전환</button><button class="cursor-interaction" data-mode="new" aria-pressed="${s.mode==='new'}" ${s.step>0?'disabled':''}>신규 설치</button></div><p class="eg-mini-step">STEP ${s.step+1} / 4 · ${names[s.step]}</p><div aria-live="polite">${content(s)}</div></div><footer class="eg-footer"><span class="eg-small">미리보기 · 실제 변경 없음</span><div class="eg-actions">${s.step>0?'<button class="eg-button eg-quiet cursor-interaction" data-action="reset">처음으로</button>':''}<button class="eg-button eg-primary cursor-interaction" data-action="next">${['다음',s.mode==='upgrade'?'업데이트 시작':'설치 시작','완료 화면 보기',s.finished?'다시 살펴보기':'완료'][s.step]}<i data-lucide="arrow-right" aria-hidden="true"></i></button></div></footer></main></div>`;
app.querySelectorAll('[data-mode]').forEach(b=>b.addEventListener('click',()=>{s.mode=b.dataset.mode;s.error=false;render(id);persist(id);}));
app.querySelector('[data-path]')?.addEventListener('input',e=>{s.path=e.target.value;});
app.querySelector('[data-agree]')?.addEventListener('change',e=>{s.agreed=e.target.checked;persist(id);});
app.querySelector('[data-launch]')?.addEventListener('change',e=>{s.launch=e.target.checked;persist(id);});
app.querySelector('[data-action=reset]')?.addEventListener('click',()=>{s.step=0;s.finished=false;s.error=false;render(id);persist(id);});
app.querySelector('[data-action=next]').addEventListener('click',()=>{
if(s.step===0&&s.mode==='new'&&(!s.path.trim()||!s.agreed))s.error=true;
else if(s.step<3){s.step++;s.error=false;}
else if(s.finished){s.step=0;s.finished=false;}
else s.finished=true;
render(id);persist(id);
app.querySelector('[data-action=next]').focus({preventScroll:true});
});
if(globalThis.lucide)lucide.createIcons({attrs:{width:16,height:16}});
}
definitions.forEach(d=>render(d.id));
window.addEventListener('openai:set_globals',e=>{
const restored=e.detail?.globals?.widgetState?.privateContent?.installers;
if(restored)definitions.forEach(d=>{states[d.id]=normalize(restored[d.id]);render(d.id);});
});
if(globalThis.Tweak)definitions.forEach(d=>{
const app=document.getElementById(d.id),style={radius:12};
const tweak=new Tweak({container:app,onChange:()=>{app.style.borderRadius=style.radius+'px';}});
tweak.addSlider(style,'radius',{label:'창 모서리',min:0,max:24,unit:'px'});
});
})();
</script>
File diff suppressed because it is too large. Load diff