egovframe-scaffold-mcp 0.8.0 → 0.9.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 +4 -2
- package/dist/index.js +283 -35
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Claude, VS Code(Copilot), Cursor 등 MCP를 지원하는 AI 도구에서 **대
|
|
|
23
23
|
| `remove_egovframe_components` | 설치 매니페스트 기반 컴포넌트 제거 — 의존 컴포넌트 보호, `dryRun` 미리보기 |
|
|
24
24
|
| `validate_egovframe_project` | 조립 프로젝트 무결성 진단 — 파일 존재·DbType↔DB 스크립트 일치 |
|
|
25
25
|
| `get_egovframe_guide` | 컴포넌트의 공식 가이드 문서 조회 (egovframe-docs, 45종 매핑) |
|
|
26
|
-
| `add_ai_components` |
|
|
26
|
+
| `add_ai_components` | 공식 [egovframe-ai-rag](https://github.com/eGovFramework/egovframe-ai-rag) 샘플 기반 AI RAG 챗봇 조립 — Spring AI(Redis Stack)·LangChain4j(PGVector) 스택 선택(상호 배타), 소스·설정(`application-ai.yml` 프로필)·UI·인프라 복사, **pom 누락 의존성만 마커 구간 삽입**(백업 생성, 제거 시 원복), 충돌 시 전체 거부, `dryRun` 미리보기 (설계: [docs/design-ai-components.md](docs/design-ai-components.md)) |
|
|
27
27
|
|
|
28
28
|
### create_egovframe_project 파라미터
|
|
29
29
|
|
|
@@ -107,11 +107,13 @@ Claude Desktop / Claude Code 설정 예 (`mcpServers`):
|
|
|
107
107
|
|
|
108
108
|
1. 공통컴포넌트 선택 설치 — **M3 완료(v0.4.0)**: 저장소 스캔 기반 카탈로그 자동 생성(`npm run generate:catalog`, 68종) + 조립 파이프라인. 남은 항목: 컴포넌트별 테이블 선별 DDL, 이름·의존성 큐레이션 확대(`catalog/overrides.json` 기여 환영) (설계: [docs/design-components-parameter.md](docs/design-components-parameter.md))
|
|
109
109
|
2. 실행환경 보일러플레이트(egovframe-msa 등) 템플릿 추가
|
|
110
|
-
3. AI 컴포넌트 조립(`add_ai_components`) — **
|
|
110
|
+
3. AI 컴포넌트 조립(`add_ai_components`) — **M2 완료(v0.9.0)**: 실조립(복사+pom 마커 병합+설정 프로필화+remove/validate 연동). M3: langchain4j 실검증 확대, 진단 확장(ONNX 모델 경로·docker 서비스 감지), 패키지 재배치 옵션 검토 (설계: [docs/design-ai-components.md](docs/design-ai-components.md))
|
|
111
111
|
4. 검증 후 eGovFramework 조직 공식 저장소 편입 제안
|
|
112
112
|
|
|
113
113
|
## 변경 이력
|
|
114
114
|
|
|
115
|
+
- **0.9.0** — AI 컴포넌트 조립 M2(실조립): `add_ai_components`가 실제로 조립합니다 — 소스(`com.example.chat`)·설정(`application-ai.yml` 프로필, 기존 설정 불변)·UI·인프라(`docker-compose.ai.yml`·`Dockerfile.ai`·`k8s/ai/`) 복사(전체 사전 충돌 검사·원자적 거부), pom에 누락 좌표만 마커 주석 구간으로 삽입(exclusions 보존, `pom.xml.bak-ai` 백업), 매니페스트 기록으로 `remove_egovframe_components`가 파일·pom 삽입분을 함께 원복(바이트 단위 복원 검증), `validate_egovframe_project`에 pom 마커 진단 추가, 통합 테스트(`npm run test:ai-assembly`) 추가.
|
|
116
|
+
|
|
115
117
|
- **0.8.0** — AI 컴포넌트 조립 M1: `add_ai_components` dryRun 미리보기(파일 복사 계획·pom 의존성 diff·부모 POM 호환성 게이트·스택 상호 배타 검사), AI 카탈로그(`catalog/ai-components.json`, egovframe-ai-rag 모듈 스캔 자동 생성 `npm run generate:ai-catalog`), `list_egovframe_components`에 AI 컴포넌트 노출, 오프라인 테스트(`npm run test:ai`) 추가.
|
|
116
118
|
|
|
117
119
|
- **0.7.0** — `get_egovframe_guide`: 컴포넌트 id로 표준프레임워크 공식 가이드 문서(egovframe-docs)를 조회. 카탈로그에 문서 매핑 자동 생성(`--docs`, 지배적 패키지 참조 기준 45종) 추가. 한글명 큐레이션 12→27종.
|
package/dist/index.js
CHANGED
|
@@ -506,6 +506,8 @@ export async function removeComponents(opts) {
|
|
|
506
506
|
fs.unlinkSync(target);
|
|
507
507
|
pruneEmptyDirs(path.dirname(target), projectDir);
|
|
508
508
|
}
|
|
509
|
+
if (entry.pom)
|
|
510
|
+
stripAiPomAdditions(projectDir, id);
|
|
509
511
|
delete manifest.components[id];
|
|
510
512
|
}
|
|
511
513
|
removed.push({ id, files: entry.files.length, sqlScripts: entry.sqlScripts.length });
|
|
@@ -533,6 +535,12 @@ export async function validateProject(opts) {
|
|
|
533
535
|
if (missing.length > 0)
|
|
534
536
|
warnings.push(`컴포넌트 '${id}'의 파일 ${missing.length}개가 없습니다 (수동 삭제 또는 이동 가능성)`);
|
|
535
537
|
components.push({ id, files: entry.files.length, missing: missing.length, missingSamples: missing.slice(0, 5) });
|
|
538
|
+
if (entry.pom) {
|
|
539
|
+
const pomPath = path.join(projectDir, "pom.xml");
|
|
540
|
+
const pomText = fs.existsSync(pomPath) ? fs.readFileSync(pomPath, "utf-8") : "";
|
|
541
|
+
if (!pomText.includes(`egovframe-scaffold-mcp:ai:${id}:deps:start`) && entry.pom.addedDeps.length > 0)
|
|
542
|
+
warnings.push(`컴포넌트 '${id}'의 pom 삽입 마커가 없습니다 (수동 편집 가능성) — 의존성 ${entry.pom.addedDeps.length}건 확인 필요`);
|
|
543
|
+
}
|
|
536
544
|
}
|
|
537
545
|
}
|
|
538
546
|
else {
|
|
@@ -584,7 +592,7 @@ export async function getGuide(componentId, docIndex = 0) {
|
|
|
584
592
|
return { componentId, docs, selected: sel, content, truncated };
|
|
585
593
|
}
|
|
586
594
|
/* ------------------------------------------------------------------ */
|
|
587
|
-
/* AI 컴포넌트 조립 (M1
|
|
595
|
+
/* AI 컴포넌트 조립 (M1 미리보기 + M2 실조립) — 설계: docs/design-ai-components.md */
|
|
588
596
|
/* 소스: eGovFramework/egovframe-ai-rag (Spring AI·LangChain4j RAG 샘플) */
|
|
589
597
|
/* ------------------------------------------------------------------ */
|
|
590
598
|
export const AI_STACKS = ["spring-ai", "langchain4j"];
|
|
@@ -608,14 +616,11 @@ export function loadAiCatalog() {
|
|
|
608
616
|
throw new Error(`AI 카탈로그 오류: '${c.id}'의 conflictsWith '${x}'가 카탈로그에 없습니다`);
|
|
609
617
|
return catalog;
|
|
610
618
|
}
|
|
611
|
-
/**
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
export async function planAiComponents(opts) {
|
|
617
|
-
if (opts.dryRun === false)
|
|
618
|
-
throw new Error("add_ai_components는 현재 dryRun 미리보기만 지원합니다 — 실제 조립은 M2(v0.9.0)에서 제공됩니다 (설계: docs/design-ai-components.md)");
|
|
619
|
+
/** pom 마커 주석 — 제거 시 이 구간만 걷어내 원복한다 */
|
|
620
|
+
const AI_POM_MARKER = (id, kind, pos) => `<!-- egovframe-scaffold-mcp:ai:${id}:${kind}:${pos} -->`;
|
|
621
|
+
const depCoord = (d) => `${d.groupId}:${d.artifactId}${d.version ? ":" + d.version : ""}${d.scope ? " (" + d.scope + ")" : ""}`;
|
|
622
|
+
/** 공통 게이트·의존성 diff·복사 그룹 계산 (dryRun/실조립 공용) */
|
|
623
|
+
function computeAiPlan(opts) {
|
|
619
624
|
if (!AI_STACKS.includes(opts.stack))
|
|
620
625
|
throw new Error(`stack은 ${AI_STACKS.join("|")} 중 하나여야 합니다: ${String(opts.stack)}`);
|
|
621
626
|
const catalog = loadAiCatalog();
|
|
@@ -630,14 +635,15 @@ export async function planAiComponents(opts) {
|
|
|
630
635
|
if (manifest.components[x])
|
|
631
636
|
throw new Error(`상호 배타 컴포넌트 '${x}'가 이미 설치되어 있습니다 — 두 AI 스택은 같은 패키지(com.example.chat)·UI 경로를 사용합니다. 먼저 remove_egovframe_components로 제거하세요`);
|
|
632
637
|
}
|
|
633
|
-
// 호환성 게이트: 부모 POM 좌표 확인 +
|
|
638
|
+
// 호환성 게이트: 부모 POM 좌표 확인 + 의존성/프로퍼티 diff
|
|
634
639
|
const warnings = [];
|
|
635
640
|
const pomPath = path.join(projectDir, "pom.xml");
|
|
636
641
|
const pomFound = fs.existsSync(pomPath);
|
|
637
642
|
let parentFound = null;
|
|
638
643
|
let parentOk = null;
|
|
639
|
-
const
|
|
644
|
+
const toAddDeps = [];
|
|
640
645
|
const alreadyPresent = [];
|
|
646
|
+
const toAddProps = {};
|
|
641
647
|
if (pomFound) {
|
|
642
648
|
const pom = fs.readFileSync(pomPath, "utf-8");
|
|
643
649
|
const pm = pom.match(/<parent>[\s\S]*?<\/parent>/);
|
|
@@ -653,17 +659,19 @@ export async function planAiComponents(opts) {
|
|
|
653
659
|
else if (parentFound !== comp.requires.parent)
|
|
654
660
|
warnings.push(`부모 POM 버전이 다릅니다(요구 ${reqV}, 발견 ${parentFound}) — BOM 관리 버전 차이를 확인하세요`);
|
|
655
661
|
for (const d of comp.mavenDependencies) {
|
|
656
|
-
const coord = `${d.groupId}:${d.artifactId}${d.version ? ":" + d.version : ""}${d.scope ? " (" + d.scope + ")" : ""}`;
|
|
657
662
|
if (pom.includes(`<artifactId>${d.artifactId}</artifactId>`))
|
|
658
|
-
alreadyPresent.push(
|
|
663
|
+
alreadyPresent.push(depCoord(d));
|
|
659
664
|
else
|
|
660
|
-
|
|
665
|
+
toAddDeps.push(d);
|
|
661
666
|
}
|
|
667
|
+
for (const [k, v] of Object.entries(comp.mavenProperties))
|
|
668
|
+
if (!pom.includes(`<${k}>`))
|
|
669
|
+
toAddProps[k] = v;
|
|
662
670
|
}
|
|
663
671
|
else {
|
|
664
672
|
warnings.push("pom.xml이 없습니다 — Boot 백엔드 프로젝트 루트 경로인지 확인하세요");
|
|
665
673
|
}
|
|
666
|
-
// 복사
|
|
674
|
+
// 복사 그룹: source·config 필수, ui/infra/tests는 옵션
|
|
667
675
|
const groups = ["source", "config"];
|
|
668
676
|
if (opts.includeUi !== false)
|
|
669
677
|
groups.push("ui");
|
|
@@ -671,30 +679,264 @@ export async function planAiComponents(opts) {
|
|
|
671
679
|
groups.push("infra");
|
|
672
680
|
if (opts.includeTests === true)
|
|
673
681
|
groups.push("tests");
|
|
674
|
-
const copyPlan = groups.map((g) => ({ group: g, files: comp.copyGroups[g].files, paths: comp.copyGroups[g].paths }));
|
|
675
|
-
const totalFiles = copyPlan.reduce((n, g) => n + g.files, 0);
|
|
676
682
|
return {
|
|
683
|
+
comp,
|
|
677
684
|
projectDir,
|
|
678
|
-
component: { id: comp.id, name: comp.name, vectorStore: comp.vectorStore },
|
|
679
685
|
compatibility: { pomFound, required: comp.requires.parent, parentFound, parentOk, warnings },
|
|
680
|
-
|
|
686
|
+
toAddDeps,
|
|
687
|
+
alreadyPresent,
|
|
688
|
+
toAddProps,
|
|
689
|
+
groups,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/** AI 컴포넌트 조립 계획 — dryRun 미리보기 (네트워크 불필요) */
|
|
693
|
+
export async function planAiComponents(opts) {
|
|
694
|
+
const p = computeAiPlan(opts);
|
|
695
|
+
const copyPlan = p.groups.map((g) => ({
|
|
696
|
+
group: g,
|
|
697
|
+
files: p.comp.copyGroups[g].files,
|
|
698
|
+
paths: p.comp.copyGroups[g].paths,
|
|
699
|
+
}));
|
|
700
|
+
return {
|
|
701
|
+
projectDir: p.projectDir,
|
|
702
|
+
component: { id: p.comp.id, name: p.comp.name, vectorStore: p.comp.vectorStore },
|
|
703
|
+
compatibility: p.compatibility,
|
|
704
|
+
dependencyChanges: { toAdd: p.toAddDeps.map(depCoord), alreadyPresent: p.alreadyPresent },
|
|
705
|
+
copyPlan,
|
|
706
|
+
totalFiles: copyPlan.reduce((n, g) => n + g.files, 0),
|
|
707
|
+
prerequisites: p.comp.prerequisites,
|
|
708
|
+
nextSteps: ["미리보기 모드입니다. 실제 조립하려면 dryRun 없이 다시 호출하세요."],
|
|
709
|
+
dryRun: true,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/** 프로세스 수명 동안 AI 샘플 zip을 1회만 내려받기 위한 캐시 */
|
|
713
|
+
let aiZipCache = null;
|
|
714
|
+
async function downloadAiZip(repo, branch) {
|
|
715
|
+
const key = `${repo}@${branch}`;
|
|
716
|
+
if (aiZipCache && aiZipCache.key === key)
|
|
717
|
+
return aiZipCache.zip;
|
|
718
|
+
const zipUrl = `https://codeload.github.com/${repo}/zip/${branch}`;
|
|
719
|
+
const res = await fetchWithTimeout(zipUrl, COMPONENTS_DOWNLOAD_TIMEOUT_MS);
|
|
720
|
+
if (!res.ok)
|
|
721
|
+
throw new Error(`AI 샘플 저장소 다운로드 실패 (${res.status}): ${zipUrl}`);
|
|
722
|
+
const zip = new AdmZip(Buffer.from(await res.arrayBuffer()));
|
|
723
|
+
aiZipCache = { key, zip };
|
|
724
|
+
return zip;
|
|
725
|
+
}
|
|
726
|
+
/** copyGroups 경로("src" 또는 "src -> dest")를 zip 상대경로 → 대상 상대경로 매핑으로 해석 */
|
|
727
|
+
function mapAiEntry(relInModule, groupPaths) {
|
|
728
|
+
for (const spec of groupPaths) {
|
|
729
|
+
const [src, dest] = spec.split("->").map((s) => s.trim());
|
|
730
|
+
const target = dest ?? src;
|
|
731
|
+
if (src.endsWith("/")) {
|
|
732
|
+
if (relInModule.startsWith(src))
|
|
733
|
+
return target + relInModule.slice(src.length);
|
|
734
|
+
}
|
|
735
|
+
else if (relInModule === src) {
|
|
736
|
+
return target;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
/** XML 의존성 블록 직렬화 (exclusions·scope·optional 보존) */
|
|
742
|
+
function depToXml(d, indent = " ") {
|
|
743
|
+
const i2 = indent + " ";
|
|
744
|
+
const lines = [`${indent}<dependency>`, `${i2}<groupId>${d.groupId}</groupId>`, `${i2}<artifactId>${d.artifactId}</artifactId>`];
|
|
745
|
+
if (d.version)
|
|
746
|
+
lines.push(`${i2}<version>${d.version}</version>`);
|
|
747
|
+
if (d.scope)
|
|
748
|
+
lines.push(`${i2}<scope>${d.scope}</scope>`);
|
|
749
|
+
if (d.optional)
|
|
750
|
+
lines.push(`${i2}<optional>true</optional>`);
|
|
751
|
+
if (d.exclusions?.length) {
|
|
752
|
+
lines.push(`${i2}<exclusions>`);
|
|
753
|
+
for (const e of d.exclusions)
|
|
754
|
+
lines.push(`${i2} <exclusion>`, `${i2} <groupId>${e.groupId}</groupId>`, `${i2} <artifactId>${e.artifactId}</artifactId>`, `${i2} </exclusion>`);
|
|
755
|
+
lines.push(`${i2}</exclusions>`);
|
|
756
|
+
}
|
|
757
|
+
lines.push(`${indent}</dependency>`);
|
|
758
|
+
return lines.join("\n");
|
|
759
|
+
}
|
|
760
|
+
/** dependencyManagement 밖의 프로젝트 직속 </dependencies> 위치를 찾는다 */
|
|
761
|
+
export function findProjectDependenciesClose(pom) {
|
|
762
|
+
const dmStart = pom.indexOf("<dependencyManagement>");
|
|
763
|
+
const dmEnd = pom.indexOf("</dependencyManagement>");
|
|
764
|
+
let idx = -1;
|
|
765
|
+
const re = /<\/dependencies>/g;
|
|
766
|
+
let m;
|
|
767
|
+
while ((m = re.exec(pom)) !== null) {
|
|
768
|
+
if (dmStart >= 0 && m.index > dmStart && m.index < dmEnd + 24)
|
|
769
|
+
continue; // dependencyManagement 내부
|
|
770
|
+
idx = m.index;
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
return idx;
|
|
774
|
+
}
|
|
775
|
+
export const AI_POM_BACKUP = "pom.xml.bak-ai";
|
|
776
|
+
/**
|
|
777
|
+
* AI 컴포넌트 실조립 (M2).
|
|
778
|
+
* - 파일 복사: 전체 사전 충돌 검사 후 하나라도 충돌하면 아무것도 쓰지 않고 거부 (원자적)
|
|
779
|
+
* - pom 병합: 누락 좌표만 마커 주석 구간으로 삽입(기존 항목 불변), 병합 전 pom.xml.bak-ai 백업
|
|
780
|
+
* - 설정 프로필화: application.yml → application-ai.yml 복사 (기존 설정 파일은 수정하지 않음)
|
|
781
|
+
* - 매니페스트 기록: remove_egovframe_components가 파일과 pom 삽입분을 함께 정리
|
|
782
|
+
*/
|
|
783
|
+
export async function addAiComponents(opts) {
|
|
784
|
+
if (opts.dryRun === true) {
|
|
785
|
+
const plan = await planAiComponents(opts);
|
|
786
|
+
return { ...plan, copiedFiles: 0, pomChanged: false, pomBackup: null };
|
|
787
|
+
}
|
|
788
|
+
const p = computeAiPlan(opts);
|
|
789
|
+
const { comp, projectDir } = p;
|
|
790
|
+
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory())
|
|
791
|
+
throw new Error(`대상 프로젝트 디렉터리가 없습니다: ${projectDir} — 먼저 create_egovframe_project로 생성하세요`);
|
|
792
|
+
if (!p.compatibility.pomFound)
|
|
793
|
+
throw new Error(`pom.xml이 없습니다: ${projectDir} — Boot 백엔드 프로젝트 루트에서 실행하세요`);
|
|
794
|
+
if (p.compatibility.parentOk === false)
|
|
795
|
+
throw new Error(`부모 POM 불일치: 요구 '${comp.requires.parent}', 발견 '${p.compatibility.parentFound ?? "없음"}' — egovframe-boot-starter-parent 기반 Boot 프로젝트만 지원합니다 (dryRun으로 진단 가능)`);
|
|
796
|
+
// ---- 다운로드 & 파일 계획 ----
|
|
797
|
+
const catalog = loadAiCatalog();
|
|
798
|
+
const zip = await downloadAiZip(catalog.source.repo, opts.ref ?? catalog.source.branch);
|
|
799
|
+
const entries = zip.getEntries().filter((e) => !e.isDirectory);
|
|
800
|
+
const rootPrefix = entries[0].entryName.split("/")[0] + "/";
|
|
801
|
+
const modPrefix = rootPrefix + comp.modulePath + "/";
|
|
802
|
+
const plan = [];
|
|
803
|
+
for (const e of entries) {
|
|
804
|
+
if (!e.entryName.startsWith(modPrefix))
|
|
805
|
+
continue;
|
|
806
|
+
const relInModule = e.entryName.slice(modPrefix.length);
|
|
807
|
+
for (const g of p.groups) {
|
|
808
|
+
const destRel = mapAiEntry(relInModule, comp.copyGroups[g].paths);
|
|
809
|
+
if (destRel !== null) {
|
|
810
|
+
plan.push({ entry: e, destRel, group: g });
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (plan.length === 0)
|
|
816
|
+
throw new Error(`조립할 파일을 찾지 못했습니다 — 카탈로그(${catalog.source.surveyedAt})와 저장소 구조가 달라졌을 수 있습니다. npm run generate:ai-catalog로 재생성하세요`);
|
|
817
|
+
// ---- 전체 사전 충돌 검사 (원자적 거부) ----
|
|
818
|
+
const conflicts = plan.filter((f) => fs.existsSync(path.join(projectDir, f.destRel))).map((f) => f.destRel);
|
|
819
|
+
if (conflicts.length > 0)
|
|
820
|
+
throw new Error(`기존 파일과 충돌하여 조립을 거부합니다 (${conflicts.length}개): ${conflicts.slice(0, 10).join(", ")}${conflicts.length > 10 ? " 외" : ""} — 아무 파일도 쓰지 않았습니다`);
|
|
821
|
+
const pomPath = path.join(projectDir, "pom.xml");
|
|
822
|
+
const backupPath = path.join(projectDir, AI_POM_BACKUP);
|
|
823
|
+
if ((p.toAddDeps.length > 0 || Object.keys(p.toAddProps).length > 0) && fs.existsSync(backupPath))
|
|
824
|
+
throw new Error(`pom 백업(${AI_POM_BACKUP})이 이미 있습니다 — 이전 조립 잔여물을 정리한 뒤 다시 시도하세요`);
|
|
825
|
+
// ---- 파일 복사 ----
|
|
826
|
+
for (const f of plan) {
|
|
827
|
+
const target = path.join(projectDir, f.destRel);
|
|
828
|
+
if (!target.startsWith(projectDir + path.sep))
|
|
829
|
+
throw new Error(`경로 이탈 감지: ${f.destRel}`);
|
|
830
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
831
|
+
fs.writeFileSync(target, f.entry.getData());
|
|
832
|
+
}
|
|
833
|
+
// ---- pom 병합 (마커 구간 삽입) ----
|
|
834
|
+
let pomChanged = false;
|
|
835
|
+
if (p.toAddDeps.length > 0 || Object.keys(p.toAddProps).length > 0) {
|
|
836
|
+
let pom = fs.readFileSync(pomPath, "utf-8");
|
|
837
|
+
fs.writeFileSync(backupPath, pom);
|
|
838
|
+
if (p.toAddDeps.length > 0) {
|
|
839
|
+
const close = findProjectDependenciesClose(pom);
|
|
840
|
+
if (close < 0)
|
|
841
|
+
throw new Error("pom.xml에서 <dependencies> 블록을 찾지 못했습니다");
|
|
842
|
+
const block = [
|
|
843
|
+
` ${AI_POM_MARKER(comp.id, "deps", "start")}`,
|
|
844
|
+
...p.toAddDeps.map((d) => depToXml(d)),
|
|
845
|
+
` ${AI_POM_MARKER(comp.id, "deps", "end")}`,
|
|
846
|
+
" ",
|
|
847
|
+
].join("\n");
|
|
848
|
+
pom = pom.slice(0, close) + block + pom.slice(close);
|
|
849
|
+
}
|
|
850
|
+
if (Object.keys(p.toAddProps).length > 0) {
|
|
851
|
+
const pClose = pom.indexOf("</properties>");
|
|
852
|
+
if (pClose < 0)
|
|
853
|
+
throw new Error("pom.xml에서 <properties> 블록을 찾지 못했습니다");
|
|
854
|
+
const block = [
|
|
855
|
+
` ${AI_POM_MARKER(comp.id, "props", "start")}`,
|
|
856
|
+
...Object.entries(p.toAddProps).map(([k, v]) => ` <${k}>${v}</${k}>`),
|
|
857
|
+
` ${AI_POM_MARKER(comp.id, "props", "end")}`,
|
|
858
|
+
" ",
|
|
859
|
+
].join("\n");
|
|
860
|
+
pom = pom.slice(0, pClose) + block + pom.slice(pClose);
|
|
861
|
+
}
|
|
862
|
+
fs.writeFileSync(pomPath, pom);
|
|
863
|
+
pomChanged = true;
|
|
864
|
+
}
|
|
865
|
+
// ---- 매니페스트 기록 ----
|
|
866
|
+
const manifest = readManifest(projectDir) ?? {
|
|
867
|
+
schemaVersion: 1,
|
|
868
|
+
source: { repo: catalog.source.repo, branch: catalog.source.branch },
|
|
869
|
+
components: {},
|
|
870
|
+
};
|
|
871
|
+
manifest.components[comp.id] = {
|
|
872
|
+
installedAt: new Date().toISOString(),
|
|
873
|
+
files: plan.map((f) => f.destRel),
|
|
874
|
+
sqlScripts: [],
|
|
875
|
+
pom: pomChanged
|
|
876
|
+
? { backup: AI_POM_BACKUP, addedDeps: p.toAddDeps.map((d) => d.artifactId), addedProps: Object.keys(p.toAddProps) }
|
|
877
|
+
: undefined,
|
|
878
|
+
};
|
|
879
|
+
writeManifest(projectDir, manifest);
|
|
880
|
+
const copyPlan = p.groups.map((g) => ({
|
|
881
|
+
group: g,
|
|
882
|
+
files: plan.filter((f) => f.group === g).length,
|
|
883
|
+
paths: comp.copyGroups[g].paths,
|
|
884
|
+
}));
|
|
885
|
+
return {
|
|
886
|
+
projectDir,
|
|
887
|
+
component: { id: comp.id, name: comp.name, vectorStore: comp.vectorStore },
|
|
888
|
+
compatibility: p.compatibility,
|
|
889
|
+
dependencyChanges: { toAdd: p.toAddDeps.map(depCoord), alreadyPresent: p.alreadyPresent },
|
|
681
890
|
copyPlan,
|
|
682
|
-
totalFiles,
|
|
891
|
+
totalFiles: plan.length,
|
|
892
|
+
copiedFiles: plan.length,
|
|
893
|
+
pomChanged,
|
|
894
|
+
pomBackup: pomChanged ? AI_POM_BACKUP : null,
|
|
683
895
|
prerequisites: comp.prerequisites,
|
|
684
896
|
nextSteps: [
|
|
685
|
-
"M1은 미리보기 전용입니다 — 실제 조립은 M2(v0.9.0)에서 제공됩니다",
|
|
686
897
|
"Ollama(>=0.17.1) 설치 및 LLM 모델 준비 (폐쇄망 절차는 egovframe-ai-rag README 참조)",
|
|
687
898
|
"ONNX 임베딩 모델 익스포트·배치",
|
|
688
|
-
|
|
899
|
+
`docker compose -f docker-compose.ai.yml up -d 로 벡터 저장소(${comp.vectorStore}) 기동`,
|
|
900
|
+
"spring.profiles.active=ai 로 애플리케이션 실행 (application-ai.yml 사용)",
|
|
901
|
+
"브라우저에서 채팅 UI 접속 (경로·포트는 application-ai.yml 참조)",
|
|
689
902
|
],
|
|
690
|
-
dryRun:
|
|
903
|
+
dryRun: false,
|
|
691
904
|
};
|
|
692
905
|
}
|
|
906
|
+
/** AI 컴포넌트의 pom 삽입분(마커 구간)을 걷어낸다 — remove 시 호출 */
|
|
907
|
+
export function stripAiPomAdditions(projectDir, componentId) {
|
|
908
|
+
const pomPath = path.join(projectDir, "pom.xml");
|
|
909
|
+
if (!fs.existsSync(pomPath))
|
|
910
|
+
return false;
|
|
911
|
+
let pom = fs.readFileSync(pomPath, "utf-8");
|
|
912
|
+
let changed = false;
|
|
913
|
+
for (const kind of ["deps", "props"]) {
|
|
914
|
+
const start = pom.indexOf(AI_POM_MARKER(componentId, kind, "start"));
|
|
915
|
+
const endMark = AI_POM_MARKER(componentId, kind, "end");
|
|
916
|
+
const end = pom.indexOf(endMark);
|
|
917
|
+
if (start >= 0 && end > start) {
|
|
918
|
+
// 마커 라인 앞 들여쓰기부터 end 마커 라인 끝(개행 포함)까지 제거
|
|
919
|
+
const lineStart = pom.lastIndexOf("\n", start) + 1;
|
|
920
|
+
let lineEnd = end + endMark.length;
|
|
921
|
+
while (lineEnd < pom.length && pom[lineEnd] !== "\n")
|
|
922
|
+
lineEnd++;
|
|
923
|
+
lineEnd++; // 개행 포함
|
|
924
|
+
pom = pom.slice(0, lineStart) + pom.slice(lineEnd);
|
|
925
|
+
changed = true;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (changed)
|
|
929
|
+
fs.writeFileSync(pomPath, pom);
|
|
930
|
+
const backup = path.join(projectDir, AI_POM_BACKUP);
|
|
931
|
+
if (fs.existsSync(backup))
|
|
932
|
+
fs.unlinkSync(backup);
|
|
933
|
+
return changed;
|
|
934
|
+
}
|
|
693
935
|
/* ------------------------------------------------------------------ */
|
|
694
936
|
/* MCP 서버 */
|
|
695
937
|
/* ------------------------------------------------------------------ */
|
|
696
938
|
export function buildServer() {
|
|
697
|
-
const server = new McpServer({ name: "egovframe-scaffold-mcp", version: "0.
|
|
939
|
+
const server = new McpServer({ name: "egovframe-scaffold-mcp", version: "0.9.0" });
|
|
698
940
|
server.tool("list_egovframe_templates", "사용 가능한 전자정부 표준프레임워크 프로젝트 템플릿 목록을 반환합니다.", {}, async () => ({
|
|
699
941
|
content: [
|
|
700
942
|
{
|
|
@@ -846,28 +1088,34 @@ export function buildServer() {
|
|
|
846
1088
|
return { content: [{ type: "text", text: head + r.content }] };
|
|
847
1089
|
});
|
|
848
1090
|
server.tool("add_ai_components", "공식 egovframe-ai-rag 샘플 기반 AI RAG 챗봇(문서 업로드→임베딩→하이브리드 검색→LLM 응답)을 기존 Boot 프로젝트에 조립합니다. " +
|
|
849
|
-
"
|
|
1091
|
+
"소스·설정(application-ai.yml 프로필)·UI·인프라를 복사하고 pom에 누락 의존성만 마커 구간으로 삽입합니다(백업 생성, 제거 시 원복). " +
|
|
1092
|
+
"기존 파일과 충돌하면 아무것도 쓰지 않고 거부합니다. dryRun=true로 먼저 미리볼 수 있습니다.", {
|
|
850
1093
|
projectDir: z.string().describe("대상 프로젝트 디렉터리(절대경로 권장). egovframe-boot-starter-parent 기반 Boot 프로젝트"),
|
|
851
1094
|
stack: z.enum(AI_STACKS).describe("AI 스택: spring-ai(Redis Stack) | langchain4j(PGVector). 상호 배타"),
|
|
852
|
-
includeInfra: z.boolean().default(true).describe("docker-compose·Dockerfile·k8s 복사
|
|
853
|
-
includeUi: z.boolean().default(true).describe("채팅 UI(chat.html·static) 복사
|
|
854
|
-
includeTests: z.boolean().default(false).describe("샘플 테스트 복사
|
|
855
|
-
|
|
1095
|
+
includeInfra: z.boolean().default(true).describe("docker-compose.ai.yml·Dockerfile.ai·k8s/ai 복사"),
|
|
1096
|
+
includeUi: z.boolean().default(true).describe("채팅 UI(chat.html·static) 복사"),
|
|
1097
|
+
includeTests: z.boolean().default(false).describe("샘플 테스트 복사"),
|
|
1098
|
+
ref: z.string().optional().describe("egovframe-ai-rag 브랜치/태그 (기본: 카탈로그 기준 브랜치)"),
|
|
1099
|
+
dryRun: z.boolean().default(false).describe("true면 복사·병합 없이 계획만 미리보기(네트워크 불필요)"),
|
|
856
1100
|
}, async (args) => {
|
|
857
|
-
const r = await
|
|
1101
|
+
const r = await addAiComponents(args);
|
|
858
1102
|
const c = r.compatibility;
|
|
1103
|
+
const head = r.dryRun
|
|
1104
|
+
? `🔍 AI 컴포넌트 조립 미리보기(dryRun): ${r.projectDir}`
|
|
1105
|
+
: `✅ AI 컴포넌트 조립 완료: ${r.projectDir}`;
|
|
859
1106
|
const text = [
|
|
860
|
-
|
|
1107
|
+
head,
|
|
861
1108
|
`- 컴포넌트: ${r.component.id} — ${r.component.name}`,
|
|
862
1109
|
`- 호환성: 부모 POM ${c.parentOk === true ? "일치" : c.parentOk === false ? "불일치" : "미확인"}` +
|
|
863
1110
|
` (요구 ${c.required}${c.parentFound ? ", 발견 " + c.parentFound : ""})`,
|
|
864
1111
|
...(c.warnings.length ? c.warnings.map((w) => ` ! ${w}`) : []),
|
|
865
|
-
`- pom 의존성: 추가 예정 ${r.dependencyChanges.toAdd.length}건` +
|
|
866
|
-
(r.dependencyChanges.alreadyPresent.length ? `, 이미 존재 ${r.dependencyChanges.alreadyPresent.length}건` : "")
|
|
1112
|
+
`- pom 의존성: ${r.dryRun ? "추가 예정" : "추가됨"} ${r.dependencyChanges.toAdd.length}건` +
|
|
1113
|
+
(r.dependencyChanges.alreadyPresent.length ? `, 이미 존재 ${r.dependencyChanges.alreadyPresent.length}건` : "") +
|
|
1114
|
+
(r.pomBackup ? ` (백업: ${r.pomBackup})` : ""),
|
|
867
1115
|
...r.dependencyChanges.toAdd.slice(0, 8).map((d) => ` + ${d}`),
|
|
868
1116
|
...(r.dependencyChanges.toAdd.length > 8 ? [` + … 외 ${r.dependencyChanges.toAdd.length - 8}건`] : []),
|
|
869
|
-
`- 복사 계획 (총 ${r.totalFiles}개 파일):`,
|
|
870
|
-
...r.copyPlan.map((g) => ` · ${g.group}: ${g.files}
|
|
1117
|
+
`- ${r.dryRun ? "복사 계획" : "복사 완료"} (총 ${r.totalFiles}개 파일):`,
|
|
1118
|
+
...r.copyPlan.map((g) => ` · ${g.group}: ${g.files}개`),
|
|
871
1119
|
`- 실행 전제: ${r.prerequisites.join(", ")}`,
|
|
872
1120
|
``,
|
|
873
1121
|
`다음 단계:`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "egovframe-scaffold-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "전자정부 표준프레임워크(eGovFrame) 프로젝트 스캐폴딩·공통컴포넌트 조립 MCP 서버 — create/search/add/validate/remove",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"generate:catalog": "node scripts/generate-catalog.mjs",
|
|
18
18
|
"prepublishOnly": "npm run build && npm run test:catalog && npm run test:ai",
|
|
19
19
|
"test:ai": "node test/ai.mjs",
|
|
20
|
-
"generate:ai-catalog": "node scripts/generate-ai-catalog.mjs"
|
|
20
|
+
"generate:ai-catalog": "node scripts/generate-ai-catalog.mjs",
|
|
21
|
+
"test:ai-assembly": "node test/ai-assembly.mjs"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
23
24
|
"node": ">=18"
|