activo 0.2.2 → 0.3.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.
Files changed (64) hide show
  1. package/README.md +79 -3
  2. package/dist/core/llm/ollama.d.ts +2 -0
  3. package/dist/core/llm/ollama.d.ts.map +1 -1
  4. package/dist/core/llm/ollama.js +26 -0
  5. package/dist/core/llm/ollama.js.map +1 -1
  6. package/dist/core/tools/ast.d.ts +81 -0
  7. package/dist/core/tools/ast.d.ts.map +1 -0
  8. package/dist/core/tools/ast.js +700 -0
  9. package/dist/core/tools/ast.js.map +1 -0
  10. package/dist/core/tools/cache.d.ts +19 -0
  11. package/dist/core/tools/cache.d.ts.map +1 -0
  12. package/dist/core/tools/cache.js +497 -0
  13. package/dist/core/tools/cache.js.map +1 -0
  14. package/dist/core/tools/cssAnalysis.d.ts +3 -0
  15. package/dist/core/tools/cssAnalysis.d.ts.map +1 -0
  16. package/dist/core/tools/cssAnalysis.js +270 -0
  17. package/dist/core/tools/cssAnalysis.js.map +1 -0
  18. package/dist/core/tools/embeddings.d.ts +8 -0
  19. package/dist/core/tools/embeddings.d.ts.map +1 -0
  20. package/dist/core/tools/embeddings.js +631 -0
  21. package/dist/core/tools/embeddings.js.map +1 -0
  22. package/dist/core/tools/frontendAst.d.ts +6 -0
  23. package/dist/core/tools/frontendAst.d.ts.map +1 -0
  24. package/dist/core/tools/frontendAst.js +680 -0
  25. package/dist/core/tools/frontendAst.js.map +1 -0
  26. package/dist/core/tools/htmlAnalysis.d.ts +3 -0
  27. package/dist/core/tools/htmlAnalysis.d.ts.map +1 -0
  28. package/dist/core/tools/htmlAnalysis.js +398 -0
  29. package/dist/core/tools/htmlAnalysis.js.map +1 -0
  30. package/dist/core/tools/index.d.ts +10 -0
  31. package/dist/core/tools/index.d.ts.map +1 -1
  32. package/dist/core/tools/index.js +21 -1
  33. package/dist/core/tools/index.js.map +1 -1
  34. package/dist/core/tools/javaAst.d.ts +6 -0
  35. package/dist/core/tools/javaAst.d.ts.map +1 -0
  36. package/dist/core/tools/javaAst.js +678 -0
  37. package/dist/core/tools/javaAst.js.map +1 -0
  38. package/dist/core/tools/memory.d.ts +11 -0
  39. package/dist/core/tools/memory.d.ts.map +1 -0
  40. package/dist/core/tools/memory.js +551 -0
  41. package/dist/core/tools/memory.js.map +1 -0
  42. package/dist/core/tools/mybatisAnalysis.d.ts +3 -0
  43. package/dist/core/tools/mybatisAnalysis.d.ts.map +1 -0
  44. package/dist/core/tools/mybatisAnalysis.js +251 -0
  45. package/dist/core/tools/mybatisAnalysis.js.map +1 -0
  46. package/dist/core/tools/sqlAnalysis.d.ts +3 -0
  47. package/dist/core/tools/sqlAnalysis.d.ts.map +1 -0
  48. package/dist/core/tools/sqlAnalysis.js +250 -0
  49. package/dist/core/tools/sqlAnalysis.js.map +1 -0
  50. package/package.json +2 -1
  51. package/src/core/llm/ollama.ts +30 -0
  52. package/src/core/tools/ast.ts +826 -0
  53. package/src/core/tools/cache.ts +570 -0
  54. package/src/core/tools/cssAnalysis.ts +324 -0
  55. package/src/core/tools/embeddings.ts +746 -0
  56. package/src/core/tools/frontendAst.ts +802 -0
  57. package/src/core/tools/htmlAnalysis.ts +466 -0
  58. package/src/core/tools/index.ts +21 -1
  59. package/src/core/tools/javaAst.ts +812 -0
  60. package/src/core/tools/memory.ts +655 -0
  61. package/src/core/tools/mybatisAnalysis.ts +322 -0
  62. package/src/core/tools/sqlAnalysis.ts +298 -0
  63. package/FINAL_SIMPLIFIED_SPEC.md +0 -456
  64. package/TODO.md +0 -193
