egovframe-scaffold-mcp 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,11 +11,11 @@
11
11
  Claude, VS Code(Copilot), Cursor 등 MCP를 지원하는 AI 도구에서 **대화 중 즉시** 표준프레임워크
12
12
  프로젝트를 만들고 공통컴포넌트·AI 계층을 조립할 수 있습니다. 기존 프로젝트 진단, 리포트, 안전한 upstream 재동기화도 지원합니다.
13
13
 
14
- 현재 v0.26.0은 **도구 22종, 공식 템플릿 10종, 공통컴포넌트 카탈로그 190항목(리프 176종+그룹 14종)**을 제공합니다.
14
+ 현재 v0.27.0은 **도구 22종, 공식 템플릿 22종, 공통컴포넌트 카탈로그 190항목(리프 176종+그룹 14종)**을 제공합니다.
15
15
 
16
16
  ## 진행 현황 (2026-09-13)
17
17
 
18
- - **소스 기준**: v0.26.0(`package.json`), 도구 22종·공식 템플릿 10종·카탈로그 190항목.
18
+ - **소스 기준**: v0.27.0(`package.json`), 도구 22종·공식 템플릿 22종·카탈로그 190항목.
19
19
  - **안전성 기준선 완료**: [PR #7](https://github.com/EricSeokgon/egovframe-scaffold-mcp/pull/7)~[#16](https://github.com/EricSeokgon/egovframe-scaffold-mcp/pull/16)을 반영해 provenance·strict assertion·safe remove와 프로젝트 생성/레시피/직접 조립/AI 조립/업그레이드 transaction을 갖췄습니다.
20
20
  - **v0.22.0 추가**: `EGOVFRAME_ALLOWED_ROOTS`를 19개 도구 진입점에 적용해 `..`·symlink/junction 이탈을 차단하고, 실패 시 rollback 결과를 구조화해 반환합니다. [PR #16](https://github.com/EricSeokgon/egovframe-scaffold-mcp/pull/16)은 7파일(+340/−31), 로컬 16종 스위트 전체 통과 후 병합됐습니다.
21
21
  - **v0.23.0 추가**: `build_egovframe_project` — 생성한 프로젝트를 실제로 빌드(maven/gradle·mvnw/gradlew 자동 감지)하고 컴파일·테스트 오류를 파일/라인 단위로 구조화해 생성→검증 루프를 완성합니다. 타임아웃·로그 상한·허용 root·dryRun 포함. [PR #18](https://github.com/EricSeokgon/egovframe-scaffold-mcp/pull/18)은 4파일(+458/−3), 오프라인 40단언과 실제 spawn 경로를 검증했습니다.
@@ -24,6 +24,7 @@ Claude, VS Code(Copilot), Cursor 등 MCP를 지원하는 AI 도구에서 **대
24
24
  - **v0.25.2 추가**: 보안·안정성 보강 — `generate_egovframe_ci`의 `jdk` 입력 검증(생성 YAML 주입 차단), 빌드·테스트 타임아웃 시 프로세스 트리 종료(래퍼가 띄운 JVM 때문에 타임아웃이 걸려도 호출이 끝나지 않던 문제 해결), MCP handshake 서버 버전을 `package.json`과 일치, 의존성 갱신으로 `npm audit` 0건.
25
25
  - **v0.25.3 추가**: Linux·macOS에서 `npx egovframe-scaffold-mcp`(bin symlink 경유) 실행 시 서버가 기동하지 않고 종료되던 문제 수정, CI를 ubuntu·windows × Node 18·20·22 매트릭스로 확장.
26
26
  - **v0.26.0 추가**: `sync_egovframe_templates` + `catalog/templates.json` — 그동안 Initializr(프로젝트 22종 zip 카탈로그)·MCP(`TEMPLATES` 10종 저장소 조달)·Development(`wizards.xml` 설정 마법사)에 흩어져 있던 "어떤 공식 프로젝트가 있는가"를 하나의 스키마로 합쳤습니다. 매핑은 `catalog/template-mapping.json` 에서 큐레이션하고(자동 추론 없음), upstream 과의 추가·삭제·변경과 MCP 커버리지 격차(현재 22종 중 9종 대응)를 도구로 조회합니다. 오프라인 148단언(`npm run test:template-catalog`).
27
+ - **v0.27.0 추가**: 공식 템플릿 커버리지 10 → **22종**(Initializr 22종 기준 대응 9 → 21종). 단독 저장소가 없는 배치 6종·빈 골격(`web`·`boot-web`)·모바일 2종·MSA 포털 2종을 Initializr 의 zip(Git LFS)으로 조달하며, commit 과 sha256·크기를 고정해 내려받은 바이트를 검증합니다. `sync_egovframe_templates` 가 zip 지문의 upstream 변화도 함께 보고합니다(설계: [docs/design-initializr-zip-templates.md](docs/design-initializr-zip-templates.md)).
27
28
  - **배포 상태**: npm 최신 배포 버전은 상단 npm 배지와 [npm 패키지 페이지](https://www.npmjs.com/package/egovframe-scaffold-mcp)를 단일 출처로 확인합니다. 이 문서의 버전 표기는 저장소 소스(`package.json`) 기준이며, git 태그 `vX.Y.Z`가 해당 배포본의 커밋을 가리킵니다.
28
29
 
29
30
  ## 제공 도구
@@ -51,18 +52,19 @@ Claude, VS Code(Copilot), Cursor 등 MCP를 지원하는 AI 도구에서 **대
51
52
  | `build_egovframe_project` | 생성한 프로젝트를 실제로 빌드(compile·test·package) — maven/gradle·mvnw/gradlew 자동 감지, 타임아웃·로그 상한, 컴파일 오류 파일/라인 구조화, dryRun |
52
53
  | `test_egovframe_project` | 테스트 실행 + JUnit XML 리포트(surefire·gradle) 구조화 — 스위트별 통과/실패/오류/건너뜀, 실패 케이스 메시지·예외 타입·테스트 파일/라인, `testFilter`, 이전 실행 리포트 제외, dryRun |
53
54
  | `generate_egovframe_crud` | 공식 Development CRUD wizard 입력 체계 기반 코드 생성 — VO·Mapper(XML)·Service·Controller·JSP(선택)·JUnit 5(선택), Classic/Boot 분기, 전체 충돌 사전 검사 |
54
- | `sync_egovframe_templates` | 공식 프로젝트 템플릿 통합 카탈로그(Initializr·MCP·Development) upstream 대조 — 추가/삭제/변경 항목과 MCP 커버리지 격차 보고, 네트워크 필요 |
55
+ | `sync_egovframe_templates` | 공식 프로젝트 템플릿 통합 카탈로그(Initializr·MCP·Development) upstream 대조 — 추가/삭제/변경 항목과 MCP 커버리지 격차, zip 조달 템플릿의 고정 지문(sha256·크기) 변화 보고, 네트워크 필요 |
55
56
 
56
57
  ### create_egovframe_project 파라미터
57
58
 
58
59
  - `projectName` — 프로젝트명(artifactId). 소문자·숫자·하이픈 (예: `my-egov-app`)
59
60
  - `groupId` — 자바 groupId (예: `egovframework.example`)
60
61
  - `database` — `hsql`(기본) | `mysql` | `oracle` | `altibase` | `tibero` (템플릿 `Globals.DbType` 지원 값)
61
- - `template` — `simple-backend`(기본, Spring Boot REST) | `simple-react` | `simple-homepage` | `portal-site` | `enterprise-business` | `web-sample` | `msa-edu` | `msa-common-components` | `mobile-device-api` | `ai-rag` (전체 10종, `list_egovframe_templates`로 확인)
62
+ - `template` — `simple-backend`(기본, Spring Boot REST) | `simple-react` | `simple-homepage` | `portal-site` | `enterprise-business` | `web-sample` | `msa-edu` | `msa-common-components` | `mobile-device-api` | `ai-rag` | `web` | `boot-web` | `batch-file-scheduler` | `batch-file-commandline` | `batch-file-web` | `batch-db-scheduler` | `batch-db-commandline` | `batch-db-web` | `mobile-web` | `mobile-common-components` | `msa-portal-backend` | `msa-portal-frontend` (전체 22종, `list_egovframe_templates`로 확인)
62
63
  - 레거시 템플릿(simple-homepage·portal-site·enterprise-business·web-sample)은 `egovProps/globals.properties`의 `Globals.DbType`에 DB 타입을 적용합니다.
63
64
  - `msa-edu`·`msa-common-components`·`mobile-device-api`·`ai-rag`는 멀티 프로젝트라 좌표·DB 자동 적용 없이 원본 그대로 생성하고 README 안내를 반환합니다.
65
+ - `web`부터 `msa-portal-frontend`까지 12종은 **Initializr zip 조달** 템플릿입니다. 고정 commit 의 zip 을 받아 sha256·크기를 검증한 뒤 `pom.xml` 의 `###GROUP_ID###` 등 자리표시자를 채우고, `globals.properties`(배치는 `egovframework/batch/properties/`)에 DB 타입을 적용합니다. `msa-portal-*` 2종은 멀티 프로젝트라 자동 적용이 없습니다.
64
66
  - `outputDir` — 생성 위치 상위 디렉터리
65
- - `ref` — (선택) 내려받을 브랜치/태그. 미지정 시 템플릿 기본 브랜치. 예: `main`, `v4.3.0`
67
+ - `ref` — (선택) 내려받을 브랜치/태그. 미지정 시 템플릿 기본 브랜치. 예: `main`, `v4.3.0` zip 조달 템플릿에 `ref`를 주면 고정 지문 검증을 건너뛰며 결과에 그 사실이 표시됩니다.
66
68
  - `dryRun` — (선택, 기본 `false`) `true`면 디스크에 쓰지 않고 생성 예정 파일 수·적용 설정만 미리보기
67
69
 
68
70
  동작: 공식 템플릿 zip 다운로드 → 압축 해제(zip-slip 방지) → `pom.xml`의 groupId/artifactId/name 적용(부모 POM 좌표는 유지) → `application.properties`의 `Globals.DbType` 설정, 프론트엔드 템플릿은 `package.json`의 `name` 설정. 기존 디렉터리가 있으면 거부합니다. 다운로드에는 30초 타임아웃이 적용되어 무응답 시 무한 대기하지 않습니다. `dryRun`으로 먼저 안전하게 미리볼 수 있습니다.
@@ -190,7 +192,8 @@ Claude Desktop / Claude Code 설정 예 (`mcpServers`):
190
192
  - 함수 레벨: 실제 템플릿(약 296파일) 생성, pom 좌표·DbType 적용, 중복 생성 거부, `dryRun` 미리보기(무기록) 확인 (`npm run smoke`)
191
193
  - Maven 좌표 레벨: 프로젝트 직접 groupId/artifactId/name만 변경하고 parent·dependency artifactId 보존 (`npm run test:pom`, 네트워크 불필요)
192
194
  - 카탈로그 레벨: schema v2, 고정 source/archive 지문, 자산 메타데이터, 무결성·위상 정렬·미리보기 검증 (`npm run test:catalog`, `npm run test:catalog-sync`, 네트워크 불필요)
193
- - 템플릿 카탈로그 레벨: `catalog/templates.json` 스키마·커버리지 계산·큐레이션 매핑 정합·변환기·upstream 차이 계산(추가/삭제/필드 변경) 검증 (`npm run test:template-catalog`, 148단언, 네트워크 불필요)
195
+ - zip 조달 레벨: 자리표시자 치환·globals 경로 판별·12종 정의 유효성(`npm run test:pom`, 네트워크 불필요), `batch-file-commandline` 실생성으로 지문 검증·좌표·DbType·zip 루트 보존, 지문 불일치 거부와 무기록, `ref` 지정 시 검증 생략 표시(`npm run test:templates`, 네트워크 필요, CI 실행)
196
+ - 템플릿 카탈로그 레벨: `catalog/templates.json` 스키마·커버리지 계산·큐레이션 매핑 정합·변환기·upstream 차이 계산(추가/삭제/필드 변경) 검증 (`npm run test:template-catalog`, 247단언, zip 지문 drift·LFS 포인터 해석 포함, 네트워크 불필요)
194
197
  - 동기화 레벨: 공식 v5.0.6 태그→commit, SHA-256·크기·파일 수, `sec.security`, 미매핑 경로 0건 검증 (`npm run test:catalog-sync-live`, 네트워크 필요)
195
198
  - 조립 레벨: 실제 공통컴포넌트 저장소로 bbs+login+sec.security(+cmm) 843파일 조립, message·IDGN·웹 자산·공용 fragment·Maven 좌표·선별 DB 스크립트·충돌 전체 거부·파일/SQL/매니페스트 fault-injection rollback·상위 symlink 경계 검증 (`npm run test:components`)
196
199
  - 수명주기 레벨: 설치 매니페스트 기록, 중복 설치 거부, 의존 컴포넌트 제거 보호, 제거·검증 동작 (`npm run test:components`)
@@ -213,11 +216,11 @@ Claude Desktop / Claude Code 설정 예 (`mcpServers`):
213
216
  - 카탈로그는 공식 v5.0.6 태그·commit·archive 지문에 고정됩니다. 최신 main 변경은 `sync_egovframe_catalog(ref="main")` 결과를 검토한 뒤 생성 스크립트로 승격합니다.
214
217
  - `build_egovframe_project`·`test_egovframe_project`는 로컬에 설치된 JDK와 maven/gradle(또는 프로젝트 래퍼)을 사용합니다. DB 등 외부 의존성이 필요한 테스트는 그 환경이 준비되지 않으면 오류(error)로 집계됩니다.
215
218
  - 자바 패키지 구조 변경(groupId에 맞춘 소스 디렉터리 이동)은 미지원입니다. 현재는 IDE rename refactoring을 권장합니다.
216
- - 템플릿·컴포넌트·가이드 원본을 받을 때 GitHub(`codeload.github.com`, `raw.githubusercontent.com`) 네트워크 접근이 필요합니다.
219
+ - 템플릿·컴포넌트·가이드 원본을 받을 때 GitHub(`codeload.github.com`, `raw.githubusercontent.com`, zip 조달 템플릿은 `media.githubusercontent.com`) 네트워크 접근이 필요합니다.
217
220
 
218
221
  ## 로드맵
219
222
 
220
- v0.26.0까지 프로젝트·CRUD 생성, 검증된 공통컴포넌트 실행 자산 조립, 안전성 기반(테스트 판정 강제·사용자 파일 보호·전 도구 트랜잭션·허용 root·구조화 rollback 보고), 생성→검증 루프(실제 빌드·오류 구조화·테스트 리포트 구조화), 그리고 공식 템플릿 카탈로그 단일화를 완료했습니다.
223
+ v0.27.0까지 프로젝트·CRUD 생성, 검증된 공통컴포넌트 실행 자산 조립, 안전성 기반(테스트 판정 강제·사용자 파일 보호·전 도구 트랜잭션·허용 root·구조화 rollback 보고), 생성→검증 루프(실제 빌드·오류 구조화·테스트 리포트 구조화), 그리고 공식 템플릿 카탈로그 단일화와 커버리지 확대(22종 중 21종)를 완료했습니다.
221
224
 
222
225
  | 버전 | 핵심 기능 | 목표 |
223
226
  |---|---|---|
@@ -228,9 +231,10 @@ v0.26.0까지 프로젝트·CRUD 생성, 검증된 공통컴포넌트 실행 자
228
231
  | **v0.24 완료** | 공식 템플릿 커버리지 확대 (7 → **10종**) | Initializr 카탈로그(22항목) 대조로 미커버 공식 자산을 식별해 `msa-common-components`(KRDS)·`mobile-device-api`·`ai-rag` 추가. 모두 멀티 프로젝트로 표시해 좌표/DB 자동 재작성을 건너뛰고 하위 모듈 참조를 보호하며, 실제 아카이브 다운로드 통합 테스트로 검증 |
229
232
  | **v0.25 완료** | `test_egovframe_project` | 테스트 실행 후 JUnit XML 리포트(surefire `target/surefire-reports`, gradle `build/test-results/test`)를 읽어 스위트·케이스 단위 집계, 실패 메시지·예외 타입·테스트 파일/라인, `testFilter`, 오래된 리포트 제외, exit 0이어도 리포트 실패면 실패 판정 |
230
233
  | **v0.26 완료** | IDE·Initializr·MCP 공통 카탈로그 | Initializr `templates-projects.json`(22종), MCP `TEMPLATES`(10종), Development `wizards.xml`(8카테고리)을 `schemaVersion: 1` 단일 스키마 `catalog/templates.json` 으로 통합. 변환기(`fromInitializr`·`fromMcpTemplates`)와 큐레이션 매핑(`catalog/template-mapping.json`), upstream 대조 도구 `sync_egovframe_templates` 로 커버리지 격차를 수작업 비교 없이 확인 |
231
- | **v0.27 후보** | `migrate_egovframe_namespace` | 3.x→4.x import·XML bean·빌드 좌표 전환. dryRun·백업·원자적 거부와 자동 변환 불가 API 보고 |
232
- | **v0.28 후보** | `check_egovframe_dependencies` + `security_patch_advisor` | 폐쇄망 최소 버전 규칙과 선택적 CVE 조회, CSRF·보안 설정·공식 패치 기준 점검 |
233
- | **v0.29+ 후보** | 접근성·배포·AI 컨텍스트 | 영문 응답/README, Homebrew·MCP Registry, `generate_agents_md`, 공식 템플릿 커버리지 확대 |
234
+ | **v0.27 완료** | 공식 템플릿 커버리지 확대 (10 → **22종**) | 통합 카탈로그가 계산한 미커버 13종 중 12종을 Initializr zip(Git LFS) 조달로 추가. commit·sha256·크기 고정과 다운로드 검증, pom 자리표시자 치환, 템플릿별 `globals.properties` 경로 대응, `sync_egovframe_templates` 의 zip 지문 drift 보고 |
235
+ | **v0.28 후보** | `migrate_egovframe_namespace` | 3.x→4.x import·XML bean·빌드 좌표 전환. dryRun·백업·원자적 거부와 자동 변환 불가 API 보고 |
236
+ | **v0.29 후보** | `check_egovframe_dependencies` + `security_patch_advisor` | 폐쇄망 최소 버전 규칙과 선택적 CVE 조회, CSRF·보안 설정·공식 패치 기준 점검 |
237
+ | **v0.30+ 후보** | 접근성·배포·AI 컨텍스트·설정 생성 | 영문 응답/README, Homebrew·MCP Registry, `generate_agents_md`, Initializr 설정 템플릿(datasource·transaction·cache 등 21종) 기반 `generate_egovframe_config` |
234
238
 
235
239
  로드맵 근거:
236
240
 
@@ -244,6 +248,7 @@ v0.26.0까지 프로젝트·CRUD 생성, 검증된 공통컴포넌트 실행 자
244
248
 
245
249
  ## 변경 이력
246
250
 
251
+ - **0.27.0** — 공식 템플릿 커버리지 확대(10 → **22종**, Initializr 22종 기준 대응 9 → 21종): v0.26.0 의 통합 카탈로그가 계산해 준 미커버 13종 가운데, 단독 GitHub 저장소 없이 Initializr 저장소의 zip(Git LFS, `templates/projects/examples/`)으로만 배포되는 12종을 추가했습니다 — `web`·`boot-web`(빈 골격), `batch-file-scheduler`·`batch-file-commandline`·`batch-file-web`·`batch-db-scheduler`·`batch-db-commandline`·`batch-db-web`, `mobile-web`·`mobile-common-components`, `msa-portal-backend`·`msa-portal-frontend`(멀티 프로젝트). 브랜치는 움직이므로 Initializr **commit** 으로 다운로드 URL 을 고정하고 LFS 포인터의 **sha256·크기**로 내려받은 바이트를 검증하며, 다르면 아무것도 쓰지 않고 거부합니다(`ref` 를 직접 주면 검증을 건너뛰고 결과에 `archiveVerified: false` 와 경고를 남깁니다). zip 은 codeload 아카이브와 달리 최상위 폴더가 없어 루트를 잘라내지 않고, `pom.xml` 의 `###GROUP_ID###`·`###ARTIFACT_ID###`·`###NAME###`·`###VERSION###`·`###URL###` 자리표시자를 채운 뒤 기존 좌표 적용을 거칩니다. `Globals.DbType` 은 템플릿마다 다른 `globals.properties` 경로(배치는 `egovframework/batch/properties/`)를 찾아 적용합니다. `sync_egovframe_templates` 는 zip 본문 대신 LFS 포인터만 읽어 고정 지문과 대조한 `archivesChecked`·`archiveDrift` 를 보고하고, 통합 카탈로그의 `mcp.archive` 에 지문을 함께 싣습니다. 23MB 올인원인 `egov-template-common-components` 는 "공통컴포넌트는 `add_egovframe_components` 로 선택 조립한다"는 기존 큐레이션 결정에 따라 제외했습니다. 12종 전부 실생성해 자리표시자 0건을 확인했고 `batch-db-commandline`·`boot-web`·`mobile-web` 은 `mvn compile` 을 통과했습니다. zip 본문을 받기 위해 `media.githubusercontent.com` 접근이 추가로 필요합니다. 기존 10종·전체 도구 하위 호환. 로드맵의 후보 번호는 한 칸씩 뒤로 옮겼습니다(namespace 전환 v0.27 → v0.28 등).
247
252
  - **0.26.0** — 공식 템플릿 카탈로그 단일화: `sync_egovframe_templates` 추가(도구 21 → 22). 그동안 같은 사실이 세 곳에 따로 적혀 있었습니다 — Initializr 는 `templates/templates-projects.json` 에 프로젝트 22종을 zip·pom 스냅샷으로, MCP 는 `TEMPLATES`(현 `src/project.ts`)에 10종을 공식 저장소 조달 방식으로, Development 는 `eGovFrameTemplates/wizards.xml` 에 설정 스니펫 마법사 8카테고리를 담고 있었고, v0.24.0 의 커버리지 확대도 이 둘을 **사람이 눈으로 대조**해 진행했습니다. 이번에 `schemaVersion: 1` 의 `catalog/templates.json` 하나로 합쳐 프로젝트마다 Initializr 쪽 zip·pom 과 MCP 쪽 저장소·브랜치·멀티프로젝트 여부를 나란히 두고, 커버리지(22종 중 대응 9종·미커버 13종·MCP 단독 2종)를 계산된 값으로 기록합니다. 매핑은 `catalog/template-mapping.json` 에서 큐레이션하며 자동 추론하지 않습니다 — 예컨대 Initializr 의 `egov-web`(빈 웹 골격)과 MCP 의 `web-sample`(게시판 샘플)은 이름이 비슷해도 다른 산출물이라 대응시키지 않고 근거를 note 로 남겼습니다. 생성기 `scripts/generate-template-catalog.mjs` 는 매핑에 없는 upstream 항목을 만나면 실패해 조용한 누락을 막고, `sync_egovframe_templates` 는 upstream 을 내려받아 추가·삭제·변경(필드 단위)과 sha256 을 대조해 보고하되 파일을 고쳐 쓰지 않습니다. `list_egovframe_templates` 응답에도 커버리지 요약이 붙습니다(카탈로그가 없으면 기존 동작 유지). 오프라인 148단언(`npm run test:template-catalog`), 기존 21개 도구 하위 호환. 생성기는 Windows 에서도 동작하도록 `dist/index.js` 로드와 직접 실행 판정을 file URL·realpath 기준으로 처리합니다.
248
253
  - **0.25.3** — 기동 버그 수정 + CI 확장(도구 인터페이스 하위 호환).
249
254
  - **npx 기동 실패 수정(Linux·macOS)**: 진입점 판정이 `process.argv[1]`의 파일명 끝을 `import.meta.url`과 비교하는 방식이어서, npm이 POSIX에서 bin을 symlink(`node_modules/.bin/egovframe-scaffold-mcp → dist/index.js`)로 설치하면 진입점이 아니라고 판단해 서버를 띄우지 않고 오류 없이 종료했습니다. README가 안내하는 `npx -y egovframe-scaffold-mcp` 설정이 Linux·macOS에서 동작하지 않던 원인입니다(Windows는 `.cmd` shim이 `dist/index.js`를 직접 실행해 영향 없음, `node dist/index.js` 직접 실행도 영향 없음). 양쪽 경로를 realpath로 풀어 비교하도록 바꾸고, symlink 경유 기동을 회귀 테스트로 고정했습니다(`npm run test:handshake`).
@@ -1,13 +1,27 @@
1
1
  {
2
2
  "comment": "Initializr 프로젝트(projectName)와 MCP 템플릿(TEMPLATES 키)의 큐레이션 매핑. 같은 공식 산출물을 가리킬 때만 mcpTemplate 을 채우고, 성격이 다르면 null 로 두고 note 에 근거를 남긴다. scripts/generate-template-catalog.mjs 가 읽는다.",
3
3
  "mappings": {
4
- "egov-template-simple": { "mcpTemplate": "simple-homepage" },
5
- "egov-template-portal": { "mcpTemplate": "portal-site" },
6
- "egov-template-enterprise": { "mcpTemplate": "enterprise-business" },
7
- "egov-boot-simple-backend": { "mcpTemplate": "simple-backend" },
8
- "egov-boot-simple-frontend": { "mcpTemplate": "simple-react" },
9
- "egov-msa-boot-common-components": { "mcpTemplate": "msa-common-components" },
10
- "egov-mobile-deviceapi": { "mcpTemplate": "mobile-device-api" },
4
+ "egov-template-simple": {
5
+ "mcpTemplate": "simple-homepage"
6
+ },
7
+ "egov-template-portal": {
8
+ "mcpTemplate": "portal-site"
9
+ },
10
+ "egov-template-enterprise": {
11
+ "mcpTemplate": "enterprise-business"
12
+ },
13
+ "egov-boot-simple-backend": {
14
+ "mcpTemplate": "simple-backend"
15
+ },
16
+ "egov-boot-simple-frontend": {
17
+ "mcpTemplate": "simple-react"
18
+ },
19
+ "egov-msa-boot-common-components": {
20
+ "mcpTemplate": "msa-common-components"
21
+ },
22
+ "egov-mobile-deviceapi": {
23
+ "mcpTemplate": "mobile-device-api"
24
+ },
11
25
  "egov-ai-rag-springai": {
12
26
  "mcpTemplate": "ai-rag",
13
27
  "note": "MCP 의 ai-rag 는 spring-ai·langchain4j 두 모듈을 함께 담은 멀티 프로젝트라 Initializr 의 두 항목이 같은 템플릿에 대응한다"
@@ -17,27 +31,60 @@
17
31
  "note": "MCP 의 ai-rag 는 spring-ai·langchain4j 두 모듈을 함께 담은 멀티 프로젝트라 Initializr 의 두 항목이 같은 템플릿에 대응한다"
18
32
  },
19
33
  "egov-web": {
20
- "mcpTemplate": null,
21
- "note": "빈 웹 프로젝트 골격이다. MCP 의 web-sample(egovframe-web-sample)은 게시판 샘플이라 같은 산출물로 보지 않는다"
34
+ "mcpTemplate": "web",
35
+ "note": "빈 웹 프로젝트 골격 — Initializr zip 으로 조달한다. 게시판 샘플인 web-sample(egovframe-web-sample)과는 다른 산출물이라 따로 둔다"
36
+ },
37
+ "egov-boot-web": {
38
+ "mcpTemplate": "boot-web",
39
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
22
40
  },
23
- "egov-boot-web": { "mcpTemplate": null },
24
41
  "egov-template-common-components": {
25
42
  "mcpTemplate": null,
26
43
  "note": "공통컴포넌트 올인원 프로젝트. MCP 는 템플릿 통째로 받는 대신 list_egovframe_components / add_egovframe_components 로 선택 조립한다"
27
44
  },
28
- "egov-msa-portal-backend": { "mcpTemplate": null },
29
- "egov-msa-portal-frontend": { "mcpTemplate": null },
30
- "egov-mobile-web": { "mcpTemplate": null },
31
- "egov-mobile-common-components": { "mcpTemplate": null },
32
- "egov-boot-batch-scheduler-file": { "mcpTemplate": null },
33
- "egov-boot-batch-commandline-file": { "mcpTemplate": null },
34
- "egov-boot-batch-web-file": { "mcpTemplate": null },
35
- "egov-boot-batch-scheduler-db": { "mcpTemplate": null },
36
- "egov-boot-batch-commandline-db": { "mcpTemplate": null },
37
- "egov-boot-batch-web-db": { "mcpTemplate": null }
45
+ "egov-msa-portal-backend": {
46
+ "mcpTemplate": "msa-portal-backend",
47
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
48
+ },
49
+ "egov-msa-portal-frontend": {
50
+ "mcpTemplate": "msa-portal-frontend",
51
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
52
+ },
53
+ "egov-mobile-web": {
54
+ "mcpTemplate": "mobile-web",
55
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
56
+ },
57
+ "egov-mobile-common-components": {
58
+ "mcpTemplate": "mobile-common-components",
59
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
60
+ },
61
+ "egov-boot-batch-scheduler-file": {
62
+ "mcpTemplate": "batch-file-scheduler",
63
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
64
+ },
65
+ "egov-boot-batch-commandline-file": {
66
+ "mcpTemplate": "batch-file-commandline",
67
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
68
+ },
69
+ "egov-boot-batch-web-file": {
70
+ "mcpTemplate": "batch-file-web",
71
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
72
+ },
73
+ "egov-boot-batch-scheduler-db": {
74
+ "mcpTemplate": "batch-db-scheduler",
75
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
76
+ },
77
+ "egov-boot-batch-commandline-db": {
78
+ "mcpTemplate": "batch-db-commandline",
79
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
80
+ },
81
+ "egov-boot-batch-web-db": {
82
+ "mcpTemplate": "batch-db-web",
83
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
84
+ }
38
85
  },
39
86
  "mcpOnlyNotes": {
40
- "web-sample": "Initializr 의 egov-web 은 빈 골격이라 대응 항목으로 보지 않는다",
87
+ "web-sample": "게시판 샘플 저장소. Initializr 의 egov-web(빈 골격)은 MCP 의 web 템플릿에 대응한다",
41
88
  "msa-edu": "Initializr 카탈로그에 없는 MCP 단독 템플릿(클라우드 네이티브 교육용 멀티 프로젝트)"
42
89
  }
43
90
  }
@@ -21,14 +21,14 @@
21
21
  },
22
22
  "mcp": {
23
23
  "source": "src/project.ts TEMPLATES",
24
- "templates": 10
24
+ "templates": 22
25
25
  }
26
26
  },
27
27
  "coverage": {
28
28
  "initializrProjects": 22,
29
- "coveredByMcp": 9,
30
- "uncovered": 13,
31
- "mcpTemplates": 10,
29
+ "coveredByMcp": 21,
30
+ "uncovered": 1,
31
+ "mcpTemplates": 22,
32
32
  "mcpOnly": 2
33
33
  },
34
34
  "projects": [
@@ -41,9 +41,20 @@
41
41
  "fileName": "egovframe-web.zip",
42
42
  "pomFile": "egovframe-web-pom.xml"
43
43
  },
44
- "mcpTemplate": null,
45
- "mcp": null,
46
- "note": "빈 웹 프로젝트 골격이다. MCP 의 web-sample(egovframe-web-sample)은 게시판 샘플이라 같은 산출물로 보지 않는다"
44
+ "mcpTemplate": "web",
45
+ "mcp": {
46
+ "repository": "eGovFramework/egovframe-vscode-initializr",
47
+ "branch": "main",
48
+ "multiProject": false,
49
+ "archive": {
50
+ "kind": "initializr-zip",
51
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
52
+ "path": "templates/projects/examples/egovframe-web.zip",
53
+ "sha256": "13674f6f76e74e6d70478b7e9ad0689b0f84b5ffc9828a49f8b511dc2b3d6b69",
54
+ "bytes": 15547491
55
+ }
56
+ },
57
+ "note": "빈 웹 프로젝트 골격 — Initializr zip 으로 조달한다. 게시판 샘플인 web-sample(egovframe-web-sample)과는 다른 산출물이라 따로 둔다"
47
58
  },
48
59
  {
49
60
  "id": "egov-template-simple",
@@ -115,8 +126,20 @@
115
126
  "fileName": "egovframe-boot-web.zip",
116
127
  "pomFile": "egovframe-boot-web-pom.xml"
117
128
  },
118
- "mcpTemplate": null,
119
- "mcp": null
129
+ "mcpTemplate": "boot-web",
130
+ "mcp": {
131
+ "repository": "eGovFramework/egovframe-vscode-initializr",
132
+ "branch": "main",
133
+ "multiProject": false,
134
+ "archive": {
135
+ "kind": "initializr-zip",
136
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
137
+ "path": "templates/projects/examples/egovframe-boot-web.zip",
138
+ "sha256": "1284019f991f0676683dc1b19e9133318e50f18092505e0c1635ef1336bd1f6c",
139
+ "bytes": 15551650
140
+ }
141
+ },
142
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
120
143
  },
