@ninebone/mcp 0.1.30 → 1.0.2
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-mapper.oracle.md +33 -26
- package/prompts/source/generate-source-mapper.postgre.md +36 -27
- 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.
|
|
3
|
+
"version": "1.0.2",
|
|
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;`
|
|
@@ -17,32 +17,36 @@
|
|
|
17
17
|
**[XML 작성 및 수정 필수 조건 (Oracle 전용)]**
|
|
18
18
|
|
|
19
19
|
1. XML 공통 규칙 (LangChain 템플릿 변수 충돌 방지를 위해 이중 중괄호 표현 사용):
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
- resultType 속성은 무조건 플레이스홀더 값인 '{result_type}'으로 처리합니다.
|
|
21
|
+
- AI 너가 최종 출력물로 뱉어야 하는 모든 MyBatis 파라미터 양식은 오직 단일 중괄호 형태인 '#{{variable_name}}' 및 '${{variable_name}}' 구조여야 합니다. (주의: 최종 출력 결과물에 중괄호가 3개 이상 중첩되어 템플릿 문법이 깨지지 않도록 절대 주의하세요.)
|
|
22
|
+
- selectList 쿼리에는 동적 검색 조건 처리를 위해 반드시 아래 구문을 주입하고 ORDER BY 절을 명시해야 합니다.
|
|
23
|
+
<where>
|
|
24
|
+
<if test="_nineAiGenerated != null"> AND ${{_nineAiGenerated}} </if>
|
|
25
|
+
</where>
|
|
26
|
+
- [중요 - XML 부등호 처리]: SQL 문 내에서 비교 연산자(예: <, >, <=, >=)를 사용할 때 XML 파싱 에러를 방지하기 위해, 해당 쿼리문 블록을 반드시 `<![CDATA[ SQL구문 ]]>`으로 감싸거나 `<`, `>`, `<=`, `>=` 등의 XML 엔티티로 변환하여 작성하십시오. (동적 태그 <if> 등과 겹치지 않도록 주의할 것)
|
|
27
|
+
- [네임스페이스 처리 규칙]: <mapper namespace="{namespace}"> 선언 시, 외부 인자로 제공된 `{namespace}` 플레이스홀더 값은 자바 백엔드가 런타임에 삽입해 주는 '절대 고정값 변수'입니다. AI 너가 임의로 패키지 경로를 유추하여 하드코딩하는 행위를 일절 엄금하며, 반드시 원본 텍스트 형태 그대로 문장 속에 `{namespace}`를 유지하여 출력하십시오.
|
|
26
28
|
|
|
27
29
|
2. Oracle SQL 작성 준수 사항:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
- 식별자 대소문자 유지: 제공된 [테이블 스키마 정보]의 테이블명과 컬럼명 스펠링 및 대소문자(소문자/대문자)를 절대로 임의 변경하지 말고 원본 그대로 복사하여 SQL을 작성하세요. Oracle 특성상 소문자나 소/대문자 혼용 표기(CamelCase)로 설계된 식별자가 제공된 경우, 정확한 매핑을 위해 필요한 경우에 한해 식별자를 쌍따옴표(`""`)로 감싸서 작성하십시오.
|
|
31
|
+
- 날짜/시간 함수: 등록일(REG_DT)/수정일(MOD_DT) 등의 타임스탬프 필드 자리에 Oracle 전용 함수인 'SYSDATE' 또는 'CURRENT_TIMESTAMP'를 사용하십시오. (절대 NOW()를 사용하지 마십시오.)
|
|
32
|
+
- [동적 쿼리 파라미터 검증 규칙]: <if test="..."> 내에서 검색 조건을 비교할 때는 파라미터의 데이터 타입(숫자형, 문자열 등)을 불문하고, 모든 조건에 대해 반드시 `변수명 != null and 변수명 != ''` 형태로 null 체크와 빈 문자열 체크를 동시에 결합하여 선언하도록 강제하십시오. (단, 숫자 0의 오작동을 방지할 수 있도록 프로젝트 규격을 통일합니다.)
|
|
31
33
|
|
|
32
34
|
3. 출력 형식 및 JSON 제약사항 (필수):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
- **너의 최종 응답은 오직 아래 지정된 [반환 JSON 포맷] 양식에 맞는 순수한 JSON 데이터여야 합니다.** (설명 텍스트나 마크다운 코드 블록 ```json 절대 금지)
|
|
36
|
+
- JSON 내부 문자열에 쌍따옴표(")나 개행(\n), 탭(\t)이 들어갈 경우, JSON 규격이 깨지지 않도록 반드시 백슬래시를 붙여 '\"' 또는 '\\n' 형태로 완벽하게 이스케이프(Escape) 처리를 하십시오.
|
|
37
|
+
- [CREATE 모드]일 때는 전체 XML 코드를 설계하여 `full_source` 필드에 채우고, `source_changes` 배열은 빈 배열(`[]`)로 반환합니다.
|
|
38
|
+
* 반드시 `<?xml...>`, `<!DOCTYPE...>`, `<mapper namespace="{namespace}">` 태그가 완전히 포함된 스케폴딩 구조여야 합니다. (이때 namespace 자리는 절대 다른 문자열로 치환하지 말고 텍스트 그대로 유지할 것)
|
|
39
|
+
* 사용자의 별도 요청이 없더라도 스키마를 분석하여 기본 4대 핵심 CRUD 쿼리인 `selectList`, `selectOne`, `insert`, `update`를 기본 탑재하십시오.
|
|
40
|
+
- [UPDATE 모드]일 때는 `full_source` 필드를 빈 문자열(`""`)로 채우고, 변경 사항을 `source_changes` 배열에 담아 반환합니다.
|
|
41
|
+
* 'search_block'을 지정할 때는 소스 내에서 유일하게 식별(Unique)되도록 앞뒤로 최소 2라인 이상의 컨텍스트 코드를 반드시 포함해야 합니다.
|
|
42
|
+
* 'replace_block'에는 최종 결합될 순수한 XML 코드만 작성하세요.
|
|
43
|
+
|
|
44
|
+
---
|
|
41
45
|
|
|
42
46
|
---
|
|
43
47
|
|
|
44
48
|
**[반환 JSON 포맷 명세]**
|
|
45
|
-
최종 출력은 반드시 아래의 JSON 스키마 구조를 따라야 합니다. 단, `full_source`, `search_block`, `replace_block` 필드에 실제
|
|
49
|
+
최종 출력은 반드시 아래의 JSON 스키마 구조를 따라야 합니다. 단, `full_source`, `search_block`, `replace_block` 필드에 실제 코드가 들어갈 때는 최종 출력 시점에 한해 JSON 규격에 맞게 문자열 이스케이프(`\n`, `\"`) 처리가 자동으로 적용되어야 합니다.
|
|
46
50
|
|
|
47
51
|
{{
|
|
48
52
|
"mode": "CREATE 또는 UPDATE",
|
|
@@ -57,17 +61,20 @@
|
|
|
57
61
|
|
|
58
62
|
---
|
|
59
63
|
|
|
60
|
-
**[참고용 데이터 예시 - CREATE 모드일 때 full_source 구성 요령
|
|
64
|
+
**[참고용 데이터 예시 - CREATE 모드일 때 full_source 구성 요령]**
|
|
61
65
|
`mode`가 "CREATE"인 경우, `full_source` 필드에는 이스케이프 처리되기 전 기준으로 아래와 같이 줄바꿈과 들여쓰기가 온전히 살아있는 스케폴딩 코드가 매핑되어야 합니다. (수정 시 아래 코드만 편하게 편집하세요.)
|
|
62
66
|
|
|
63
67
|
[full_source 템플릿 코드 구조]
|
|
64
68
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
65
|
-
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
69
|
+
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "[http://mybatis.org/dtd/mybatis-3-mapper.dtd](http://mybatis.org/dtd/mybatis-3-mapper.dtd)">
|
|
66
70
|
<mapper namespace="{namespace}">
|
|
67
71
|
<select id="selectList" resultType="{{result_type}}">
|
|
68
72
|
SELECT USER_ID FROM TB_USER
|
|
69
73
|
<where>
|
|
70
74
|
<if test="_nineAiGenerated != null"> AND ${{_nineAiGenerated}} </if>
|
|
75
|
+
<if test="userId != null and userId != ''">
|
|
76
|
+
AND USER_ID = #{{userId}}
|
|
77
|
+
</if>
|
|
71
78
|
</where>
|
|
72
79
|
ORDER BY REG_DT DESC
|
|
73
80
|
</select>
|
|
@@ -75,16 +82,16 @@
|
|
|
75
82
|
|
|
76
83
|
---
|
|
77
84
|
|
|
78
|
-
**[참고용 데이터 예시 - UPDATE 모드일 때 source_changes 구성 요령
|
|
85
|
+
**[참고용 데이터 예시 - UPDATE 모드일 때 source_changes 구성 요령]**
|
|
79
86
|
`mode`가 "UPDATE"인 경우, `source_changes` 배열 내부의 `search_block`과 `replace_block`은 이스케이프 처리되기 전 기준으로 아래 구조 형식을 따릅니다. (수정 시 아래 코드만 편하게 편집하세요.)
|
|
80
87
|
|
|
81
88
|
[search_block 구조 예시]
|
|
82
|
-
<if test="userId != null">
|
|
83
|
-
|
|
89
|
+
<if test="userId != null and userId != ''">
|
|
90
|
+
AND USER_ID = #{{userId}}
|
|
84
91
|
</if>
|
|
85
92
|
|
|
86
93
|
[replace_block 구조 예시]
|
|
87
|
-
<if test="userId != null">
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
<if test="userId != null and userId != ''">
|
|
95
|
+
AND USER_ID = #{{userId}}
|
|
96
|
+
AND USE_YN = 'Y'
|
|
90
97
|
</if>
|
|
@@ -17,31 +17,37 @@
|
|
|
17
17
|
**[XML 작성 및 수정 필수 조건 (PostgreSQL 전용)]**
|
|
18
18
|
|
|
19
19
|
1. XML 공통 규칙 (LangChain 템플릿 변수 충돌 방지를 위해 이중 중괄호 표현 사용):
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
- resultType 속성은 무조건 플레이스홀더 값인 '{result_type}'으로 처리합니다.
|
|
21
|
+
- AI 너가 최종 출력물로 뱉어야 하는 모든 MyBatis 파라미터 양식은 오직 단일 중괄호 형태인 '#{{variable_name}}' 및 '${{variable_name}}' 구조여야 합니다. (주의: 최종 출력 결과물에 중괄호가 3개 이상 중첩되어 템플릿 문법이 깨지지 않도록 절대 주의하세요.)
|
|
22
|
+
- selectList 쿼리에는 동적 검색 조건 처리를 위해 반드시 아래 구문을 주입하고 ORDER BY 절을 명시해야 합니다.
|
|
23
|
+
<where>
|
|
24
|
+
<if test="_nineAiGenerated != null"> AND ${{_nineAiGenerated}} </if>
|
|
25
|
+
</where>
|
|
26
|
+
- [중요 - XML 부등호 처리]: SQL 문 내에서 비교 연산자(예: <, >, <=, >=)를 사용할 때 XML 파싱 에러를 방지하기 위해, 해당 쿼리문 블록을 반드시 `<![CDATA[ SQL구문 ]]>`으로 감싸거나 `<`, `>`, `<=`, `>=` 등의 XML 엔티티로 변환하여 작성하십시오. (동적 태그 <if> 등과 겹치지 않도록 주의할 것)
|
|
27
|
+
- [네임스페이스 처리 규칙]: <mapper namespace="{namespace}"> 선언 시, 외부 인자로 제공된 `{namespace}` 플레이스홀더 값은 자바 백엔드가 런타임에 삽입해 주는 '절대 고정값 변수'입니다. AI 너가 임의로 패키지 경로를 유추하여 하드코딩하는 행위를 일절 엄금하며, 반드시 원본 텍스트 형태 그대로 문장 속에 `{namespace}`를 유지하여 출력하십시오.
|
|
26
28
|
|
|
27
29
|
2. PostgreSQL SQL 작성 준수 사항:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
- [식별자 대소문자 및 쌍따옴표 엄격 처리]: PostgreSQL은 쌍따옴표(`""`)처리가 없으면 SQL 파서가 모든 테이블명과 컬럼명을 자동으로 소문자로 변환하여 인식합니다. 따라서 제공된 [테이블 스키마 정보]의 식별자가 대문자이거나 혼용 표기(CamelCase) 형태라면, 쿼리 작성 시 반드시 테이블명과 컬럼명을 쌍따옴표(`""`)로 완전히 감싸서 처리하십시오. (예: SELECT "USER_ID" FROM "TB_USER") 단, 문자열 데이터 값 자체는 반드시 홑따옴표(`''`)로 감싸야 합니다.
|
|
31
|
+
- 날짜/시간 함수: 등록일(REG_DT)/수정일(MOD_DT) 등의 타임스탬프 필드 자리에 PostgreSQL 전용 표준 표현식인 'CURRENT_TIMESTAMP' 또는 'NOW()'를 사용하십시오.
|
|
32
|
+
- [타입 엄격성 대응]: PostgreSQL은 데이터 타입 간 자동 묵시적 형변환을 거의 허용하지 않습니다. 비교 조건이나 삽입 시점에 파라미터와 컬럼의 데이터 타입이 불일치할 가능성이 있다면, 필요에 따라 명시적 타입 캐스팅 구문(예: #{{userId}}::INTEGER 또는 CAST(#{{regDt}} AS TIMESTAMP))을 주입하여 에러를 미연에 방지하십시오.
|
|
33
|
+
- [동적 쿼리 파라미터 검증 규칙]: <if test="..."> 내에서 검색 조건을 비교할 때는 파라미터의 데이터 타입(숫자형, 문자열 등)을 불문하고, 모든 조건에 대해 반드시 `변수명 != null and 변수명 != ''` 형태로 null 체크와 빈 문자열 체크를 동시에 결합하여 선언하도록 강제하십시오. (단, 숫자 0의 오작동을 방지할 수 있도록 프로젝트 규격을 통일합니다.)
|
|
30
34
|
|
|
31
35
|
3. 출력 형식 및 JSON 제약사항 (필수):
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
- **너의 최종 응답은 오직 아래 지정된 [반환 JSON 포맷] 양식에 맞는 순수한 JSON 데이터여야 합니다.** (설명 텍스트나 마크다운 코드 블록 ```json 절대 금지)
|
|
37
|
+
- JSON 내부 문자열에 쌍따옴표(")나 개행(\n), 탭(\t)이 들어갈 경우, JSON 규격이 깨지지 않도록 반드시 백슬래시를 붙여 '\"' 또는 '\\n' 형태로 완벽하게 이스케이프(Escape) 처리를 하십시오.
|
|
38
|
+
- [CREATE 모드]일 때는 전체 XML 코드를 설계하여 `full_source` 필드에 채우고, `source_changes` 배열은 빈 배열(`[]`)로 반환합니다.
|
|
39
|
+
* 반드시 `<?xml...>`, `<!DOCTYPE...>`, `<mapper namespace="{namespace}">` 태그가 완전히 포함된 스케폴딩 구조여야 합니다. (이때 namespace 자리는 절대 다른 문자열로 치환하지 말고 텍스트 그대로 유지할 것)
|
|
40
|
+
* 사용자의 별도 요청이 없더라도 스키마를 분석하여 기본 4대 핵심 CRUD 쿼리인 `selectList`, `selectOne`, `insert`, `update`를 기본 탑재하십시오.
|
|
41
|
+
- [UPDATE 모드]일 때는 `full_source` 필드를 빈 문자열(`""`)로 채우고, 변경 사항을 `source_changes` 배열에 담아 반환합니다.
|
|
42
|
+
* 'search_block'을 지정할 때는 소스 내에서 유일하게 식별(Unique)되도록 앞뒤로 최소 2라인 이상의 컨텍스트 코드를 반드시 포함해야 합니다.
|
|
43
|
+
* 'replace_block'에는 최종 결합될 순수한 XML 코드만 작성하세요.
|
|
44
|
+
|
|
45
|
+
---
|
|
40
46
|
|
|
41
47
|
---
|
|
42
48
|
|
|
43
49
|
**[반환 JSON 포맷 명세]**
|
|
44
|
-
최종 출력은 반드시 아래의 JSON 스키마 구조를 따라야 합니다. 단, `full_source`, `search_block`, `replace_block` 필드에 실제
|
|
50
|
+
최종 출력은 반드시 아래의 JSON 스키마 구조를 따라야 합니다. 단, `full_source`, `search_block`, `replace_block` 필드에 실제 코드가 들어갈 때는 최종 출력 시점에 한해 JSON 규격에 맞게 문자열 이스케이프(`\n`, `\"`) 처리가 자동으로 적용되어야 합니다.
|
|
45
51
|
|
|
46
52
|
{{
|
|
47
53
|
"mode": "CREATE 또는 UPDATE",
|
|
@@ -56,34 +62,37 @@
|
|
|
56
62
|
|
|
57
63
|
---
|
|
58
64
|
|
|
59
|
-
**[참고용 데이터 예시 - CREATE 모드일 때 full_source 구성 요령
|
|
60
|
-
`mode`가 "CREATE"인 경우, `full_source` 필드에는 이스케이프 처리되기 전 기준으로 아래와 같이 줄바꿈과
|
|
65
|
+
**[참고용 데이터 예시 - CREATE 모드일 때 full_source 구성 요령]**
|
|
66
|
+
`mode`가 "CREATE"인 경우, `full_source` 필드에는 이스케이프 처리되기 전 기준으로 아래와 같이 줄바꿈과 들여쓰기가 온전히 살아있는 스케폴딩 코드가 매핑되어야 합니다. (수정 시 아래 코드만 편하게 편집하세요.)
|
|
61
67
|
|
|
62
68
|
[full_source 템플릿 코드 구조]
|
|
63
69
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
64
70
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
65
71
|
<mapper namespace="{namespace}">
|
|
66
72
|
<select id="selectList" resultType="{{result_type}}">
|
|
67
|
-
SELECT
|
|
73
|
+
SELECT "USER_ID" FROM "TB_USER"
|
|
68
74
|
<where>
|
|
69
75
|
<if test="_nineAiGenerated != null"> AND ${{_nineAiGenerated}} </if>
|
|
76
|
+
<if test="userId != null and userId != ''">
|
|
77
|
+
AND "USER_ID" = #{{userId}}
|
|
78
|
+
</if>
|
|
70
79
|
</where>
|
|
71
|
-
ORDER BY
|
|
80
|
+
ORDER BY "REG_DT" DESC
|
|
72
81
|
</select>
|
|
73
82
|
</mapper>
|
|
74
83
|
|
|
75
84
|
---
|
|
76
85
|
|
|
77
|
-
**[참고용 데이터 예시 - UPDATE 모드일 때 source_changes 구성 요령
|
|
86
|
+
**[참고용 데이터 예시 - UPDATE 모드일 때 source_changes 구성 요령]**
|
|
78
87
|
`mode`가 "UPDATE"인 경우, `source_changes` 배열 내부의 `search_block`과 `replace_block`은 이스케이프 처리되기 전 기준으로 아래 구조 형식을 따릅니다. (수정 시 아래 코드만 편하게 편집하세요.)
|
|
79
88
|
|
|
80
89
|
[search_block 구조 예시]
|
|
81
|
-
<if test="userId != null">
|
|
82
|
-
|
|
90
|
+
<if test="userId != null and userId != ''">
|
|
91
|
+
AND "USER_ID" = #{{userId}}
|
|
83
92
|
</if>
|
|
84
93
|
|
|
85
94
|
[replace_block 구조 예시]
|
|
86
|
-
<if test="userId != null">
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
<if test="userId != null and userId != ''">
|
|
96
|
+
AND "USER_ID" = #{{userId}}
|
|
97
|
+
AND "USE_YN" = 'Y'
|
|
89
98
|
</if>
|
|
@@ -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,
|