@@ -0,0 +1,322 @@
1
+ import { Tool, ToolResult } from "./types.js";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ interface MybatisStatement {
6
+ id: string;
7
+ type: "select" | "insert" | "update" | "delete" | "sql";
8
+ parameterType?: string;
9
+ resultType?: string;
10
+ resultMap?: string;
11
+ sql: string;
12
+ line: number;
13
+ issues: string[];
14
+ dynamicElements: string[];
15
+ }
16
+
17
+ interface MybatisMapper {
18
+ file: string;
19
+ namespace: string;
20
+ statements: MybatisStatement[];
21
+ resultMaps: string[];
22
+ sqlFragments: string[];
23
+ issues: string[];
24
+ }
25
+
26
+ interface MybatisAnalysisResult {
27
+ file: string;
28
+ isMyBatis: boolean;
29
+ mapper?: MybatisMapper;
30
+ summary: {
31
+ statements: number;
32
+ withIssues: number;
33
+ injectionRisks: number;
34
+ dynamicSqlCount: number;
35
+ };
36
+ }
37
+
38
+ // MyBatis XML 파싱 및 분석
39
+ function analyzeMyBatisXml(filePath: string): MybatisAnalysisResult {
40
+ const content = fs.readFileSync(filePath, "utf-8");
41
+ const lines = content.split("\n");
42
+
43
+ // MyBatis XML 여부 확인
44
+ const isMyBatis =
45
+ content.includes("mybatis") ||
46
+ content.includes("ibatis") ||
47
+ content.includes('PUBLIC "-//mybatis.org') ||
48
+ content.includes('PUBLIC "-//ibatis.org') ||
49
+ /<mapper\s+namespace=/i.test(content);
50
+
51
+ if (!isMyBatis) {
52
+ return {
53
+ file: filePath,
54
+ isMyBatis: false,
55
+ summary: { statements: 0, withIssues: 0, injectionRisks: 0, dynamicSqlCount: 0 },
56
+ };
57
+ }
58
+
59
+ const statements: MybatisStatement[] = [];
60
+ const resultMaps: string[] = [];
61
+ const sqlFragments: string[] = [];
62
+ const mapperIssues: string[] = [];
63
+
64
+ // namespace 추출
65
+ const namespaceMatch = content.match(/<mapper\s+namespace=["']([^"']+)["']/);
66
+ const namespace = namespaceMatch ? namespaceMatch[1] : "unknown";
67
+
68
+ // resultMap 추출
69
+ const resultMapRegex = /<resultMap\s+[^>]*id=["']([^"']+)["'][^>]*>/g;
70
+ let match;
71
+ while ((match = resultMapRegex.exec(content)) !== null) {
72
+ resultMaps.push(match[1]);
73
+ }
74
+
75
+ // sql fragment 추출
76
+ const sqlFragmentRegex = /<sql\s+[^>]*id=["']([^"']+)["'][^>]*>/g;
77
+ while ((match = sqlFragmentRegex.exec(content)) !== null) {
78
+ sqlFragments.push(match[1]);
79
+ }
80
+
81
+ // statement 추출 함수
82
+ const extractStatements = (type: "select" | "insert" | "update" | "delete") => {
83
+ const regex = new RegExp(
84
+ `<${type}\\s+([^>]*)>([\\s\\S]*?)</${type}>`,
85
+ "gi"
86
+ );
87
+
88
+ while ((match = regex.exec(content)) !== null) {
89
+ const attrs = match[1];
90
+ const sqlContent = match[2];
91
+
92
+ // 속성 파싱
93
+ const idMatch = attrs.match(/id=["']([^"']+)["']/);
94
+ const paramMatch = attrs.match(/parameterType=["']([^"']+)["']/);
95
+ const resultTypeMatch = attrs.match(/resultType=["']([^"']+)["']/);
96
+ const resultMapMatch = attrs.match(/resultMap=["']([^"']+)["']/);
97
+
98
+ const id = idMatch ? idMatch[1] : "unknown";
99
+ const issues: string[] = [];
100
+ const dynamicElements: string[] = [];
101
+
102
+ // ${} 사용 검사 (SQL Injection 위험)
103
+ const dollarBraceMatches = sqlContent.match(/\$\{[^}]+\}/g) || [];
104
+ if (dollarBraceMatches.length > 0) {
105
+ issues.push(`SQL Injection 위험: ${dollarBraceMatches.join(", ")} 사용`);
106
+ }
107
+
108
+ // 동적 SQL 요소 추출
109
+ const dynamicTags = ["if", "choose", "when", "otherwise", "where", "set", "foreach", "trim", "bind"];
110
+ dynamicTags.forEach((tag) => {
111
+ const tagRegex = new RegExp(`<${tag}[\\s>]`, "gi");
112
+ const count = (sqlContent.match(tagRegex) || []).length;
113
+ if (count > 0) {
114
+ dynamicElements.push(`${tag}(${count})`);
115
+ }
116
+ });
117
+
118
+ // SELECT * 검사
119
+ if (/SELECT\s+\*\s+FROM/i.test(sqlContent)) {
120
+ issues.push("SELECT * 사용");
121
+ }
122
+
123
+ // WHERE 없는 UPDATE/DELETE
124
+ if ((type === "update" || type === "delete") && !/<where>|WHERE/i.test(sqlContent)) {
125
+ issues.push("WHERE 절 없음 - 전체 테이블 영향 위험");
126
+ }
127
+
128
+ // 복잡한 동적 SQL
129
+ if (dynamicElements.length >= 5) {
130
+ issues.push(`복잡한 동적 SQL (${dynamicElements.length}개 요소)`);
131
+ }
132
+
133
+ // 중첩 foreach
134
+ const foreachCount = (sqlContent.match(/<foreach/gi) || []).length;
135
+ if (foreachCount >= 2) {
136
+ issues.push(`중첩 foreach ${foreachCount}개 - 성능 검토 필요`);
137
+ }
138
+
139
+ // LIKE '%${...}%' 패턴
140
+ if (/LIKE\s+['"]?%?\$\{/i.test(sqlContent)) {
141
+ issues.push("LIKE + ${} 패턴 - SQL Injection 및 인덱스 문제");
142
+ }
143
+
144
+ // ORDER BY ${} 패턴
145
+ if (/ORDER\s+BY\s+\$\{/i.test(sqlContent)) {
146
+ issues.push("ORDER BY ${} - SQL Injection 위험");
147
+ }
148
+
149
+ // 라인 번호 찾기
150
+ const statementIndex = content.indexOf(match[0]);
151
+ let lineNum = 1;
152
+ for (let i = 0; i < statementIndex; i++) {
153
+ if (content[i] === "\n") lineNum++;
154
+ }
155
+
156
+ // SQL 정리
157
+ const cleanSql = sqlContent
158
+ .replace(/<[^>]+>/g, " ") // XML 태그 제거
159
+ .replace(/\s+/g, " ")
160
+ .trim();
161
+
162
+ statements.push({
163
+ id,
164
+ type,
165
+ parameterType: paramMatch ? paramMatch[1] : undefined,
166
+ resultType: resultTypeMatch ? resultTypeMatch[1] : undefined,
167
+ resultMap: resultMapMatch ? resultMapMatch[1] : undefined,
168
+ sql: cleanSql,
169
+ line: lineNum,
170
+ issues,
171
+ dynamicElements,
172
+ });
173
+ }
174
+ };
175
+
176
+ // 각 statement 타입 처리
177
+ extractStatements("select");
178
+ extractStatements("insert");
179
+ extractStatements("update");
180
+ extractStatements("delete");
181
+
182
+ // mapper 레벨 이슈 검사
183
+ if (!namespaceMatch) {
184
+ mapperIssues.push("namespace 미정의");
185
+ }
186
+
187
+ // 미사용 resultMap 검사 (간단한 체크)
188
+ resultMaps.forEach((rm) => {
189
+ const usageCount = (content.match(new RegExp(`resultMap=["']${rm}["']`, "g")) || []).length;
190
+ if (usageCount <= 1) {
191
+ // 정의만 있고 사용 없음
192
+ mapperIssues.push(`미사용 가능성: resultMap '${rm}'`);
193
+ }
194
+ });
195
+
196
+ // 통계
197
+ const injectionRisks = statements.filter((s) =>
198
+ s.issues.some((i) => i.includes("Injection"))
199
+ ).length;
200
+ const dynamicSqlCount = statements.filter((s) => s.dynamicElements.length > 0).length;
201
+
202
+ return {
203
+ file: filePath,
204
+ isMyBatis: true,
205
+ mapper: {
206
+ file: filePath,
207
+ namespace,
208
+ statements,
209
+ resultMaps,
210
+ sqlFragments,
211
+ issues: mapperIssues,
212
+ },
213
+ summary: {
214
+ statements: statements.length,
215
+ withIssues: statements.filter((s) => s.issues.length > 0).length,
216
+ injectionRisks,
217
+ dynamicSqlCount,
218
+ },
219
+ };
220
+ }
221
+
222
+ // 도구 정의
223
+ export const mybatisTools: Tool[] = [
224
+ {
225
+ name: "mybatis_check",
226
+ description:
227
+ "MyBatis XML 매퍼 파일을 분석합니다. ${} SQL Injection 위험, 동적 SQL 복잡도, SELECT *, resultMap 사용 등을 검사합니다. DTD/namespace로 MyBatis 파일을 자동 감지합니다.",
228
+ parameters: {
229
+ type: "object",
230
+ properties: {
231
+ path: {
232
+ type: "string",
233
+ description: "분석할 XML 파일 또는 디렉토리 경로",
234
+ },
235
+ recursive: {
236
+ type: "boolean",
237
+ description: "디렉토리인 경우 하위 폴더 포함 여부 (기본: true)",
238
+ },
239
+ },
240
+ required: ["path"],
241
+ },
242
+ handler: async (args: Record<string, unknown>): Promise<ToolResult> => {
243
+ const targetPath = args.path as string;
244
+ const recursive = args.recursive !== false;
245
+
246
+ if (!fs.existsSync(targetPath)) {
247
+ return {
248
+ success: false,
249
+ content: "",
250
+ error: `경로를 찾을 수 없습니다: ${targetPath}`,
251
+ };
252
+ }
253
+
254
+ const results: MybatisAnalysisResult[] = [];
255
+ const stats = fs.statSync(targetPath);
256
+
257
+ if (stats.isFile()) {
258
+ if (targetPath.endsWith(".xml")) {
259
+ const result = analyzeMyBatisXml(targetPath);
260
+ if (result.isMyBatis) {
261
+ results.push(result);
262
+ }
263
+ }
264
+ } else if (stats.isDirectory()) {
265
+ const walkDir = (dir: string) => {
266
+ const files = fs.readdirSync(dir);
267
+ for (const file of files) {
268
+ const filePath = path.join(dir, file);
269
+ const fileStat = fs.statSync(filePath);
270
+ if (fileStat.isDirectory() && recursive) {
271
+ if (!file.startsWith(".") && file !== "node_modules" && file !== "target" && file !== "build") {
272
+ walkDir(filePath);
273
+ }
274
+ } else if (file.endsWith(".xml")) {
275
+ const result = analyzeMyBatisXml(filePath);
276
+ if (result.isMyBatis) {
277
+ results.push(result);
278
+ }
279
+ }
280
+ }
281
+ };
282
+ walkDir(targetPath);
283
+ }
284
+
285
+ // 전체 통계
286
+ const totalStatements = results.reduce((sum, r) => sum + r.summary.statements, 0);
287
+ const totalWithIssues = results.reduce((sum, r) => sum + r.summary.withIssues, 0);
288
+ const totalInjectionRisks = results.reduce((sum, r) => sum + r.summary.injectionRisks, 0);
289
+ const totalDynamicSql = results.reduce((sum, r) => sum + r.summary.dynamicSqlCount, 0);
290
+
291
+ const output = {
292
+ analyzedMappers: results.length,
293
+ totalStatements,
294
+ statementsWithIssues: totalWithIssues,
295
+ injectionRisks: totalInjectionRisks,
296
+ dynamicSqlStatements: totalDynamicSql,
297
+ mappers: results.map((r) => ({
298
+ file: r.file,
299
+ namespace: r.mapper?.namespace,
300
+ resultMaps: r.mapper?.resultMaps,
301
+ sqlFragments: r.mapper?.sqlFragments,
302
+ mapperIssues: r.mapper?.issues,
303
+ statements: r.mapper?.statements.map((s) => ({
304
+ id: s.id,
305
+ type: s.type,
306
+ line: s.line,
307
+ parameterType: s.parameterType,
308
+ resultType: s.resultType || s.resultMap,
309
+ dynamicElements: s.dynamicElements,
310
+ issues: s.issues,
311
+ sql: s.sql.length > 150 ? s.sql.substring(0, 150) + "..." : s.sql,
312
+ })),
313
+ })),
314
+ };
315
+
316
+ return {
317
+ success: true,
318
+ content: JSON.stringify(output, null, 2),
319
+ };
320
+ },
321
+ },
322
+ ];
@@ -0,0 +1,298 @@
1
+ import { Tool, ToolResult } from "./types.js";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ interface SqlQuery {
6
+ type: "jpa" | "jdbc" | "native" | "jpql";
7
+ query: string;
8
+ location: string;
9
+ line: number;
10
+ issues: string[];
11
+ }
12
+
13
+ interface SqlAnalysisResult {
14
+ file: string;
15
+ queries: SqlQuery[];
16
+ summary: {
17
+ total: number;
18
+ withIssues: number;
19
+ issueTypes: Record<string, number>;
20
+ };
21
+ }
22
+
23
+ // SQL 쿼리 추출 및 분석
24
+ function analyzeJavaForSql(filePath: string): SqlAnalysisResult {
25
+ const content = fs.readFileSync(filePath, "utf-8");
26
+ const lines = content.split("\n");
27
+ const queries: SqlQuery[] = [];
28
+
29
+ // @Query 어노테이션 찾기
30
+ const queryAnnotationRegex = /@Query\s*\(\s*(?:value\s*=\s*)?["'`]([^"'`]+)["'`]/g;
31
+ const nativeQueryRegex = /@Query\s*\([^)]*nativeQuery\s*=\s*true[^)]*value\s*=\s*["'`]([^"'`]+)["'`]/g;
32
+ const namedQueryRegex = /@NamedQuery\s*\([^)]*query\s*=\s*["'`]([^"'`]+)["'`]/g;
33
+
34
+ // JDBC/JPA 문자열 쿼리 찾기
35
+ const createQueryRegex = /(?:createQuery|createNativeQuery|prepareStatement)\s*\(\s*["'`]([^"'`]+)["'`]/g;
36
+ const jdbcExecuteRegex = /(?:executeQuery|executeUpdate|execute)\s*\(\s*["'`]([^"'`]+)["'`]/g;
37
+
38
+ // 문자열 변수에 할당된 SQL 찾기
39
+ const sqlStringRegex = /(?:String\s+)?(?:sql|query|hql|jpql)\s*=\s*["'`]([^"'`]*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)[^"'`]*)["'`]/gi;
40
+
41
+ // 멀티라인 SQL 문자열 찾기
42
+ const multiLineSqlRegex = /["'`]\s*(SELECT|INSERT|UPDATE|DELETE)\s+[\s\S]*?["'`]\s*(?:\+\s*["'`][\s\S]*?["'`]\s*)*/gi;
43
+
44
+ const findLineNumber = (match: RegExpExecArray): number => {
45
+ const index = match.index;
46
+ let lineNum = 1;
47
+ for (let i = 0; i < index && i < content.length; i++) {
48
+ if (content[i] === "\n") lineNum++;
49
+ }
50
+ return lineNum;
51
+ };
52
+
53
+ const analyzeQuery = (sql: string): string[] => {
54
+ const issues: string[] = [];
55
+ const upperSql = sql.toUpperCase().trim();
56
+
57
+ // SELECT * 검사
58
+ if (/SELECT\s+\*\s+FROM/i.test(sql)) {
59
+ issues.push("SELECT * 사용 - 필요한 컬럼만 명시 권장");
60
+ }
61
+
62
+ // WHERE 절 없는 UPDATE/DELETE
63
+ if (/^(UPDATE|DELETE)\s+/i.test(upperSql) && !/WHERE/i.test(sql)) {
64
+ issues.push("WHERE 절 없음 - 전체 테이블 영향 위험");
65
+ }
66
+
67
+ // 서브쿼리 검사
68
+ const subqueryCount = (sql.match(/\(\s*SELECT/gi) || []).length;
69
+ if (subqueryCount >= 2) {
70
+ issues.push(`중첩 서브쿼리 ${subqueryCount}개 - 성능 저하 가능`);
71
+ }
72
+
73
+ // JOIN 개수 검사
74
+ const joinCount = (sql.match(/\bJOIN\b/gi) || []).length;
75
+ if (joinCount >= 4) {
76
+ issues.push(`JOIN ${joinCount}개 - 쿼리 복잡도 높음`);
77
+ }
78
+
79
+ // LIKE '%...' 패턴 (인덱스 미사용)
80
+ if (/LIKE\s+['"]%/i.test(sql)) {
81
+ issues.push("LIKE '%...' 패턴 - 인덱스 사용 불가");
82
+ }
83
+
84
+ // OR 조건 과다
85
+ const orCount = (sql.match(/\bOR\b/gi) || []).length;
86
+ if (orCount >= 3) {
87
+ issues.push(`OR 조건 ${orCount}개 - IN 절로 변환 검토`);
88
+ }
89
+
90
+ // ORDER BY RAND() 검사
91
+ if (/ORDER\s+BY\s+RAND\s*\(\)/i.test(sql)) {
92
+ issues.push("ORDER BY RAND() - 대용량 테이블에서 성능 저하");
93
+ }
94
+
95
+ // N+1 가능성 힌트 (단순 ID 조회)
96
+ if (/WHERE\s+\w+\.?id\s*=\s*[?:]/i.test(sql) && /SELECT/i.test(sql)) {
97
+ issues.push("단일 ID 조회 - 반복 호출 시 N+1 문제 가능");
98
+ }
99
+
100
+ // 문자열 연결 (SQL Injection 위험)
101
+ if (/\+\s*["']|["']\s*\+/.test(sql)) {
102
+ issues.push("문자열 연결 감지 - SQL Injection 위험");
103
+ }
104
+
105
+ return issues;
106
+ };
107
+
108
+ // @Query 어노테이션 처리
109
+ let match;
110
+ while ((match = queryAnnotationRegex.exec(content)) !== null) {
111
+ const query = match[1].replace(/\s+/g, " ").trim();
112
+ queries.push({
113
+ type: "jpql",
114
+ query,
115
+ location: "@Query",
116
+ line: findLineNumber(match),
117
+ issues: analyzeQuery(query),
118
+ });
119
+ }
120
+
121
+ // Native Query 처리
122
+ while ((match = nativeQueryRegex.exec(content)) !== null) {
123
+ const query = match[1].replace(/\s+/g, " ").trim();
124
+ queries.push({
125
+ type: "native",
126
+ query,
127
+ location: "@Query(nativeQuery)",
128
+ line: findLineNumber(match),
129
+ issues: analyzeQuery(query),
130
+ });
131
+ }
132
+
133
+ // @NamedQuery 처리
134
+ while ((match = namedQueryRegex.exec(content)) !== null) {
135
+ const query = match[1].replace(/\s+/g, " ").trim();
136
+ queries.push({
137
+ type: "jpa",
138
+ query,
139
+ location: "@NamedQuery",
140
+ line: findLineNumber(match),
141
+ issues: analyzeQuery(query),
142
+ });
143
+ }
144
+
145
+ // createQuery/prepareStatement 처리
146
+ while ((match = createQueryRegex.exec(content)) !== null) {
147
+ const query = match[1].replace(/\s+/g, " ").trim();
148
+ if (/SELECT|INSERT|UPDATE|DELETE/i.test(query)) {
149
+ queries.push({
150
+ type: "jdbc",
151
+ query,
152
+ location: "createQuery/prepareStatement",
153
+ line: findLineNumber(match),
154
+ issues: analyzeQuery(query),
155
+ });
156
+ }
157
+ }
158
+
159
+ // executeQuery 처리
160
+ while ((match = jdbcExecuteRegex.exec(content)) !== null) {
161
+ const query = match[1].replace(/\s+/g, " ").trim();
162
+ if (/SELECT|INSERT|UPDATE|DELETE/i.test(query)) {
163
+ queries.push({
164
+ type: "jdbc",
165
+ query,
166
+ location: "execute*",
167
+ line: findLineNumber(match),
168
+ issues: analyzeQuery(query),
169
+ });
170
+ }
171
+ }
172
+
173
+ // SQL 문자열 변수 처리
174
+ while ((match = sqlStringRegex.exec(content)) !== null) {
175
+ const query = match[1].replace(/\s+/g, " ").trim();
176
+ queries.push({
177
+ type: "jdbc",
178
+ query,
179
+ location: "String 변수",
180
+ line: findLineNumber(match),
181
+ issues: analyzeQuery(query),
182
+ });
183
+ }
184
+
185
+ // 이슈 통계
186
+ const issueTypes: Record<string, number> = {};
187
+ queries.forEach((q) => {
188
+ q.issues.forEach((issue) => {
189
+ const key = issue.split(" - ")[0];
190
+ issueTypes[key] = (issueTypes[key] || 0) + 1;
191
+ });
192
+ });
193
+
194
+ return {
195
+ file: filePath,
196
+ queries,
197
+ summary: {
198
+ total: queries.length,
199
+ withIssues: queries.filter((q) => q.issues.length > 0).length,
200
+ issueTypes,
201
+ },
202
+ };
203
+ }
204
+
205
+ // 도구 정의
206
+ export const sqlTools: Tool[] = [
207
+ {
208
+ name: "sql_check",
209
+ description:
210
+ "Java 파일에서 SQL 쿼리를 추출하고 품질 검사합니다. @Query, JDBC, JPA 쿼리를 분석하여 SELECT *, 인덱스 미사용 패턴, N+1 가능성 등을 검출합니다.",
211
+ parameters: {
212
+ type: "object",
213
+ properties: {
214
+ path: {
215
+ type: "string",
216
+ description: "분석할 Java 파일 또는 디렉토리 경로",
217
+ },
218
+ recursive: {
219
+ type: "boolean",
220
+ description: "디렉토리인 경우 하위 폴더 포함 여부 (기본: true)",
221
+ },
222
+ },
223
+ required: ["path"],
224
+ },
225
+ handler: async (args: Record<string, unknown>): Promise<ToolResult> => {
226
+ const targetPath = args.path as string;
227
+ const recursive = args.recursive !== false;
228
+
229
+ if (!fs.existsSync(targetPath)) {
230
+ return {
231
+ success: false,
232
+ content: "",
233
+ error: `경로를 찾을 수 없습니다: ${targetPath}`,
234
+ };
235
+ }
236
+
237
+ const results: SqlAnalysisResult[] = [];
238
+ const stats = fs.statSync(targetPath);
239
+
240
+ if (stats.isFile()) {
241
+ if (targetPath.endsWith(".java")) {
242
+ results.push(analyzeJavaForSql(targetPath));
243
+ }
244
+ } else if (stats.isDirectory()) {
245
+ const walkDir = (dir: string) => {
246
+ const files = fs.readdirSync(dir);
247
+ for (const file of files) {
248
+ const filePath = path.join(dir, file);
249
+ const fileStat = fs.statSync(filePath);
250
+ if (fileStat.isDirectory() && recursive) {
251
+ if (!file.startsWith(".") && file !== "node_modules" && file !== "target" && file !== "build") {
252
+ walkDir(filePath);
253
+ }
254
+ } else if (file.endsWith(".java")) {
255
+ const result = analyzeJavaForSql(filePath);
256
+ if (result.queries.length > 0) {
257
+ results.push(result);
258
+ }
259
+ }
260
+ }
261
+ };
262
+ walkDir(targetPath);
263
+ }
264
+
265
+ // 전체 통계
266
+ const totalQueries = results.reduce((sum, r) => sum + r.summary.total, 0);
267
+ const totalWithIssues = results.reduce((sum, r) => sum + r.summary.withIssues, 0);
268
+ const allIssueTypes: Record<string, number> = {};
269
+ results.forEach((r) => {
270
+ Object.entries(r.summary.issueTypes).forEach(([key, count]) => {
271
+ allIssueTypes[key] = (allIssueTypes[key] || 0) + count;
272
+ });
273
+ });
274
+
275
+ const output = {
276
+ analyzed: results.length,
277
+ totalQueries,
278
+ queriesWithIssues: totalWithIssues,
279
+ issueTypes: allIssueTypes,
280
+ files: results.map((r) => ({
281
+ file: r.file,
282
+ queries: r.queries.map((q) => ({
283
+ line: q.line,
284
+ type: q.type,
285
+ location: q.location,
286
+ query: q.query.length > 100 ? q.query.substring(0, 100) + "..." : q.query,
287
+ issues: q.issues,
288
+ })),
289
+ })),
290
+ };
291
+
292
+ return {
293
+ success: true,
294
+ content: JSON.stringify(output, null, 2),
295
+ };
296
+ },
297
+ },
298
+ ];