121
144
  {
122
145
  "id": "egov-boot-simple-backend",
@@ -175,8 +198,20 @@
175
198
  "fileName": "egovframe-msa-portal-backend.zip",
176
199
  "pomFile": null
177
200
  },
178
- "mcpTemplate": null,
179
- "mcp": null
201
+ "mcpTemplate": "msa-portal-backend",
202
+ "mcp": {
203
+ "repository": "eGovFramework/egovframe-vscode-initializr",
204
+ "branch": "main",
205
+ "multiProject": true,
206
+ "archive": {
207
+ "kind": "initializr-zip",
208
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
209
+ "path": "templates/projects/examples/egovframe-msa-portal-backend.zip",
210
+ "sha256": "dd8e7563c6eb981c4ad6641f68bf9473badccac10164f51c8357b217c64fdd1d",
211
+ "bytes": 1101491
212
+ }
213
+ },
214
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
180
215
  },
181
216
  {
182
217
  "id": "egov-msa-portal-frontend",
@@ -187,8 +222,20 @@
187
222
  "fileName": "egovframe-msa-portal-frontend.zip",
188
223
  "pomFile": null
189
224
  },
190
- "mcpTemplate": null,
191
- "mcp": null
225
+ "mcpTemplate": "msa-portal-frontend",
226
+ "mcp": {
227
+ "repository": "eGovFramework/egovframe-vscode-initializr",
228
+ "branch": "main",
229
+ "multiProject": true,
230
+ "archive": {
231
+ "kind": "initializr-zip",
232
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
233
+ "path": "templates/projects/examples/egovframe-msa-portal-frontend.zip",
234
+ "sha256": "022b3615d7140f05361f09ce54024284b3be072a0f6f355ecd6738c71bb9e8e0",
235
+ "bytes": 6562922
236
+ }
237
+ },
238
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
192
239
  },
