@ninebone/mcp 0.1.30 → 1.0.1
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/bak/generate-menu.md +89 -0
- package/package.json +2 -1
- package/prompts/menu/generate-menu.md +28 -27
- package/prompts/source/generate-source-controller.md +7 -10
- package/prompts/source/generate-source-service.md +8 -11
- package/prompts/source/generate-source-ui-react.md +1 -1
- package/src/core/init.js +1 -0
- package/src/database/core/DatabaseManager.js +23 -0
- package/src/mcp/mcp-server.js +89 -0
- package/src/mcp/tools/generateSourceBrainTool.js +33 -2
- package/src/mcp/tools/modifySourceBrainTool.js +30 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Role
|
|
2
|
+
너는 시스템 아키텍트이자 데이터 모델러야. 사용자의 요구사항을 시스템 설계에 완벽하게 반영하는 전문가이지.
|
|
3
|
+
|
|
4
|
+
# Task
|
|
5
|
+
제공된 [전체 테이블 목록]과 [기존 Route 정보]를 분석하고, [사용자 추가 요청]을 반영하여 조건에 맞는 **"최종 통합 Route 리스트"**를 설계해줘.
|
|
6
|
+
|
|
7
|
+
# 사용자 추가 요청
|
|
8
|
+
{user_input}
|
|
9
|
+
|
|
10
|
+
# 기존 Route 정보
|
|
11
|
+
{routes}
|
|
12
|
+
|
|
13
|
+
# 전체 테이블 목록 (대조군)
|
|
14
|
+
{schema_summary}
|
|
15
|
+
|
|
16
|
+
# 분석 및 설계 규칙
|
|
17
|
+
|
|
18
|
+
1. **작업 분기 및 격리 규칙 (Action Isolation) [최우선 필수]**:
|
|
19
|
+
- 사용자의 요청({user_input})이 **오직 기존 메뉴에 대한 '삭제(DELETE)' 또는 '수정(UPDATE)' 명령으로만 이루어진 경우**, 오직 해당 변경 사항만을 엄격하게 반영한다.
|
|
20
|
+
- 이 경우, 시스템 안정성과 사용자의 제어 의도를 존중하기 위해, **'2번 규칙(전체 테이블 대조를 통한 신규 메뉴 도출)' 프로세스는 완전히 차단(Ignore)**하고 수행하지 않는다.
|
|
21
|
+
- 사용자의 지시가 **'신규 메뉴 추가(CREATE)'이거나 '누락/미구현 메뉴 분석 요청', 혹은 '삭제와 신규 추가가 결합된 복합 요청'일 경우에만** 2번 규칙을 발동한다.
|
|
22
|
+
|
|
23
|
+
2. **전체 테이블 대조를 통한 누락 메뉴 도출 (Gap Analysis 조건부 수행)**:
|
|
24
|
+
- 1번 규칙에 의해 허용된 경우에만, [기존 Route 정보]와 [전체 테이블 목록]을 대조한다. 매핑되지 않은 누락 테이블 기반의 신규 메뉴(`"action": "CREATE"`)를 시스템 도메인에 맞게 도출하여 추가한다.
|
|
25
|
+
|
|
26
|
+
3. **Soft Delete 및 데이터 보존 규칙 (Never Drop Objects) [치명적 필수]**:
|
|
27
|
+
- 결과 데이터(`data`) 배열은 입력받은 `{routes}`의 모든 객체를 **단 하나도 누락하거나 삭제(Hard Delete)해서는 안 된다.**
|
|
28
|
+
- 사용자가 삭제를 요청한 메뉴나, 상하 관계 규칙에 의해 삭제되는 메뉴는 배열에서 제거하지 말고, 오직 객체 내부의 속성값만 **`"action": "DELETE"`, `"isNew": false`**로 변경하여 배열에 그대로 유지(Soft Delete)해야 한다.
|
|
29
|
+
|
|
30
|
+
4. **Action 및 isNew 필드 상태 동기화**:
|
|
31
|
+
- 결과 데이터(`data`) 배열의 모든 객체는 작업 성격에 따라 반드시 아래의 규칙을 따른다.
|
|
32
|
+
- `"NONE"`: 변경 사항이 없는 기존 유지 메뉴 (`isNew`: false)
|
|
33
|
+
- `"CREATE"`: 누락된 테이블 대조를 통해 완전히 새로 추가된 신규 메뉴 (`isNew`: true)
|
|
34
|
+
- `"UPDATE"`: 사용자의 명시적 요청에 의해 수정된 기존 메뉴 (`isNew`: false)
|
|
35
|
+
- `"DELETE"`: 사용자가 삭제를 요청했거나 아키텍처 구조상 삭제되어야 하는 메뉴 (`isNew`: false)
|
|
36
|
+
|
|
37
|
+
5. **상하 관계 종속성 규칙 (Cascading Rule)**:
|
|
38
|
+
- **순방향 종속**: 상위 메뉴(Level 1)가 `"DELETE"` 되는 경우, 해당 그룹(`id`)을 `parentId`로 가지는 모든 하위 메뉴(Level 2) 역시 연쇄적으로 영향을 받아 반드시 `"action": "DELETE"` 상태로 변경되어야 한다. (배열에서는 탈락시키지 않음)
|
|
39
|
+
- **역방향 종속**: 특정 상위 메뉴(Level 1)에 속한 모든 하위 메뉴(Level 2)가 `"DELETE"` 상태가 되는 경우, 해당 상위 메뉴 역시 자식이 없는 빈 그룹이 되므로 연쇄적으로 `"action": "DELETE"` 처리한다.
|
|
40
|
+
|
|
41
|
+
6. **메뉴 설명 가독성 규칙 (User-Friendly Description)**:
|
|
42
|
+
- `desc`(메뉴 설명) 필드는 실제 현업 사용자가 보는 화면이다. 따라서 `t_qna`, `tb_rooms_hist` 같은 물리적인 데이터베이스 테이블명을 `desc`에 절대 포함하지 마라.
|
|
43
|
+
- 오직 사용자가 이해하기 쉬운 깔끔하고 친절한 비즈니스 용어와 자연어로만 설명을 작성해라. (예: "Q&A(t_qna) 관리" -> X / "고객들의 1:1 문의 및 답변 내역을 관리합니다." -> O)
|
|
44
|
+
|
|
45
|
+
7. **계층 구조 및 식별자 포맷 규칙**:
|
|
46
|
+
- **Level 구조**: Level 1(Group)의 `parentId`는 명확히 `null`로 지정하며, Level 2(Task)는 부모 그룹의 `id`를 `parentId`에 지정한다. (3단계 구조 생성 절대 금지)
|
|
47
|
+
- **기존 데이터 보존**: 기존 베이스([기존 Route 정보])에 있는 항목들은 원래의 id, path, class 포맷을 임의로 수정하지 않고 원형을 그대로 유지한다.
|
|
48
|
+
- **신규 데이터 포맷 및 아이콘 규칙**: 완전히 새로 생성("action": "CREATE")되는 항목에 한해서만 아래 규칙을 엄격하게 적용한다:
|
|
49
|
+
- ID는 소문자 스네이크 케이스(snake_case), Path는 소문자 케밥 케이스(kebab-case) 표준을 적용한다.
|
|
50
|
+
- class 규칙: 완전히 새로 추가되는 신규 항목의 경우, level이 1이면 기본값으로 "icon-folder"를 지정하고, level이 2이면 "icon-none"을 지정한다.
|
|
51
|
+
- **부모 미존재 시 처리**: 신규 도출된 Level 2 메뉴를 수용할 수 있는 적절한 Level 1 그룹이 기존 데이터에 없다면, 도메인에 맞는 Level 1 그룹을 먼저 `"action": "CREATE"`로 생성한 후 그 하위에 매핑한다.
|
|
52
|
+
- **배열 정렬 순서**: 기존 [기존 Route 정보]의 순서 구조를 원형 그대로 보존한다. 단, 신규 추가(`CREATE`)되는 Level 2 메뉴가 존재할 경우에 한해서만, 매핑된 상위 그룹(Level 1) 내부의 가장 마지막 하위 요소 바로 뒤 인덱스 위치에 삽입한다.
|
|
53
|
+
|
|
54
|
+
# 출력 포맷 및 제약 조건 (Strict Output Rule)
|
|
55
|
+
- 반드시 Markdown Wrapper(```json ... ```)를 포함한 유효한 JSON 포맷으로만 응답하라. JSON 외의 텍스트를 시작이나 끝에 절대 덧붙이지 마라.
|
|
56
|
+
- `message` 필드 내부의 줄바꿈은 JSON 문법 오류를 방지하기 위해 실제 엔터(개행)를 입력하지 말고, 반드시 이스케이프 문자인 `\n`을 사용하여 한 줄의 문자열로 처리해라.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# [참조 예시 (Few-Shot)] - 출력 스키마 구조 및 Soft Delete 적용 예시
|
|
61
|
+
{{
|
|
62
|
+
"message": "시스템 아키텍트로서 이번 라우트 재구성 작업에 대한 요약입니다.\\n사용자 요청에 따라 명시된 메뉴의 속성을 변경하였으며, 격리 규칙에 의해 신규 도출 프로세스는 제외되었습니다.",
|
|
63
|
+
"data": [
|
|
64
|
+
{{
|
|
65
|
+
"level": 1,
|
|
66
|
+
"id": "group_doc",
|
|
67
|
+
"parentId": null,
|
|
68
|
+
"path": null,
|
|
69
|
+
"name": "문서 관리 그룹",
|
|
70
|
+
"desc": "전사 문서 및 결재 파일을 통합 관리하는 그룹입니다.",
|
|
71
|
+
"class": "icon-home",
|
|
72
|
+
"isNew": false,
|
|
73
|
+
"action": "DELETE"
|
|
74
|
+
}},
|
|
75
|
+
{{
|
|
76
|
+
"level": 2,
|
|
77
|
+
"id": "doc_manage",
|
|
78
|
+
"parentId": "group_doc",
|
|
79
|
+
"path": "/doc/management",
|
|
80
|
+
"name": "문서 관리",
|
|
81
|
+
"desc": "부서별 문서 업로드 및 권한 관리를 수행합니다.",
|
|
82
|
+
"class": "icon-none",
|
|
83
|
+
"isNew": false,
|
|
84
|
+
"action": "DELETE"
|
|
85
|
+
}}
|
|
86
|
+
]
|
|
87
|
+
}}
|
|
88
|
+
|
|
89
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninebone/mcp",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "NineQuery AI Connector for Database",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"cors": "^2.8.6",
|
|
22
22
|
"dotenv": "^16.4.5",
|
|
23
23
|
"express": "^4.22.1",
|
|
24
|
+
"jsonwebtoken": "^9.0.3",
|
|
24
25
|
"mariadb": "^3.5.2",
|
|
25
26
|
"mysql2": "^3.9.7",
|
|
26
27
|
"oracledb": "^6.10.0",
|
|
@@ -16,43 +16,46 @@
|
|
|
16
16
|
# 분석 및 설계 규칙
|
|
17
17
|
|
|
18
18
|
1. **작업 분기 및 격리 규칙 (Action Isolation) [최우선 필수]**:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
- 사용자의 요청({user_input})이 **오직 기존 메뉴에 대한 '삭제(DELETE)' 또는 '수정(UPDATE)' 명령으로만 이루어진 경우**, 오직 해당 변경 사항만을 엄격하게 반영한다.
|
|
20
|
+
- 이 경우, 시스템 안정성과 사용자의 제어 의도를 존중하기 위해, **'2번 규칙(전체 테이블 대조를 통한 신규 메뉴 도출)' 프로세스는 완전히 차단(Ignore)**하고 수행하지 않는다.
|
|
21
|
+
- 사용자의 지시가 **'신규 메뉴 추가(CREATE)'이거나 '누락/미구현 메뉴 분석 요청', 혹은 '삭제와 신규 추가가 결합된 복합 요청'일 경우에만** 2번 규칙을 발동한다.
|
|
22
22
|
|
|
23
23
|
2. **전체 테이블 대조를 통한 누락 메뉴 도출 (Gap Analysis 조건부 수행)**:
|
|
24
|
-
|
|
24
|
+
- 1번 규칙에 의해 허용된 경우에만, [기존 Route 정보]와 [전체 테이블 목록]을 대조한다. 매핑되지 않은 누락 테이블 기반의 신규 메뉴(`"action": "CREATE"`)를 시스템 도메인에 맞게 도출하여 추가한다.
|
|
25
25
|
|
|
26
26
|
3. **Soft Delete 및 데이터 보존 규칙 (Never Drop Objects) [치명적 필수]**:
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
- 결과 데이터(`data`) 배열은 입력받은 `{routes}`의 모든 객체를 **단 하나도 누락하거나 삭제(Hard Delete)해서는 안 된다.**
|
|
28
|
+
- 사용자가 삭제를 요청한 메뉴나, 상하 관계 규칙에 의해 삭제되는 메뉴는 배열에서 제거하지 말고, 오직 객체 내부의 속성값만 **`"action": "DELETE"`, `"isNew": false`**로 변경하여 배열에 그대로 유지(Soft Delete)해야 한다.
|
|
29
29
|
|
|
30
30
|
4. **Action 및 isNew 필드 상태 동기화**:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
- 결과 데이터(`data`) 배열의 모든 객체는 작업 성격에 따라 반드시 아래의 규칙을 따른다.
|
|
32
|
+
- `"NONE"`: 변경 사항이 없는 기존 유지 메뉴 (`isNew`: false)
|
|
33
|
+
- `"CREATE"`: 누락된 테이블 대조를 통해 완전히 새로 추가된 신규 메뉴 (`isNew`: true)
|
|
34
|
+
- `"UPDATE"`: 사용자의 명시적 요청에 의해 수정된 기존 메뉴 (`isNew`: false)
|
|
35
|
+
- `"DELETE"`: 사용자가 삭제를 요청했거나 아키텍처 구조상 삭제되어야 하는 메뉴 (`isNew`: false)
|
|
36
36
|
|
|
37
37
|
5. **상하 관계 종속성 규칙 (Cascading Rule)**:
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
- **순방향 종속**: 상위 메뉴(Level 1)가 `"DELETE"` 되는 경우, 해당 그룹(`id`)을 `parentId`로 가지는 모든 하위 메뉴(Level 2) 역시 연쇄적으로 영향을 받아 반드시 `"action": "DELETE"` 상태로 변경되어야 한다. (배열에서는 탈락시키지 않음)
|
|
39
|
+
- **역방향 종속**: 특정 상위 메뉴(Level 1)에 속한 모든 하위 메뉴(Level 2)가 `"DELETE"` 상태가 되는 경우, 해당 상위 메뉴 역시 자식이 없는 빈 그룹이 되므로 연쇄적으로 `"action": "DELETE"` 처리한다.
|
|
40
40
|
|
|
41
41
|
6. **메뉴 설명 가독성 규칙 (User-Friendly Description)**:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- ID는 소문자 스네이크 케이스(snake_case)
|
|
42
|
+
- `desc`(메뉴 설명) 필드는 실제 현업 사용자가 보는 화면이다. 따라서 `t_qna`, `tb_rooms_hist` 같은 물리적인 데이터베이스 테이블명을 `desc`에 절대 포함하지 마라.
|
|
43
|
+
- 오직 사용자가 이해하기 쉬운 깔끔하고 친절한 비즈니스 용어와 자연어로만 설명을 작성해라. (예: "Q&A(t_qna) 관리" -> X / "고객들의 1:1 문의 및 답변 내역을 관리합니다." -> O)
|
|
44
|
+
|
|
45
|
+
7. **계층 구조 및 식별자 포맷 규칙**:
|
|
46
|
+
- **Level 구조**: Level 1(Group)의 `parentId`는 명확히 `null`로 지정하며, Level 2(Task)는 부모 그룹의 `id`를 `parentId`에 지정한다. (3단계 구조 생성 절대 금지)
|
|
47
|
+
- **기존 데이터 보존**: 기존 베이스([기존 Route 정보])에 있는 항목들은 원래의 id, path, class 포맷을 임의로 수정하지 않고 원형을 그대로 유지한다.
|
|
48
|
+
- **신규 데이터 포맷 및 아이콘 규칙**: 완전히 새로 생성("action": "CREATE")되는 항목에 한해서만 아래 규칙을 엄격하게 적용한다:
|
|
49
|
+
- ID는 소문자 스네이크 케이스(snake_case)를 적용한다.
|
|
50
|
+
- **Path 포맷 제약 규칙 [치명적 필수 - 특수문자 전면 금지]**: 새로 생성되는 Path에는 **하이픈(`-`)과 언더바(`_`)를 포함하는 것을 절대 금지**한다. 여러 단어로 이루어진 경로명이라도 구분자 없이 오직 소문자 전체를 하나의 단어로 길게 이어 붙여서 생성해야 한다.
|
|
51
|
+
- *올바른 예시*: `/aaa/ipprecedentinfo` (하이픈, 언더바 없이 한 단어로 밀어붙임)
|
|
52
|
+
- *잘못된 예시*: `/aaa/ip-precedent-info` (X), `/aaa/ip_precedent_info` (X)
|
|
50
53
|
- class 규칙: 완전히 새로 추가되는 신규 항목의 경우, level이 1이면 기본값으로 "icon-folder"를 지정하고, level이 2이면 "icon-none"을 지정한다.
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
- **부모 미존재 시 처리**: 신규 도출된 Level 2 메뉴를 수용할 수 있는 적절한 Level 1 그룹이 기존 데이터에 없다면, 도메인에 맞는 Level 1 그룹을 먼저 `"action": "CREATE"`로 생성 한 후 그 하위에 매핑한다.
|
|
55
|
+
- **배열 정렬 순서**: 기존 [기존 Route 정보]의 순서 구조를 원형 그대로 보존한다. 단, 신규 추가(`CREATE`)되는 Level 2 메뉴가 존재할 경우에 한해서만, 매핑된 상위 그룹(Level 1) 내부의 가장 마지막 하위 요소 바로 뒤 인덱스 위치에 삽입한다.
|
|
53
56
|
|
|
54
57
|
# 출력 포맷 및 제약 조건 (Strict Output Rule)
|
|
55
|
-
- 반드시 Markdown Wrapper(```json ... ```)를 포함한 유효한 JSON 포맷으로만 응답하라. JSON 외의 텍스트를 시작이나 끝에 절대 덧붙이지 마라.
|
|
58
|
+
- 다른 부연 설명, 인사말, 질문은 모두 생략하고 반드시 Markdown Wrapper(```json ... ```)를 포함한 유효한 JSON 포맷으로만 응답하라. JSON 외의 텍스트를 시작이나 끝에 절대 덧붙이지 마라.
|
|
56
59
|
- `message` 필드 내부의 줄바꿈은 JSON 문법 오류를 방지하기 위해 실제 엔터(개행)를 입력하지 말고, 반드시 이스케이프 문자인 `\n`을 사용하여 한 줄의 문자열로 처리해라.
|
|
57
60
|
|
|
58
61
|
---
|
|
@@ -84,6 +87,4 @@
|
|
|
84
87
|
"action": "DELETE"
|
|
85
88
|
}}
|
|
86
89
|
]
|
|
87
|
-
}}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
}}
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
1. [대상 메뉴명]: {menu_description}
|
|
11
11
|
2. [사용자 요청 사항]: {user_input}
|
|
12
12
|
3. [클래스 Package]: {controller_package}
|
|
13
|
-
4. [
|
|
14
|
-
5. [
|
|
15
|
-
6. [
|
|
13
|
+
4. [클래스 기준명(BaseName)]: {base_name}
|
|
14
|
+
5. [API Base URL]: {api_base_url}
|
|
15
|
+
6. [선행 작성된 Service 소스 코드]: {service_source}
|
|
16
|
+
7. [AS-IS 원본 소스 코드]: {asis_source}
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -21,18 +22,14 @@
|
|
|
21
22
|
1. 클래스 구조 및 선언:
|
|
22
23
|
- 첫 줄에 `package {controller_package};` 선언을 반드시 포함합니다.
|
|
23
24
|
|
|
24
|
-
- **[클래스명 생성 규칙 (엄격)]**
|
|
25
|
-
|
|
26
|
-
2. 추출된 단어들에 하이픈('-')이나 언더바('_')가 포함되어 있다면 해당 기호를 기준으로 단어를 한 번 더 쪼갭니다.
|
|
27
|
-
3. 쪼개진 모든 단어들을 순서대로 결합하여 완벽한 'PascalCase' 형태의 기점명(BaseName)을 만듭니다.
|
|
28
|
-
4. 최종 클래스명은 이 기점명 뒤에 접미사 'Controller'를 결합한 명칭으로 삼습니다.
|
|
29
|
-
- 공식: [ClassName] = [BaseName]Controller (임의 변형 및 단어 축약 절대 금지)
|
|
25
|
+
- **[클래스명 생성 규칙 (엄격)]** - 최종 클래스명은 제공된 `{base_name}` 뒤에 접미사 'Controller'를 결합한 명칭으로 확정합니다.
|
|
26
|
+
- 공식: [ClassName] = {base_name}Controller (임의 변형, 철자 변경 및 단어 축약 절대 금지)
|
|
30
27
|
|
|
31
28
|
- 클래스 레벨에 `@RestController` 및 `@RequestMapping("{api_base_url}")` 어노테이션을 반드시 추가합니다.
|
|
32
29
|
- 생성자 DI 주입을 위해 `@RequiredArgsConstructor` 어노테이션을 추가합니다.
|
|
33
30
|
|
|
34
31
|
- **[빈 이름 충돌 방지]** 스프링 컨텍스트 내 빈 이름 충돌 방지를 위해, `@RestController` 선언 시 반드시 생성된 클래스명의 첫 글자만 소문자로 바꾼 식별 이름을 부여하십시오.
|
|
35
|
-
- 규칙: `@RestController("{{
|
|
32
|
+
- 규칙: `@RestController("{{ {base_name}의 첫 글자만 소문자로 변환 }}Controller")` 형태로 명시해야 하며, 그냥 `@RestController`만 선언하는 것은 절대 금지합니다.
|
|
36
33
|
|
|
37
34
|
2. 필수 Import 구문 준수:
|
|
38
35
|
- `org.springframework.web.bind.annotation.RestController;`
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
1. [대상 메뉴명]: {menu_description}
|
|
11
11
|
2. [사용자 요청 사항]: {user_input}
|
|
12
12
|
3. [클래스 Package]: {service_package}
|
|
13
|
-
4. [
|
|
14
|
-
5. [MyBatis
|
|
15
|
-
6. [
|
|
13
|
+
4. [클래스 기준명(BaseName)]: {base_name}
|
|
14
|
+
5. [MyBatis Namespace]: {mapper_package}
|
|
15
|
+
6. [MyBatis Mapper XML 소스]: {mapper_source}
|
|
16
|
+
7. [AS-IS 원본 소스 코드]: {asis_source}
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -21,18 +22,14 @@
|
|
|
21
22
|
1. 클래스 구조 및 선언:
|
|
22
23
|
- 첫 줄에 `package {service_package};` 선언을 반드시 포함합니다.
|
|
23
24
|
|
|
24
|
-
- **[클래스명 생성 규칙 (엄격)]**
|
|
25
|
-
|
|
26
|
-
2. 추출된 단어들에 하이픈('-')이나 언더바('_')가 포함되어 있다면 해당 기호를 기준으로 단어를 한 번 더 쪼갭니다.
|
|
27
|
-
3. 쪼개진 모든 단어들을 순서대로 결합하여 완벽한 'PascalCase' 형태의 기점명(BaseName)을 만듭니다.
|
|
28
|
-
4. 최종 클래스명은 이 기점명 뒤에 접미사 'Service'를 결합한 명칭으로 삼습니다.
|
|
29
|
-
- 공식: [ClassName] = [BaseName]Service (임의 변형 및 단어 축약 절대 금지)
|
|
25
|
+
- **[클래스명 생성 규칙 (엄격)]** - 최종 클래스명은 제공된 `{base_name}` 뒤에 접미사 'Service'를 결합한 명칭으로 확정합니다.
|
|
26
|
+
- 공식: [ClassName] = {base_name}Service (임의 변형, 철자 변경 및 단어 축약 절대 금지)
|
|
30
27
|
|
|
31
28
|
- 클래스 레벨에 `@Service` 및 `@RequiredArgsConstructor` 어노테이션을 반드시 추가합니다.
|
|
32
29
|
|
|
33
30
|
- **[빈 이름 충돌 방지]** 스프링 컨텍스트 내 빈 이름 충돌 방지를 위해, `@Service` 선언 시 반드시 생성된 클래스명의 첫 글자만 소문자로 바꾼 식별 이름을 부여하십시오.
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
- 규칙: `@Service("{{ {base_name}의 첫 글자만 소문자로 변환 }}Service")` 형태로 명시해야 하며, 그냥 `@Service`만 선언하는 것은 절대 금지합니다.
|
|
32
|
+
|
|
36
33
|
2. 필수 Import 구문 준수:
|
|
37
34
|
- `org.springframework.stereotype.Service;`
|
|
38
35
|
- `lombok.RequiredArgsConstructor;`
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
### [사용자 제공 정보]
|
|
10
10
|
1. [사용자 질문]: {user_input}
|
|
11
11
|
2. [메뉴 URL 및 메뉴명]: {api_base_url} / {menu_description}
|
|
12
|
-
3. [베이스 클래스명]: {
|
|
12
|
+
3. [베이스 클래스명]: {base_name}
|
|
13
13
|
4. [테이블 정의]: {schema_detail}
|
|
14
14
|
5. [MyBatis Mapper XML 소스]: {mapper_source}
|
|
15
15
|
6. [Controller 클래스 소스 코드]: {controller_source}
|
package/src/core/init.js
CHANGED
|
@@ -13,6 +13,7 @@ const SOURCE_PROMPT_DIR = path.resolve(__dirname, '../../prompts');
|
|
|
13
13
|
*/
|
|
14
14
|
async function createEnvFile(rl) {
|
|
15
15
|
const questions = [
|
|
16
|
+
{ key: 'NINE_BONE_API_KEY', label: '0. NineBone API Key (발급받은 FREE KEY)', default: '' },
|
|
16
17
|
{ key: 'SERVER_PORT', label: '1. 커넥터 서버 포트', default: '4001' },
|
|
17
18
|
{ key: 'GEMINI_API_KEY', label: '2. Gemini API Key', default: '' },
|
|
18
19
|
{ key: 'GEMINI_MODEL', label: '3. 사용할 모델', default: 'gemini-2.5-flash' },
|
|
@@ -32,6 +32,29 @@ class DatabaseManager {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// 💡 [추가] 프로세스 종료 시 커넥션 풀을 명시적으로 해제하는 메서드
|
|
36
|
+
async disconnect() {
|
|
37
|
+
if (!this.pool) return;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// 1. 일반적인 락 해제 (this.pool 자체에 end나 close가 있는 경우)
|
|
41
|
+
if (typeof this.pool.end === 'function') {
|
|
42
|
+
await this.pool.end();
|
|
43
|
+
} else if (typeof this.pool.close === 'function') {
|
|
44
|
+
await this.pool.close();
|
|
45
|
+
}
|
|
46
|
+
// 2. PoolManager 내부에 별도의 closePool이나 해제 로직이 구현되어 있다면 호출
|
|
47
|
+
else if (PoolManager && typeof PoolManager.closePool === 'function') {
|
|
48
|
+
await PoolManager.closePool(this.pool);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.pool = null;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(`[${this.type}] Failed to close database pool:`, error.message);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
/**
|
|
36
59
|
* DB별 결과 포맷 차이를 여기서 해결합니다. (데이터 배열만 반환)
|
|
37
60
|
*/
|
package/src/mcp/mcp-server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import cors from "cors";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
3
4
|
import { WebSocketServer } from "ws";
|
|
4
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
6
|
import { CustomWsTransport } from "../utils/CustomWsTransport.js";
|
|
@@ -34,6 +35,37 @@ export async function bootstrap() {
|
|
|
34
35
|
throw new Error(".env 파일의 DB_TYPE을 읽을 수 없습니다.");
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const apiKey = process.env.NINE_BONE_API_KEY;
|
|
39
|
+
if (!apiKey) {
|
|
40
|
+
console.error("[인증 오류] .env 파일에 NINE_BONE_API_KEY가 존재하지 않습니다.");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let maxConnectionsLimit = 1;
|
|
45
|
+
let expiresAt;
|
|
46
|
+
try {
|
|
47
|
+
const decoded = jwt.verify(apiKey, "nandoo");
|
|
48
|
+
expiresAt = new Date(decoded.exp * 1000);
|
|
49
|
+
maxConnectionsLimit = Number(decoded.maxConnections) || 1; // 💡 토큰에서 동시접속 제한 수 추출
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
console.log("\n-------------------------------------------");
|
|
53
|
+
console.log("🔐 NineBone 라이선스 인증 성공");
|
|
54
|
+
//console.log(`👤 사용자 식별자(userId): ${decoded.userId}`);
|
|
55
|
+
console.log(`👥 동시 접속 제한(maxConnections): ${maxConnectionsLimit}개`);
|
|
56
|
+
console.log(`📅 토큰 만료 일시: ${expiresAt.toLocaleString()}`);
|
|
57
|
+
console.log("-------------------------------------------\n");
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
if (expiresAt < new Date()) {
|
|
61
|
+
console.error("❌ [인증 실패] 사용 기간이 만료된 API Key입니다. 정식 키를 구매해 주세요.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error("❌ [인증 실패] 유효하지 않거나 변조된 NineBone API Key입니다.", err.message);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
37
69
|
// 1. 코어 인프라 자원 초기화
|
|
38
70
|
const db = new DatabaseManager({
|
|
39
71
|
type: process.env.DB_TYPE,
|
|
@@ -95,6 +127,56 @@ export async function bootstrap() {
|
|
|
95
127
|
process.exit(1);
|
|
96
128
|
}
|
|
97
129
|
|
|
130
|
+
// 💡 [순서 조정] 타이머에서 사용하기 위해 gracefulShutdown 함수를 위로 끌어올림
|
|
131
|
+
const gracefulShutdown = async (signal) => {
|
|
132
|
+
console.log(`\n👋 ${signal} 신호 감지: Nine MCP Engine 안전 종료 절차를 시작합니다.`);
|
|
133
|
+
|
|
134
|
+
if (expiryTimer) clearInterval(expiryTimer);
|
|
135
|
+
|
|
136
|
+
const forceExitTimeout = setTimeout(() => {
|
|
137
|
+
console.warn("⚠️ [경고] DB 해제가 지연되어 프로세스를 강제 종료합니다.");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}, 2000);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (db && typeof db.disconnect === 'function') {
|
|
143
|
+
console.log("⏳ DB 커넥션 풀 반환 중...");
|
|
144
|
+
await db.disconnect();
|
|
145
|
+
console.log("🔒 DB 커넥션 풀이 성공적으로 닫혔습니다.");
|
|
146
|
+
} else {
|
|
147
|
+
console.log("ℹ️ 정의된 DB disconnect 메서드가 없어 해제 단계를 건너넙니다.");
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("❌ DB 해제 중 오류 발생:", err.message);
|
|
151
|
+
} finally {
|
|
152
|
+
clearTimeout(forceExitTimeout);
|
|
153
|
+
console.log("🛑 Nine MCP Engine이 완전히 종료되었습니다.");
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// 윈도우/리눅스 환경에서 Ctrl+C 나 PM2 종료 신호 가로채기
|
|
159
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
160
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
161
|
+
|
|
162
|
+
// 💡 백그라운드 만료 체크 함수 및 24시간 타이머 구동
|
|
163
|
+
const checkLicenseExpiry = () => {
|
|
164
|
+
try {
|
|
165
|
+
const decoded = jwt.verify(process.env.NINE_BONE_API_KEY, "nandoo");
|
|
166
|
+
const expiresAt = new Date(decoded.exp * 1000);
|
|
167
|
+
|
|
168
|
+
if (expiresAt < new Date()) {
|
|
169
|
+
console.error("\n🚨 [라이선스 만료] 구동 중 NineBone API Key 사용 기간이 만료되었습니다. 엔진을 종료합니다.");
|
|
170
|
+
gracefulShutdown('LICENSE_EXPIRED');
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error("\n🚨 [라이선스 오류] 변조되거나 잘못된 API Key입니다. 엔진을 종료합니다.");
|
|
174
|
+
gracefulShutdown('LICENSE_INVALID');
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const expiryTimer = setInterval(checkLicenseExpiry, 24 * 60 * 60 * 1000);
|
|
179
|
+
|
|
98
180
|
// 🌐 9. Express & WebSocket 인프라 기동 및 트랜스포트 바인딩
|
|
99
181
|
const app = express();
|
|
100
182
|
app.use(cors(), express.json());
|
|
@@ -105,6 +187,13 @@ export async function bootstrap() {
|
|
|
105
187
|
try {
|
|
106
188
|
const httpServer = app.listen(PORT, () => {
|
|
107
189
|
console.log(`🚀 Nine MCP Engine ON: ws://localhost:${PORT}`);
|
|
190
|
+
|
|
191
|
+
// 💡 [수정] 사용자 식별자(userId) 제거 및 맨 마지막에 명확하게 만료일 노출
|
|
192
|
+
console.log("\n===========================================");
|
|
193
|
+
console.log("🔐 NineBone 라이선스 인증 완료");
|
|
194
|
+
console.log(`👥 동시 접속 제한(maxConnections): ${maxConnectionsLimit}개`);
|
|
195
|
+
console.log(`📅 토큰 만료 일시: ${expiresAt ? expiresAt.toLocaleString() : '확인 불가'}`);
|
|
196
|
+
console.log("===========================================\n");
|
|
108
197
|
});
|
|
109
198
|
|
|
110
199
|
const wss = new WebSocketServer({ server: httpServer });
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
const convertToPascalBaseName = (rawPath) => {
|
|
4
|
+
if (!rawPath || typeof rawPath !== 'string') return "Generated";
|
|
5
|
+
|
|
6
|
+
// 앞뒤 슬래시 정리 후 "/"로 분리
|
|
7
|
+
const cleanPath = rawPath.replace(/^\/+|\/+$/g, "");
|
|
8
|
+
const pathParts = cleanPath.split("/").filter(Boolean);
|
|
9
|
+
const len = pathParts.length;
|
|
10
|
+
|
|
11
|
+
if (len === 0) return "Main";
|
|
12
|
+
|
|
13
|
+
// 무조건 맨 뒤에서 최대 2개 폴더 타겟팅
|
|
14
|
+
const startIdx = Math.max(0, len - 2);
|
|
15
|
+
let baseName = "";
|
|
16
|
+
|
|
17
|
+
for (let i = startIdx; i < len; i++) {
|
|
18
|
+
// 하이픈(-)과 언더바(_) 기준 분리
|
|
19
|
+
const subParts = pathParts[i].split(/[-_]/).filter(Boolean);
|
|
20
|
+
for (const subPart of subParts) {
|
|
21
|
+
baseName += subPart.charAt(0).toUpperCase() + subPart.slice(1).toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return baseName || "Main";
|
|
25
|
+
};
|
|
26
|
+
|
|
3
27
|
export const generateSourceBrainTool = (db, ai) => ({
|
|
4
28
|
name: "generate-source-brain",
|
|
5
29
|
description: "미매핑 라우터 정보를 전달받아, 해당 메뉴들과 연관된 데이터베이스 테이블 스키마를 정밀 분석한 후 MyBatis Mapper XML ➡️ 비즈니스 Service ➡️ 컨트롤러 API ➡️ React UI 컴포넌트까지 4대 영역의 소스코드를 순차적 의존성 주입 구조로 일괄 생성(EXECUTE_BATCH)하는 소스코드 빌드 전용 도구입니다.",
|
|
@@ -49,10 +73,12 @@ export const generateSourceBrainTool = (db, ai) => ({
|
|
|
49
73
|
|
|
50
74
|
const pathParts = (batch?.path || "").split("/");
|
|
51
75
|
const rawClassName = pathParts[pathParts.length - 1] || "Sample";
|
|
76
|
+
|
|
77
|
+
/**
|
|
52
78
|
const baseClassName = rawClassName
|
|
53
79
|
.split("-")
|
|
54
80
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
55
|
-
.join("");
|
|
81
|
+
.join(""); */
|
|
56
82
|
|
|
57
83
|
// 💡 [중요] 연쇄 주입을 위해 루프가 돌기 전, 생성된 소스들을 저장할 버퍼 임시 객체 선언
|
|
58
84
|
const generatedOutputs = {
|
|
@@ -60,6 +86,8 @@ export const generateSourceBrainTool = (db, ai) => ({
|
|
|
60
86
|
serviceCode: ""
|
|
61
87
|
};
|
|
62
88
|
|
|
89
|
+
const baseClassName = convertToPascalBaseName(batch.path);
|
|
90
|
+
|
|
63
91
|
// 2. ⚡ 레이어별 동적 파라미터 빌더 팩토리 (익명 함수가 아닌 런타임에 인자를 받도록 변경)
|
|
64
92
|
const targetChains = [
|
|
65
93
|
{
|
|
@@ -82,7 +110,9 @@ export const generateSourceBrainTool = (db, ai) => ({
|
|
|
82
110
|
buildParams: () => ({
|
|
83
111
|
...params,
|
|
84
112
|
asis_source: "",
|
|
113
|
+
|
|
85
114
|
menu_description: batch.description,
|
|
115
|
+
base_name: baseClassName,
|
|
86
116
|
//schema_detail: filteredSchema,
|
|
87
117
|
service_package: `${params.base_package}.${cleanPath}.service`,
|
|
88
118
|
mapper_package: `${params.base_package}.${cleanPath}.mapper`,
|
|
@@ -98,6 +128,7 @@ export const generateSourceBrainTool = (db, ai) => ({
|
|
|
98
128
|
...params,
|
|
99
129
|
asis_source: "",
|
|
100
130
|
menu_description: batch.description,
|
|
131
|
+
base_name: baseClassName,
|
|
101
132
|
//schema_detail: filteredSchema,
|
|
102
133
|
api_base_url: batch.path,
|
|
103
134
|
controller_package: `${params.base_package}.${cleanPath}.controller`,
|
|
@@ -116,7 +147,7 @@ export const generateSourceBrainTool = (db, ai) => ({
|
|
|
116
147
|
menu_description: batch.description,
|
|
117
148
|
api_base_url: batch.path,
|
|
118
149
|
menu_name: batch.description,
|
|
119
|
-
|
|
150
|
+
base_name: baseClassName,
|
|
120
151
|
schema_detail: filteredSchema, // 스키마 세부 사항 전달
|
|
121
152
|
mapper_source: generatedOutputs.mapperSource, // ★ 의존성 연쇄 주입
|
|
122
153
|
controller_source: generatedOutputs.controllerSource
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
const convertToPascalBaseName = (rawPath) => {
|
|
4
|
+
if (!rawPath || typeof rawPath !== 'string') return "Generated";
|
|
5
|
+
|
|
6
|
+
// 앞뒤 슬래시 정리 후 "/"로 분리
|
|
7
|
+
const cleanPath = rawPath.replace(/^\/+|\/+$/g, "");
|
|
8
|
+
const pathParts = cleanPath.split("/").filter(Boolean);
|
|
9
|
+
const len = pathParts.length;
|
|
10
|
+
|
|
11
|
+
if (len === 0) return "Main";
|
|
12
|
+
|
|
13
|
+
// 무조건 맨 뒤에서 최대 2개 폴더 타겟팅
|
|
14
|
+
const startIdx = Math.max(0, len - 2);
|
|
15
|
+
let baseName = "";
|
|
16
|
+
|
|
17
|
+
for (let i = startIdx; i < len; i++) {
|
|
18
|
+
// 하이픈(-)과 언더바(_) 기준 분리
|
|
19
|
+
const subParts = pathParts[i].split(/[-_]/).filter(Boolean);
|
|
20
|
+
for (const subPart of subParts) {
|
|
21
|
+
baseName += subPart.charAt(0).toUpperCase() + subPart.slice(1).toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return baseName || "Main";
|
|
25
|
+
};
|
|
26
|
+
|
|
3
27
|
/**
|
|
4
28
|
* 💡 [초정밀 문맥 매칭] 공백, 들여쓰기, 따옴표 격차를 완전히 초월한 최종 진화형 머지 엔진
|
|
5
29
|
*/
|
|
@@ -129,7 +153,7 @@ export const modifySourceBrainTool = (db, ai) => ({
|
|
|
129
153
|
|
|
130
154
|
const pathParts = (target?.path || params.current_path || "").split("/");
|
|
131
155
|
const rawClassName = pathParts[pathParts.length - 1] || "Sample";
|
|
132
|
-
const baseClassName = rawClassName.split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
156
|
+
//const baseClassName = rawClassName.split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
133
157
|
|
|
134
158
|
const mybatisFile = extractFileByExtension(asisSources.mybatis, [".xml"]);
|
|
135
159
|
const serviceFile = extractFileByExtension(asisSources.service, [".java"]);
|
|
@@ -143,6 +167,8 @@ export const modifySourceBrainTool = (db, ai) => ({
|
|
|
143
167
|
uiSource: javascriptFile.contents
|
|
144
168
|
};
|
|
145
169
|
|
|
170
|
+
const baseClassName = convertToPascalBaseName(target?.path || params?.current_path || "");
|
|
171
|
+
|
|
146
172
|
const targetChains = [
|
|
147
173
|
{
|
|
148
174
|
id: "generate-source-mapper",
|
|
@@ -170,6 +196,7 @@ export const modifySourceBrainTool = (db, ai) => ({
|
|
|
170
196
|
buildParams: () => ({
|
|
171
197
|
...params,
|
|
172
198
|
menu_description: target.description,
|
|
199
|
+
base_name: baseClassName,
|
|
173
200
|
service_package: `${basePackage}.${cleanPath}.service`,
|
|
174
201
|
mapper_package: `${basePackage}.${cleanPath}.mapper`,
|
|
175
202
|
asis_source: updatedOutputs.serviceSource,
|
|
@@ -187,6 +214,7 @@ export const modifySourceBrainTool = (db, ai) => ({
|
|
|
187
214
|
buildParams: () => ({
|
|
188
215
|
...params,
|
|
189
216
|
menu_description: target.description,
|
|
217
|
+
base_name: baseClassName,
|
|
190
218
|
api_base_url: target.path || params.current_path,
|
|
191
219
|
controller_package: `${basePackage}.${cleanPath}.controller`,
|
|
192
220
|
service_package: `${basePackage}.${cleanPath}.service`,
|
|
@@ -207,7 +235,7 @@ export const modifySourceBrainTool = (db, ai) => ({
|
|
|
207
235
|
menu_description: target.description,
|
|
208
236
|
api_base_url: target.path || params.current_path,
|
|
209
237
|
menu_name: target.description,
|
|
210
|
-
|
|
238
|
+
base_name: baseClassName,
|
|
211
239
|
schema_detail: filteredSchema,
|
|
212
240
|
asis_source: updatedOutputs.uiSource,
|
|
213
241
|
modification_task: target.modificationTask,
|