193
240
  {
194
241
  "id": "egov-mobile-web",
@@ -199,8 +246,20 @@
199
246
  "fileName": "egovframe-mobile-web.zip",
200
247
  "pomFile": "egovframe-mobile-web-pom.xml"
201
248
  },
202
- "mcpTemplate": null,
203
- "mcp": null
249
+ "mcpTemplate": "mobile-web",
250
+ "mcp": {
251
+ "repository": "eGovFramework/egovframe-vscode-initializr",
252
+ "branch": "main",
253
+ "multiProject": false,
254
+ "archive": {
255
+ "kind": "initializr-zip",
256
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
257
+ "path": "templates/projects/examples/egovframe-mobile-web.zip",
258
+ "sha256": "b1ebcf172a775fada2418509a61b51914709eeb803ca6d03ef1bba766c9bc57b",
259
+ "bytes": 359099
260
+ }
261
+ },
262
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
204
263
  },
205
264
  {
206
265
  "id": "egov-mobile-common-components",
@@ -211,8 +270,20 @@
211
270
  "fileName": "egovframe-mobile-common-components.zip",
212
271
  "pomFile": "egovframe-mobile-common-components-pom.xml"
213
272
  },
214
- "mcpTemplate": null,
215
- "mcp": null
273
+ "mcpTemplate": "mobile-common-components",
274
+ "mcp": {
275
+ "repository": "eGovFramework/egovframe-vscode-initializr",
276
+ "branch": "main",
277
+ "multiProject": false,
278
+ "archive": {
279
+ "kind": "initializr-zip",
280
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
281
+ "path": "templates/projects/examples/egovframe-mobile-common-components.zip",
282
+ "sha256": "0c0abe6922042e940221ea05f94868dac293ba4d2315d17fdc16d26ef23b42c0",
283
+ "bytes": 10696748
284
+ }
285
+ },
286
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
216
287
  },
217
288
  {
218
289
  "id": "egov-mobile-deviceapi",
@@ -239,8 +310,20 @@
239
310
  "fileName": "egovframe-boot-batch-file-scheduler.zip",
240
311
  "pomFile": "egovframe-boot-batch-file-scheduler-pom.xml"
241
312
  },
242
- "mcpTemplate": null,
243
- "mcp": null
313
+ "mcpTemplate": "batch-file-scheduler",
314
+ "mcp": {
315
+ "repository": "eGovFramework/egovframe-vscode-initializr",
316
+ "branch": "main",
317
+ "multiProject": false,
318
+ "archive": {
319
+ "kind": "initializr-zip",
320
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
321
+ "path": "templates/projects/examples/egovframe-boot-batch-file-scheduler.zip",
322
+ "sha256": "407b09f3c678f8bd96c5da7aa38fe574622a1b507a676d3937c1a8cafafb95d1",
323
+ "bytes": 714016
324
+ }
325
+ },
326
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
244
327
  },
245
328
  {
246
329
  "id": "egov-boot-batch-commandline-file",
@@ -251,8 +334,20 @@
251
334
  "fileName": "egovframe-boot-batch-file-commandline.zip",
252
335
  "pomFile": "egovframe-boot-batch-file-commandline-pom.xml"
253
336
  },
254
- "mcpTemplate": null,
255
- "mcp": null
337
+ "mcpTemplate": "batch-file-commandline",
338
+ "mcp": {
339
+ "repository": "eGovFramework/egovframe-vscode-initializr",
340
+ "branch": "main",
341
+ "multiProject": false,
342
+ "archive": {
343
+ "kind": "initializr-zip",
344
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
345
+ "path": "templates/projects/examples/egovframe-boot-batch-file-commandline.zip",
346
+ "sha256": "c51352a2b4ce41ed64f0a0f543c5319164dcc16ac32f853bac44f1ee166535cc",
347
+ "bytes": 713252
348
+ }
349
+ },
350
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
256
351
  },
257
352
  {
258
353
  "id": "egov-boot-batch-web-file",
@@ -263,8 +358,20 @@
263
358
  "fileName": "egovframe-boot-batch-file-web.zip",
264
359
  "pomFile": "egovframe-boot-batch-file-web-pom.xml"
265
360
  },
266
- "mcpTemplate": null,
267
- "mcp": null
361
+ "mcpTemplate": "batch-file-web",
362
+ "mcp": {
363
+ "repository": "eGovFramework/egovframe-vscode-initializr",
364
+ "branch": "main",
365
+ "multiProject": false,
366
+ "archive": {
367
+ "kind": "initializr-zip",
368
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
369
+ "path": "templates/projects/examples/egovframe-boot-batch-file-web.zip",
370
+ "sha256": "15b75e445cfd9dd0162dbf23034937acb6cac4cd25af45cc162b55ef91a7a1c8",
371
+ "bytes": 1465409
372
+ }
373
+ },
374
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
268
375
  },
269
376
  {
270
377
  "id": "egov-boot-batch-scheduler-db",
@@ -275,8 +382,20 @@
275
382
  "fileName": "egovframe-boot-batch-db-scheduler.zip",
276
383
  "pomFile": "egovframe-boot-batch-db-scheduler-pom.xml"
277
384
  },
278
- "mcpTemplate": null,
279
- "mcp": null
385
+ "mcpTemplate": "batch-db-scheduler",
386
+ "mcp": {
387
+ "repository": "eGovFramework/egovframe-vscode-initializr",
388
+ "branch": "main",
389
+ "multiProject": false,
390
+ "archive": {
391
+ "kind": "initializr-zip",
392
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
393
+ "path": "templates/projects/examples/egovframe-boot-batch-db-scheduler.zip",
394
+ "sha256": "b0cb160b252325eb6c29c93d978a5026913d377339e15c5551f615ca0cd2b7e7",
395
+ "bytes": 712628
396
+ }
397
+ },
398
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
280
399
  },
281
400
  {
282
401
  "id": "egov-boot-batch-commandline-db",
@@ -287,8 +406,20 @@
287
406
  "fileName": "egovframe-boot-batch-db-commandline.zip",
288
407
  "pomFile": "egovframe-boot-batch-db-commandline-pom.xml"
289
408
  },
290
- "mcpTemplate": null,
291
- "mcp": null
409
+ "mcpTemplate": "batch-db-commandline",
410
+ "mcp": {
411
+ "repository": "eGovFramework/egovframe-vscode-initializr",
412
+ "branch": "main",
413
+ "multiProject": false,
414
+ "archive": {
415
+ "kind": "initializr-zip",
416
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
417
+ "path": "templates/projects/examples/egovframe-boot-batch-db-commandline.zip",
418
+ "sha256": "0de452a317f939f37867f66d293d8e6879fd868aa89c3ad1cc7a07c2b1c0ffd9",
419
+ "bytes": 1440218
420
+ }
421
+ },
422
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
292
423
  },
293
424
  {
294
425
  "id": "egov-boot-batch-web-db",
@@ -299,8 +430,20 @@
299
430
  "fileName": "egovframe-boot-batch-db-web.zip",
300
431
  "pomFile": "egovframe-boot-batch-db-web-pom.xml"
301
432
  },
302
- "mcpTemplate": null,
303
- "mcp": null
433
+ "mcpTemplate": "batch-db-web",
434
+ "mcp": {
435
+ "repository": "eGovFramework/egovframe-vscode-initializr",
436
+ "branch": "main",
437
+ "multiProject": false,
438
+ "archive": {
439
+ "kind": "initializr-zip",
440
+ "commit": "f8f572596e0c19d7992b132298992d5ce56ea819",
441
+ "path": "templates/projects/examples/egovframe-boot-batch-db-web.zip",
442
+ "sha256": "e229849102dee90fe7605107d9a4122425b67ddf2aa1bbc41b01af42bbc9e0c9",
443
+ "bytes": 1470738
444
+ }
445
+ },
446
+ "note": "단독 공식 저장소가 없어 Initializr 의 zip(Git LFS)을 commit·sha256 고정으로 조달한다"
304
447
  },
305
448
  {
306
449
  "id": "egov-ai-rag-springai",
@@ -343,7 +486,7 @@
343
486
  "repository": "eGovFramework/egovframe-web-sample",
344
487
  "branch": "main",
345
488
  "multiProject": false,
346
- "note": "Initializr 의 egov-web 은 빈 골격이라 대응 항목으로 보지 않는다"
489
+ "note": "게시판 샘플 저장소. Initializr 의 egov-web(빈 골격)은 MCP 의 web 템플릿에 대응한다"
347
490
  },
348
491
  {
349
492
  "id": "msa-edu",
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import { fileURLToPath } from "node:url";
24
24
  import { buildServer } from "./server.js";
25
25
  // ── 공개 API — 모듈 분리 전과 동일한 표면을 유지한다 ──
26
26
  export { DOWNLOAD_TIMEOUT_MS, COMPONENTS_DOWNLOAD_TIMEOUT_MS } from "./shared.js";
27
- export { TEMPLATES, GLOBALS_PROPS_REL, DB_TYPES, customizePomCoordinates, createProject } from "./project.js";
27
+ export { TEMPLATES, GLOBALS_PROPS_REL, DB_TYPES, INITIALIZR_REPO, INITIALIZR_COMMIT, archiveDownloadUrl, applyPomPlaceholders, isGlobalsPropertiesPath, customizePomCoordinates, createProject } from "./project.js";
28
28
  export { loadCatalog, resolveComponents, searchComponents } from "./catalog.js";
29
29
  export { MANIFEST_FILE, readManifest } from "./manifest.js";
30
30
  export { AI_STACKS, loadAiCatalog, planAiComponents, findProjectDependenciesClose, AI_POM_BACKUP, addAiComponents, stripAiPomAdditions } from "./ai.js";
@@ -43,7 +43,7 @@ export { ProjectFileTransaction, TransactionError, withDirectoryTransaction, wit
43
43
  export { ALLOWED_ROOTS_ENV, AllowedRootsError, assertPathAllowed, describeAllowedRoots, enforceAllowedRoots, loadAllowedRoots } from "./allowed-roots.js";
44
44
  export { detectBuildToolAt, resolveGoals, resolveCommand, parseBuildErrors, capOutput, defaultRunner, killProcessTree, KILL_GRACE_MS, runBuild } from "./build-runner.js";
45
45
  export { reportDirFor, resolveTestArgs, validateTestFilter, locateInStack, parseJUnitXml, snapshotReports, readJUnitReports, summarizeReports, runTests } from "./test-runner.js";
46
- export { loadTemplateCatalog, syncTemplateCatalog, diffProjects, normalizeInitializrProjects } from "./template-catalog.js";
46
+ export { loadTemplateCatalog, syncTemplateCatalog, diffProjects, normalizeInitializrProjects, parseLfsPointer } from "./template-catalog.js";
47
47
  /**
48
48
  * 이 파일이 실행 진입점인지 판정한다.
49
49
  *
package/dist/project.js CHANGED
@@ -3,7 +3,15 @@ import AdmZip from "adm-zip";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { withDirectoryTransaction } from "./file-transaction.js";
6
- import { DOWNLOAD_TIMEOUT_MS, fetchWithTimeout } from "./shared.js";
6
+ import { createHash } from "node:crypto";
7
+ import { COMPONENTS_DOWNLOAD_TIMEOUT_MS, DOWNLOAD_TIMEOUT_MS, fetchWithTimeout } from "./shared.js";
8
+ export const INITIALIZR_REPO = "eGovFramework/egovframe-vscode-initializr";
9
+ /** zip 지문을 조사한 Initializr commit (2026-09-18). 갱신 절차: docs/design-initializr-zip-templates.md */
10
+ export const INITIALIZR_COMMIT = "f8f572596e0c19d7992b132298992d5ce56ea819";
11
+ /** Git LFS 객체를 commit 기준으로 내려받는 URL. */
12
+ export function archiveDownloadUrl(repo, ref, filePath) {
13
+ return `https://media.githubusercontent.com/media/${repo}/${ref}/${filePath}`;
14
+ }
7
15
  /** 지원 템플릿 목록 (공식 eGovFramework 조직 저장소) */
8
16
  export const TEMPLATES = {
9
17
  "simple-backend": {
@@ -60,7 +68,176 @@ export const TEMPLATES = {
60
68
  description: "AI RAG 예제 (Spring AI · LangChain4j 2종 멀티 프로젝트, 좌표·DB 자동 적용 없음)",
61
69
  multiProject: true,
62
70
  },
71
+ // ── Initializr zip 조달 (v0.27.0) — 단독 저장소가 없는 공식 프로젝트 ──
72
+ "web": {
73
+ repo: INITIALIZR_REPO,
74
+ branch: "main",
75
+ description: "웹 프로젝트 빈 골격 (Spring MVC + JSP, 공통 설정만 포함 — 게시판 샘플이 필요하면 web-sample)",
76
+ archive: {
77
+ kind: "initializr-zip",
78
+ commit: INITIALIZR_COMMIT,
79
+ path: "templates/projects/examples/egovframe-web.zip",
80
+ sha256: "13674f6f76e74e6d70478b7e9ad0689b0f84b5ffc9828a49f8b511dc2b3d6b69",
81
+ bytes: 15547491,
82
+ },
83
+ },
84
+ "boot-web": {
85
+ repo: INITIALIZR_REPO,
86
+ branch: "main",
87
+ description: "Boot 웹 프로젝트 빈 골격 (Spring Boot 기반)",
88
+ archive: {
89
+ kind: "initializr-zip",
90
+ commit: INITIALIZR_COMMIT,
91
+ path: "templates/projects/examples/egovframe-boot-web.zip",
92
+ sha256: "1284019f991f0676683dc1b19e9133318e50f18092505e0c1635ef1336bd1f6c",
93
+ bytes: 15551650,
94
+ },
95
+ },
96
+ "batch-file-scheduler": {
97
+ repo: INITIALIZR_REPO,
98
+ branch: "main",
99
+ description: "Boot 배치 템플릿 — 파일(SAM) 기반, 스케줄러 실행",
100
+ archive: {
101
+ kind: "initializr-zip",
102
+ commit: INITIALIZR_COMMIT,
103
+ path: "templates/projects/examples/egovframe-boot-batch-file-scheduler.zip",
104
+ sha256: "407b09f3c678f8bd96c5da7aa38fe574622a1b507a676d3937c1a8cafafb95d1",
105
+ bytes: 714016,
106
+ },
107
+ },
108
+ "batch-file-commandline": {
109
+ repo: INITIALIZR_REPO,
110
+ branch: "main",
111
+ description: "Boot 배치 템플릿 — 파일(SAM) 기반, 커맨드라인 실행",
112
+ archive: {
113
+ kind: "initializr-zip",
114
+ commit: INITIALIZR_COMMIT,
115
+ path: "templates/projects/examples/egovframe-boot-batch-file-commandline.zip",
116
+ sha256: "c51352a2b4ce41ed64f0a0f543c5319164dcc16ac32f853bac44f1ee166535cc",
117
+ bytes: 713252,
118
+ },
119
+ },
120
+ "batch-file-web": {
121
+ repo: INITIALIZR_REPO,
122
+ branch: "main",
123
+ description: "Boot 배치 템플릿 — 파일(SAM) 기반, 웹 실행",
124
+ archive: {
125
+ kind: "initializr-zip",
126
+ commit: INITIALIZR_COMMIT,
127
+ path: "templates/projects/examples/egovframe-boot-batch-file-web.zip",
128
+ sha256: "15b75e445cfd9dd0162dbf23034937acb6cac4cd25af45cc162b55ef91a7a1c8",
129
+ bytes: 1465409,
130
+ },
131
+ },
132
+ "batch-db-scheduler": {
133
+ repo: INITIALIZR_REPO,
134
+ branch: "main",
135
+ description: "Boot 배치 템플릿 — DB 기반, 스케줄러 실행",
136
+ archive: {
137
+ kind: "initializr-zip",
138
+ commit: INITIALIZR_COMMIT,
139
+ path: "templates/projects/examples/egovframe-boot-batch-db-scheduler.zip",
140
+ sha256: "b0cb160b252325eb6c29c93d978a5026913d377339e15c5551f615ca0cd2b7e7",
141
+ bytes: 712628,
142
+ },
143
+ },
144
+ "batch-db-commandline": {
145
+ repo: INITIALIZR_REPO,
146
+ branch: "main",
147
+ description: "Boot 배치 템플릿 — DB 기반, 커맨드라인 실행",
148
+ archive: {
149
+ kind: "initializr-zip",
150
+ commit: INITIALIZR_COMMIT,
151
+ path: "templates/projects/examples/egovframe-boot-batch-db-commandline.zip",
152
+ sha256: "0de452a317f939f37867f66d293d8e6879fd868aa89c3ad1cc7a07c2b1c0ffd9",
153
+ bytes: 1440218,
154
+ },
155
+ },
156
+ "batch-db-web": {
157
+ repo: INITIALIZR_REPO,
158
+ branch: "main",
159
+ description: "Boot 배치 템플릿 — DB 기반, 웹 실행",
160
+ archive: {
161
+ kind: "initializr-zip",
162
+ commit: INITIALIZR_COMMIT,
163
+ path: "templates/projects/examples/egovframe-boot-batch-db-web.zip",
164
+ sha256: "e229849102dee90fe7605107d9a4122425b67ddf2aa1bbc41b01af42bbc9e0c9",
165
+ bytes: 1470738,
166
+ },
167
+ },
168
+ "mobile-web": {
169
+ repo: INITIALIZR_REPO,
170
+ branch: "main",
171
+ description: "모바일 웹 프로젝트 빈 골격",
172
+ archive: {
173
+ kind: "initializr-zip",
174
+ commit: INITIALIZR_COMMIT,
175
+ path: "templates/projects/examples/egovframe-mobile-web.zip",
176
+ sha256: "b1ebcf172a775fada2418509a61b51914709eeb803ca6d03ef1bba766c9bc57b",
177
+ bytes: 359099,
178
+ },
179
+ },
180
+ "mobile-common-components": {
181
+ repo: INITIALIZR_REPO,
182
+ branch: "main",
183
+ description: "모바일 공통컴포넌트 올인원 프로젝트",
184
+ archive: {
185
+ kind: "initializr-zip",
186
+ commit: INITIALIZR_COMMIT,
187
+ path: "templates/projects/examples/egovframe-mobile-common-components.zip",
188
+ sha256: "0c0abe6922042e940221ea05f94868dac293ba4d2315d17fdc16d26ef23b42c0",
189
+ bytes: 10696748,
190
+ },
191
+ },
192
+ "msa-portal-backend": {
193
+ repo: INITIALIZR_REPO,
194
+ branch: "main",
195
+ description: "MSA 포털 백엔드 (apigateway·config·discovery·서비스별 Gradle 멀티 프로젝트, 좌표·DB 자동 적용 없음)",
196
+ multiProject: true,
197
+ archive: {
198
+ kind: "initializr-zip",
199
+ commit: INITIALIZR_COMMIT,
200
+ path: "templates/projects/examples/egovframe-msa-portal-backend.zip",
201
+ sha256: "dd8e7563c6eb981c4ad6641f68bf9473badccac10164f51c8357b217c64fdd1d",
202
+ bytes: 1101491,
203
+ },
204
+ },
205
+ "msa-portal-frontend": {
206
+ repo: INITIALIZR_REPO,
207
+ branch: "main",
208
+ description: "MSA 포털 프론트엔드 (portal·admin 멀티 프로젝트, 좌표·DB 자동 적용 없음)",
209
+ multiProject: true,
210
+ archive: {
211
+ kind: "initializr-zip",
212
+ commit: INITIALIZR_COMMIT,
213
+ path: "templates/projects/examples/egovframe-msa-portal-frontend.zip",
214
+ sha256: "022b3615d7140f05361f09ce54024284b3be072a0f6f355ecd6738c71bb9e8e0",
215
+ bytes: 6562922,
216
+ },
217
+ },
63
218
  };
219
+ /** Initializr zip 의 pom.xml 이 쓰는 자리표시자. */
220
+ const POM_PLACEHOLDER_RE = /###(GROUP_ID|ARTIFACT_ID|NAME|VERSION|URL)###/g;
221
+ /** Initializr 방식 pom 자리표시자(###GROUP_ID### 등)를 실제 값으로 바꾼다. 자리표시자가 없으면 원문 그대로. */
222
+ export function applyPomPlaceholders(pom, groupId, projectName) {
223
+ const values = {
224
+ GROUP_ID: groupId,
225
+ ARTIFACT_ID: projectName,
226
+ NAME: projectName,
227
+ VERSION: "1.0.0",
228
+ URL: "https://www.egovframe.go.kr",
229
+ };
230
+ let replaced = 0;
231
+ const out = pom.replace(POM_PLACEHOLDER_RE, (_m, key) => {
232
+ replaced++;
233
+ return values[key];
234
+ });
235
+ return { pom: out, replaced };
236
+ }
237
+ /** zip 항목 중 DbType 설정을 가진 globals.properties 후보(템플릿마다 경로가 다르다). */
238
+ export function isGlobalsPropertiesPath(relPath) {
239
+ return /^src\/main\/resources\/.+\/globals\.properties$/.test(relPath);
240
+ }
64
241
  /** 레거시 템플릿의 DB 설정 파일 경로 */
65
242
  export const GLOBALS_PROPS_REL = "src/main/resources/egovframework/egovProps/globals.properties";
66
243
  /** 템플릿의 application.properties가 지원하는 DB 타입 */
@@ -117,17 +294,41 @@ export async function createProjectInternal(opts, transactionStagingPath) {
117
294
  const projectPath = path.resolve(opts.outputDir, opts.projectName);
118
295
  if (!dryRun && fs.existsSync(projectPath))
119
296
  throw new Error(`대상 디렉터리가 이미 존재합니다: ${projectPath}`);
120
- // 1) 공식 템플릿 다운로드 (branch/tag/SHA 모두 허용)
121
- const zipUrl = `https://codeload.github.com/${tpl.repo}/zip/${ref}`;
122
- const res = await fetchWithTimeout(zipUrl, DOWNLOAD_TIMEOUT_MS);
297
+ // 1) 공식 템플릿 다운로드
298
+ // - 저장소 템플릿: codeload 아카이브 (branch/tag/SHA 모두 허용)
299
+ // - zip 조달 템플릿: 고정 commit 의 LFS 객체 + sha256·크기 검증 (ref 를 직접 주면 검증 생략)
300
+ const archive = tpl.archive;
301
+ const effectiveRef = archive ? (opts.ref ? ref : archive.commit) : ref;
302
+ const zipUrl = archive
303
+ ? archiveDownloadUrl(tpl.repo, effectiveRef, archive.path)
304
+ : `https://codeload.github.com/${tpl.repo}/zip/${ref}`;
305
+ const res = await fetchWithTimeout(zipUrl, archive ? COMPONENTS_DOWNLOAD_TIMEOUT_MS : DOWNLOAD_TIMEOUT_MS);
123
306
  if (!res.ok)
124
- throw new Error(`템플릿 다운로드 실패 (${res.status}) — ref='${ref}'가 존재하는지 확인하세요: ${zipUrl}`);
307
+ throw new Error(`템플릿 다운로드 실패 (${res.status}) — ref='${effectiveRef}'가 존재하는지 확인하세요: ${zipUrl}`);
125
308
  const buf = Buffer.from(await res.arrayBuffer());
309
+ const customized = [];
310
+ let archiveVerified;
311
+ if (archive) {
312
+ if (opts.ref) {
313
+ archiveVerified = false;
314
+ customized.push(`⚠️ ref='${effectiveRef}' 지정 — 고정 지문(sha256) 검증을 건너뜁니다`);
315
+ }
316
+ else {
317
+ // shared.sha256() 은 매니페스트용 "sha256:" 접두 표기라, LFS oid 와 비교할 순수 hex 를 따로 계산한다.
318
+ const actual = createHash("sha256").update(buf).digest("hex");
319
+ if (buf.length !== archive.bytes || actual !== archive.sha256)
320
+ throw new Error(`템플릿 zip 지문이 고정값과 다릅니다 (${opts.template}) — 크기 ${buf.length}/${archive.bytes}, sha256 ${actual.slice(0, 12)}…/${archive.sha256.slice(0, 12)}…\n` +
321
+ `upstream 이 바뀌었는지 sync_egovframe_templates 로 확인하세요: ${zipUrl}`);
322
+ archiveVerified = true;
323
+ }
324
+ }
126
325
  const zip = new AdmZip(buf);
127
326
  const entries = zip.getEntries();
128
- const rootPrefix = entries[0].entryName.split("/")[0] + "/";
129
- const rel = (name) => (name.startsWith(rootPrefix) ? name.slice(rootPrefix.length) : name);
130
- const customized = [];
327
+ if (entries.length === 0)
328
+ throw new Error(`템플릿 zip 이 비어 있습니다: ${zipUrl}`);
329
+ // codeload 아카이브는 `<repo>-<ref>/` 최상위 폴더가 있지만, Initializr zip 은 프로젝트 루트가 곧 zip 루트다.
330
+ const rootPrefix = archive ? "" : entries[0].entryName.split("/")[0] + "/";
331
+ const rel = (name) => (rootPrefix && name.startsWith(rootPrefix) ? name.slice(rootPrefix.length) : name);
131
332
  // 2) 미리보기(dryRun): 쓰지 않고 수행 예정 내용만 계산
132
333
  if (dryRun) {
133
334
  const fileCount = entries.filter((e) => !e.isDirectory && rel(e.entryName)).length;
@@ -135,6 +336,11 @@ export async function createProjectInternal(opts, transactionStagingPath) {
135
336
  const hasProps = entries.some((e) => rel(e.entryName) === "src/main/resources/application.properties");
136
337
  const hasPkg = entries.some((e) => rel(e.entryName) === "package.json");
137
338
  const hasGlobals = entries.some((e) => rel(e.entryName) === GLOBALS_PROPS_REL);
339
+ const extraGlobals = archive
340
+ ? entries.map((e) => rel(e.entryName)).filter((r) => r !== GLOBALS_PROPS_REL && isGlobalsPropertiesPath(r))
341
+ : [];
342
+ if (archive && archiveVerified)
343
+ customized.push(`zip 지문 검증 통과 (sha256 ${archive.sha256.slice(0, 12)}…, ${archive.bytes} bytes)`);
138
344
  if (tpl.multiProject) {
139
345
  customized.push("멀티 프로젝트 템플릿 — 좌표/DB 설정 자동 적용 없음 (하위 프로젝트별 README 참조)");
140
346
  }
@@ -145,6 +351,8 @@ export async function createProjectInternal(opts, transactionStagingPath) {
145
351
  customized.push(`src/main/resources/application.properties (Globals.DbType=${opts.database}) — 적용 예정`);
146
352
  if (hasGlobals)
147
353
  customized.push(`${GLOBALS_PROPS_REL} (Globals.DbType=${opts.database}) — 적용 예정`);
354
+ for (const g of extraGlobals)
355
+ customized.push(`${g} (Globals.DbType=${opts.database}) — 파일에 설정이 있으면 적용 예정`);
148
356
  if (hasPkg)
149
357
  customized.push(`package.json (name=${opts.projectName}) — 적용 예정`);
150
358
  }
@@ -152,8 +360,9 @@ export async function createProjectInternal(opts, transactionStagingPath) {
152
360
  projectPath,
153
361
  filesExtracted: fileCount,
154
362
  customized,
155
- ref,
363
+ ref: effectiveRef,
156
364
  dryRun: true,
365
+ archiveVerified,
157
366
  nextSteps: [`미리보기 모드입니다. 실제 생성하려면 dryRun 없이 다시 호출하세요.`],
158
367
  };
159
368
  }
@@ -180,7 +389,9 @@ export async function createProjectInternal(opts, transactionStagingPath) {
180
389
  const pomPath = path.join(stagingPath, "pom.xml");
181
390
  if (!tpl.multiProject && fs.existsSync(pomPath)) {
182
391
  let pom = fs.readFileSync(pomPath, "utf-8");
183
- pom = customizePomCoordinates(pom, opts.groupId, opts.projectName);
392
+ // Initializr zip 은 좌표가 ###GROUP_ID### 같은 자리표시자다 — 먼저 채운 뒤 공통 좌표 적용을 거친다.
393
+ const filled = applyPomPlaceholders(pom, opts.groupId, opts.projectName);
394
+ pom = customizePomCoordinates(filled.pom, opts.groupId, opts.projectName);
184
395
  fs.writeFileSync(pomPath, pom);
185
396
  customized.push(`pom.xml (groupId=${opts.groupId}, artifactId/name=${opts.projectName})`);
186
397
  }
@@ -204,6 +415,24 @@ export async function createProjectInternal(opts, transactionStagingPath) {
204
415
  customized.push(`${GLOBALS_PROPS_REL} (Globals.DbType=${opts.database})`);
205
416
  }
206
417
  }
418
+ // 5c) zip 조달 템플릿: globals.properties 위치가 템플릿마다 다르다(batch/properties 등)
419
+ if (archive && !tpl.multiProject) {
420
+ for (const e of entries) {
421
+ if (e.isDirectory)
422
+ continue;
423
+ const r = rel(e.entryName);
424
+ if (r === GLOBALS_PROPS_REL || !isGlobalsPropertiesPath(r))
425
+ continue;
426
+ const target = path.join(stagingPath, r);
427
+ if (!target.startsWith(stagingPath + path.sep) || !fs.existsSync(target))
428
+ continue;
429
+ const props = fs.readFileSync(target, "utf-8");
430
+ if (!/^Globals\.DbType\s*=.*$/m.test(props))
431
+ continue;
432
+ fs.writeFileSync(target, props.replace(/^Globals\.DbType\s*=.*$/m, `Globals.DbType = ${opts.database}`));
433
+ customized.push(`${r} (Globals.DbType=${opts.database})`);
434
+ }
435
+ }
207
436
  // 6) package.json: 프론트엔드 템플릿의 프로젝트명 적용
208
437
  const pkgPath = path.join(stagingPath, "package.json");
209
438
  if (!tpl.multiProject && fs.existsSync(pkgPath)) {
@@ -237,19 +466,23 @@ export async function createProjectInternal(opts, transactionStagingPath) {
237
466
  else {
238
467
  count = await withDirectoryTransaction(path.resolve(opts.outputDir), opts.projectName, "프로젝트 생성", populate);
239
468
  }
469
+ if (archive && archiveVerified)
470
+ customized.unshift(`zip 지문 검증 통과 (sha256 ${archive.sha256.slice(0, 12)}…, ${archive.bytes} bytes)`);
240
471
  const buildStep = tpl.multiProject
241
472
  ? "README.md 참조 — backend/frontend/k8s/docker-compose 하위 프로젝트별 기동 안내"
242
473
  : opts.template === "simple-react"
243
474
  ? "npm install && npm start"
244
- : opts.template === "simple-backend"
245
- ? "mvn -B verify # 빌드/테스트 (JDK 17, hsql 외 DB는 접속정보를 application-*.properties에 설정)"
246
- : "mvn -B package # WAR 빌드 (DB 초기화는 DATABASE/ 또는 README의 스크립트 참조)";
475
+ : archive
476
+ ? "mvn -B package # 빌드 (실행 방식·DB 준비는 프로젝트의 readme·DATABASE/·설정 파일 참조)"
477
+ : opts.template === "simple-backend"
478
+ ? "mvn -B verify # 빌드/테스트 (JDK 17, hsql 외 DB는 접속정보를 application-*.properties에 설정)"
479
+ : "mvn -B package # WAR 빌드 (DB 초기화는 DATABASE/ 또는 README의 스크립트 참조)";
247
480
  const nextSteps = [
248
481
  `cd ${projectPath}`,
249
482
  buildStep,
250
483
  "자바 패키지 구조 변경(groupId에 맞춘 소스 이동)은 PoC 범위 밖입니다 — IDE의 rename refactoring 사용을 권장합니다.",
251
484
  ];
252
- return { projectPath, filesExtracted: count, customized, nextSteps, ref, dryRun: false };
485
+ return { projectPath, filesExtracted: count, customized, nextSteps, ref: effectiveRef, dryRun: false, archiveVerified };
253
486
  }
254
487
  /** 템플릿 zip 다운로드 → 압축 해제 → 사용자 값 적용 (dryRun이면 미리보기만) */
255
488
  export async function createProject(opts) {
package/dist/server.js CHANGED
@@ -127,6 +127,10 @@ export function buildServer() {
127
127
  ...(result.uncovered.length > 0
128
128
  ? [`- 미커버 목록: ${result.uncovered.map((project) => `${project.id}(${project.category})`).join(", ")}`]
129
129
  : []),
130
+ `- zip 조달 템플릿 지문: ${result.archivesChecked}종 대조, 차이 ${result.archiveDrift.length}건`,
131
+ ...result.archiveDrift.map((d) => d.error
132
+ ? ` - ${d.template}: 확인 실패 (${d.error})`
133
+ : ` - ${d.template}: sha256 ${d.pinnedSha256.slice(0, 12)}… → ${d.upstreamSha256} (${d.pinnedBytes} → ${d.upstreamBytes} bytes)`),
130
134
  ...result.warnings.map((warning) => `- 경고: ${warning}`),
131
135
  ].join("\n");
132
136
  return { content: [{ type: "text", text }] };
@@ -59,6 +59,16 @@ export function diffProjects(pinned, upstream) {
59
59
  }
60
60
  return drift.sort((a, b) => a.id.localeCompare(b.id));
61
61
  }
62
+ /** Git LFS 포인터 본문에서 oid·size 를 읽는다. 포인터가 아니면 null. */
63
+ export function parseLfsPointer(text) {
64
+ if (!/^version https:\/\/git-lfs\.github\.com\/spec\/v1\s*$/m.test(text))
65
+ return null;
66
+ const oid = /^oid sha256:([0-9a-f]{64})\s*$/m.exec(text);
67
+ const size = /^size (\d+)\s*$/m.exec(text);
68
+ if (!oid || !size)
69
+ return null;
70
+ return { sha256: oid[1], bytes: Number(size[1]) };
71
+ }
62
72
  async function fetchTextWithTimeout(url, timeoutMs) {
63
73
  const controller = new AbortController();
64
74
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -113,18 +123,44 @@ export async function syncTemplateCatalog(options = {}, deps = {}) {
113
123
  const uncovered = catalog.projects
114
124
  .filter((p) => !p.mcpTemplate)
115
125
  .map((p) => ({ id: p.id, category: p.category, displayName: p.displayName }));
126
+ // zip 조달 템플릿: raw 호스트가 돌려주는 LFS 포인터(작은 텍스트)만 읽어 고정 지문과 대조한다(zip 본문은 받지 않는다).
127
+ const pins = new Map();
128
+ for (const p of catalog.projects) {
129
+ if (p.mcpTemplate && p.mcp?.archive)
130
+ pins.set(p.mcpTemplate, { template: p.mcpTemplate, repository: p.mcp.repository, pin: p.mcp.archive });
131
+ }
132
+ const archiveDrift = [];
133
+ await Promise.all([...pins.values()].map(async ({ template, repository, pin }) => {
134
+ const pointerUrl = `https://raw.githubusercontent.com/${repository}/${ref}/${pin.path}`;
135
+ const base = { template, path: pin.path, pinnedSha256: pin.sha256, pinnedBytes: pin.bytes };
136
+ try {
137
+ const pointer = parseLfsPointer(await fetchText(pointerUrl, TEMPLATE_CATALOG_TIMEOUT_MS));
138
+ if (!pointer)
139
+ archiveDrift.push({ ...base, upstreamSha256: null, upstreamBytes: null, error: "LFS 포인터 형식이 아닙니다" });
140
+ else if (pointer.sha256 !== pin.sha256 || pointer.bytes !== pin.bytes)
141
+ archiveDrift.push({ ...base, upstreamSha256: pointer.sha256, upstreamBytes: pointer.bytes });
142
+ }
143
+ catch (error) {
144
+ archiveDrift.push({ ...base, upstreamSha256: null, upstreamBytes: null, error: error.message });
145
+ }
146
+ }));
147
+ archiveDrift.sort((a, b) => a.template.localeCompare(b.template));
148
+ if (archiveDrift.length > 0)
149
+ warnings.push("zip 조달 템플릿의 upstream 지문이 고정값과 다릅니다 — 고정 commit 의 zip 은 계속 받을 수 있으므로 생성은 동작합니다. 새 zip 을 검토한 뒤 src/project.ts 의 INITIALIZR_COMMIT·sha256·bytes 를 갱신하세요");
116
150
  return {
117
151
  repository: source.repository,
118
152
  path: source.path,
119
153
  requestedRef: ref,
120
154
  pinnedSha256: source.sha256,
121
155
  upstreamSha256,
122
- upToDate: drift.length === 0 && source.sha256 === upstreamSha256,
156
+ upToDate: drift.length === 0 && source.sha256 === upstreamSha256 && archiveDrift.length === 0,
123
157
  pinnedProjects: catalog.projects.length,
124
158
  upstreamProjects: upstream.length,
125
159
  drift,
126
160
  coverage: catalog.coverage,
127
161
  uncovered,
162
+ archivesChecked: pins.size,
163
+ archiveDrift,
128
164
  warnings,
129
165
  };
130
166
  }
@@ -0,0 +1,50 @@
1
+ # Initializr zip 조달 템플릿 설계 (v0.27.0)
2
+
3
+ ## 배경
4
+
5
+ v0.26.0의 통합 카탈로그(`catalog/templates.json`)는 공식 프로젝트 22종 가운데 MCP가 9종만 다룬다는 것을 계산값으로 보여 주었습니다. 남은 13종 가운데 12종은 단독 GitHub 저장소가 없고, eGovFrame VSCode Initializr 저장소 안에 zip으로만 배포됩니다.
6
+
7
+ - 위치: `eGovFramework/egovframe-vscode-initializr` 의 `templates/projects/examples/*.zip`
8
+ - 저장 방식: Git LFS. 저장소에는 `oid sha256:…`·`size …` 를 담은 포인터만 있고, 본문은 `media.githubusercontent.com` 에서 받습니다.
9
+ - 구조: zip 루트가 곧 프로젝트 루트입니다(codeload 아카이브와 달리 `<repo>-<ref>/` 최상위 폴더가 없음).
10
+ - 좌표: `pom.xml` 의 groupId·artifactId·name·version·url 이 `###GROUP_ID###` 같은 자리표시자입니다. Initializr 는 같은 내용의 pom 템플릿으로 덮어쓰는데, zip 안의 `pom.xml` 과 그 템플릿이 동일함을 확인했으므로 MCP 는 zip 의 `pom.xml` 에서 자리표시자만 채웁니다.
11
+
12
+ ## 결정
13
+
14
+ | 항목 | 결정 | 이유 |
15
+ |---|---|---|
16
+ | 조달 단위 | `TEMPLATES` 항목에 선택 필드 `archive` 추가 | 기존 10종(저장소 조달)의 동작과 타입을 그대로 둔다 |
17
+ | 고정 방식 | Initializr **commit** + LFS 포인터의 **sha256·bytes** | 브랜치는 움직인다. commit 으로 URL 을 고정하고 바이트까지 검증해 조사 시점과 같은 산출물임을 보장한다(공통컴포넌트 카탈로그의 v5.0.6 고정과 같은 원칙) |
18
+ | 검증 실패 | 아무 파일도 쓰지 않고 거부 | 검증은 압축 해제·transaction 시작 전에 끝난다 |
19
+ | `ref` 직접 지정 | 허용하되 지문 검증을 건너뛰고 결과에 `archiveVerified: false` 와 경고를 남긴다 | 최신 zip 을 시험해 볼 길은 열어 두되, 검증되지 않았음을 숨기지 않는다 |
20
+ | 멀티 프로젝트 | `msa-portal-backend`·`msa-portal-frontend` 는 `multiProject: true` | 하위 모듈 좌표를 건드리지 않는 기존 규칙과 동일 |
21
+ | DbType | `src/main/resources/**/globals.properties` 중 `Globals.DbType` 줄이 있는 파일에 적용 | 배치 템플릿은 `egovframework/batch/properties/`, 웹 템플릿은 `egovframework/egovProps/` 로 경로가 다르다 |
22
+ | 제외 | `egov-template-common-components`(23MB 올인원) | MCP 는 공통컴포넌트를 `add_egovframe_components` 로 선택 조립한다는 기존 큐레이션 결정을 유지 |
23
+
24
+ 추가된 템플릿 id: `web`, `boot-web`, `batch-file-scheduler`, `batch-file-commandline`, `batch-file-web`, `batch-db-scheduler`, `batch-db-commandline`, `batch-db-web`, `mobile-web`, `mobile-common-components`, `msa-portal-backend`, `msa-portal-frontend`. 커버리지는 22종 중 9 → 21종입니다.
25
+
26
+ ## upstream 변화 감지
27
+
28
+ `sync_egovframe_templates` 가 zip 본문을 받지 않고 **LFS 포인터(약 130바이트)** 만 `raw.githubusercontent.com` 에서 읽어 고정 지문과 대조합니다.
29
+
30
+ - 결과 필드: `archivesChecked`, `archiveDrift[]`(template·path·pinned/upstream sha256·bytes·error)
31
+ - 지문이 달라져도 **생성은 계속 동작**합니다. 다운로드 URL 이 고정 commit 을 가리키기 때문입니다. drift 는 "새 zip 이 나왔으니 검토하라"는 신호입니다.
32
+ - 포인터 하나를 읽지 못해도 동기화 전체가 실패하지 않고 항목별 `error` 로 보고합니다.
33
+
34
+ ## 고정값 갱신 절차
35
+
36
+ 1. `sync_egovframe_templates()` 를 실행해 `archiveDrift` 에 나온 템플릿과 upstream sha256·bytes 를 확인합니다.
37
+ 2. 새 zip 을 검토합니다(구조 변화, 자리표시자, `globals.properties` 경로).
38
+ 3. `src/project.ts` 의 `INITIALIZR_COMMIT` 을 새 commit 으로, 해당 항목의 `sha256`·`bytes` 를 포인터 값으로 바꿉니다. commit 은 전 항목이 공유하므로, 바뀌지 않은 zip 의 지문은 그대로 두면 됩니다(같은 객체).
39
+ 4. `npm run build && npm run generate:template-catalog` 로 `catalog/templates.json` 을 다시 만듭니다.
40
+ 5. `npm run test:pom && npm run test:template-catalog && npm run test:templates` 로 확인합니다.
41
+
42
+ ## 필요한 네트워크
43
+
44
+ 기존 `codeload.github.com`·`raw.githubusercontent.com` 에 더해 zip 본문을 받는 `media.githubusercontent.com` 이 필요합니다.
45
+
46
+ ## 검증
47
+
48
+ - 오프라인: 자리표시자 치환(5개·비자리표시자 `######` 보존·parent 보존), globals 경로 판별, URL 구성, 12종 정의 유효성(`npm run test:pom`), LFS 포인터 해석·지문 drift·포인터 오류 보고(`npm run test:template-catalog`)
49
+ - 네트워크: `batch-file-commandline` 실생성(지문 검증·좌표·DbType·zip 루트 보존), 지문 불일치 거부와 무기록, `ref` 지정 시 검증 생략 표시, `msa-portal-backend` dryRun(`npm run test:templates`)
50
+ - 수동: 12종 전부 실생성해 자리표시자 0건 확인, `batch-db-commandline`·`boot-web`·`mobile-web` 은 `mvn compile` 통과
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "egovframe-scaffold-mcp",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "전자정부 표준프레임워크(eGovFrame) 프로젝트·CRUD 스캐폴딩과 검증된 공통컴포넌트 완전 조립 MCP 서버",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",