@nine-lab/nine-connector 0.1.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/README.md ADDED
@@ -0,0 +1,67 @@
1
+ cat <<EOF > README.md
2
+ # @nine-lab/nine-connector ๐Ÿš€
3
+
4
+ ์ž์—ฐ์–ด ๊ธฐ๋ฐ˜ SQL ์ƒ์„ฑ ์—”์ง„(NineQuery)๊ณผ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์ด๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” **AI ์ „์šฉ DB ์ปค๋„ฅํ„ฐ ์„œ๋น„์Šค**์ž…๋‹ˆ๋‹ค.
5
+
6
+ AI๊ฐ€ ์ƒ์„ฑํ•œ SQL์„ ์•ˆ์ „ํ•˜๊ฒŒ ์‹คํ–‰ํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๊ตฌ์กฐ(Schema)๋ฅผ AI์—๊ฒŒ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ํ‘œ์ค€ API ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
7
+
8
+ ---
9
+
10
+ ## ๐ŸŒŸ ์ฃผ์š” ๊ธฐ๋Šฅ
11
+
12
+ * **Multi-DB ์ง€์›**: MySQL, PostgreSQL, MariaDB์™€ ์ฆ‰์‹œ ์—ฐ๊ฒฐ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
13
+ * **์Šคํ‚ค๋งˆ ์ž๋™ ์ถ”์ถœ**: AI๊ฐ€ ์ฟผ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ํ•„์š”ํ•œ ํ…Œ์ด๋ธ” ๋ฐ ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ API๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
14
+ * **๋ณด์•ˆ ๊ฐ€๋“œ**: ์˜ค์ง \`SELECT\` ์ฟผ๋ฆฌ๋งŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์–ด ๋ฐ์ดํ„ฐ ๋ณ€์กฐ๋ฅผ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
15
+ * **BigInt ๋Œ€์‘**: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํฐ ์ˆซ์žํ˜•(\`BigInt\`) ๋ฐ์ดํ„ฐ๋ฅผ JSON์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
16
+
17
+ ---
18
+
19
+ ## ๐Ÿ›  ์„ค์น˜ ๋ฐ ์‹คํ–‰ ๋ฐฉ๋ฒ•
20
+
21
+ ### 1. ์ „์—ญ(Global) ์„ค์น˜
22
+ ์–ด๋А ๊ฒฝ๋กœ์—์„œ๋‚˜ ๋ช…๋ น์–ด๋กœ ์ฆ‰์‹œ ๊ธฐ๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ „์—ญ ์„ค์น˜๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
23
+
24
+ \`\`\`bash
25
+ npm install -g @nine-lab/nine-connector
26
+ \`\`\`
27
+
28
+ ### 2. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • (\`.env\`)
29
+ ์„œ๋ฒ„ ์‹คํ–‰ ์ „, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘์† ์ •๋ณด๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์‹คํ–‰ํ•  ๊ฒฝ๋กœ์— \`.env\` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์‹œ์Šคํ…œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.
30
+
31
+ \`\`\`env
32
+ # ์„œ๋ฒ„ ํฌํŠธ (๊ธฐ๋ณธ๊ฐ’: 3000)
33
+ SERVER_PORT=3000
34
+
35
+ # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •
36
+ DB_TYPE=mysql
37
+ DB_HOST=127.0.0.1
38
+ DB_PORT=3306
39
+ DB_USER=your_user
40
+ DB_PASS=your_password
41
+ DB_NAME=your_database
42
+ \`\`\`
43
+
44
+ ### 3. ์„œ๋ฒ„ ๊ธฐ๋™
45
+ ์„ค์ •์ด ์™„๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด ํ„ฐ๋ฏธ๋„์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.
46
+
47
+ \`\`\`bash
48
+ nine-connector
49
+ \`\`\`
50
+
51
+ ---
52
+
53
+ ## ๐Ÿ”Œ API ๋ช…์„ธ
54
+
55
+ ### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์กฐํšŒ
56
+ AI ๋ชจ๋ธ์ด ํ˜„์žฌ DB ๊ตฌ์กฐ๋ฅผ ํ•™์Šตํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
57
+ * **Endpoint:** \`GET /api/schema\`
58
+
59
+ ### 2. SQL ์ฟผ๋ฆฌ ์‹คํ–‰
60
+ AI๊ฐ€ ์ƒ์„ฑํ•œ SQL์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋ณด์•ˆ์„ ์œ„ํ•ด **SELECT ๋ฌธ**๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค.
61
+ * **Endpoint:** \`POST /api/query\`
62
+
63
+ ---
64
+
65
+ ## ๐Ÿ“„ ๋ผ์ด์„ ์Šค
66
+ MIT ยฉ **nine-lab**
67
+ EOF
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@nine-lab/nine-connector",
3
+ "version": "0.1.1",
4
+ "description": "NineQuery AI Connector for Database",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "nine-query-connector": "src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "dev": "node --watch src/index.js",
13
+ "build": "echo '๋นŒ๋“œ๊ฐ€ ํ•„์š”ํ•œ ํ”„๋กœ์ ํŠธ๋ผ๋ฉด ์—ฌ๊ธฐ์— ๋นŒ๋“œ ๋ช…๋ น ์ž‘์„ฑ'",
14
+ "release": "npm version patch && npm run build && npm publish"
15
+ },
16
+ "dependencies": {
17
+ "@langchain/core": "^1.1.44",
18
+ "@langchain/google-genai": "^2.1.30",
19
+ "@nine-lab/nine-util": "^0.9.35",
20
+ "cors": "^2.8.6",
21
+ "dotenv": "^16.4.5",
22
+ "express": "^4.22.1",
23
+ "mariadb": "^3.5.2",
24
+ "mysql2": "^3.9.7",
25
+ "pg": "^8.11.5"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }
@@ -0,0 +1,24 @@
1
+ ### SQL_GENERATE_PROMPT
2
+ ๋‹น์‹ ์€ SQL ์ƒ์„ฑ๊ธฐ์ž…๋‹ˆ๋‹ค. ์ œ๊ณต๋œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ๋‹ตํ•˜๋Š” ์ตœ์ ์˜ SQL ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”.
3
+
4
+ [์„ ํƒ๋œ ํ…Œ์ด๋ธ” ์ •๋ณด]
5
+ {detailed_schema}
6
+
7
+ [์‚ฌ์šฉ์ž ์งˆ๋ฌธ]
8
+ "{question}"
9
+
10
+ [๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž…]
11
+ {db_type}
12
+
13
+ [์‘๋‹ต ๊ทœ์น™]
14
+ 1. ๋ฐ˜๋“œ์‹œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•˜์„ธ์š”.
15
+ 2. SQL์€ {db_type} ๋ฌธ๋ฒ•์— ์™„๋ฒฝํ•˜๊ฒŒ ๋งž์•„์•ผ ํ•˜๋ฉฐ, ์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
16
+ 3. ์ฟผ๋ฆฌ๋ฌธ ์•ˆ์— ๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋ก(```sql)์„ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”. ์˜ค์ง ์ˆœ์ˆ˜ ๋ฌธ์ž์—ด๋กœ๋งŒ ์ž‘์„ฑํ•˜์„ธ์š”.
17
+ 4. ์‚ฌ์šฉ์ž๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋„๋ก ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์„ค๋ช…(explanation)์„ ์นœ์ ˆํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”.
18
+ 5. ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปฌ๋Ÿผ์ด๋‚˜ ํ…Œ์ด๋ธ”์€ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”.
19
+
20
+ [์‘๋‹ต ํฌ๋งท]
21
+ {{
22
+ "sql": "SELECT ... FROM ... WHERE ...",
23
+ "explanation": "์ด ์ฟผ๋ฆฌ๋Š” ์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์กฐํšŒํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค."
24
+ }}
@@ -0,0 +1,21 @@
1
+ ### TABLE_FILTER_PROMPT
2
+ ๋‹น์‹ ์€ SQL ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒํ•ด์•ผ ํ•  ํ•„์ˆ˜ ํ…Œ์ด๋ธ”๋“ค๋งŒ ์„ ์ •ํ•˜์„ธ์š”.
3
+
4
+ [์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก]
5
+ {schema_summary}
6
+
7
+ [์‚ฌ์šฉ์ž ์งˆ๋ฌธ]
8
+ "{question}"
9
+
10
+ [์‘๋‹ต ๊ทœ์น™]
11
+ 1. ๋ฐ˜๋“œ์‹œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•˜์„ธ์š”.
12
+ 2. ์งˆ๋ฌธ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ˜๋“œ์‹œ ์ฐธ์กฐํ•ด์•ผ ํ•˜๋Š” ํ…Œ์ด๋ธ” ์ด๋ฆ„๋งŒ 'selected_tables' ๋ฐฐ์—ด์— ๋„ฃ์œผ์„ธ์š”.
13
+ 3. ๋งŒ์•ฝ ์งˆ๋ฌธ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒ์™€ ๊ด€๋ จ์ด ์—†๊ฑฐ๋‚˜, ์ œ๊ณต๋œ ํ…Œ์ด๋ธ” ์ •๋ณด๋กœ ๋‹ตํ•  ์ˆ˜ ์—†๋‹ค๋ฉด 'is_executable'์„ false๋กœ ์„ค์ •ํ•˜์„ธ์š”.
14
+ 4. 'reasoning' ํ•„๋“œ์—๋Š” ์™œ ํ•ด๋‹น ํ…Œ์ด๋ธ”๋“ค์„ ์„ ํƒํ–ˆ๋Š”์ง€ ๊ฐ„๋žตํ•˜๊ฒŒ ์„ค๋ช…ํ•˜์„ธ์š”.
15
+
16
+ [์‘๋‹ต ํฌ๋งท]
17
+ {{
18
+ "reasoning": "์„ ์ • ์ด์œ ๋ฅผ ์—ฌ๊ธฐ์— ์ž‘์„ฑ",
19
+ "selected_tables": ["table1", "table2"],
20
+ "is_executable": true
21
+ }}
@@ -0,0 +1,41 @@
1
+ import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
2
+ import { PromptTemplate } from "@langchain/core/prompts";
3
+ import { JsonOutputParser } from "@langchain/core/output_parsers";
4
+
5
+ export class AIProcessor {
6
+ #model;
7
+ #chains = {};
8
+ #parser;
9
+
10
+ constructor(apiKey, modelName = "gemini-2.5-flash") {
11
+ this.#model = new ChatGoogleGenerativeAI({
12
+ apiKey: apiKey,
13
+ model: modelName,
14
+ temperature: 0,
15
+ });
16
+ this.#parser = new JsonOutputParser();
17
+ }
18
+
19
+ registerChain(key, promptText) {
20
+ const promptTemplate = PromptTemplate.fromTemplate(promptText);
21
+ this.#chains[key] = promptTemplate.pipe(this.#model).pipe(this.#parser);
22
+ console.log(`[AIProcessor] ${key} ์ฒด์ธ ๋“ฑ๋ก ์™„๋ฃŒ.`);
23
+ }
24
+
25
+ async filterTables(question, schemaSummary) {
26
+ if (!this.#chains['table-filter']) throw new Error("table-filter ์ฒด์ธ ๋ฏธ๋“ฑ๋ก");
27
+ return await this.#chains['table-filter'].invoke({
28
+ question,
29
+ schema_summary: JSON.stringify(schemaSummary, null, 2)
30
+ });
31
+ }
32
+
33
+ async generateSql(question, detailedSchema, dbType = 'mysql') {
34
+ if (!this.#chains['query-generator']) throw new Error("query-generator ์ฒด์ธ ๋ฏธ๋“ฑ๋ก");
35
+ return await this.#chains['query-generator'].invoke({
36
+ question,
37
+ detailed_schema: JSON.stringify(detailedSchema, null, 2),
38
+ db_type: dbType
39
+ });
40
+ }
41
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ์ž์—ฐ์–ด ์งˆ์˜ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค
3
+ */
4
+ export class AIService {
5
+ constructor(db, ai) {
6
+ this.db = db;
7
+ this.ai = ai;
8
+ }
9
+
10
+ async processNaturalLanguageQuery(question) {
11
+ // 1) ์ „์ฒด ์Šคํ‚ค๋งˆ ์š”์•ฝ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
12
+ const schemaSummary = await this.db.getTableSchema();
13
+
14
+ // 2) AI: ํ•„์š”ํ•œ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง
15
+ // AI ๊ฒฐ๊ณผ๊ฐ€ ๋ฌธ์ž์—ด์ผ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด ํŒŒ์‹ฑ ๋กœ์ง์ด AIProcessor์— ์—†๋‹ค๋ฉด ์—ฌ๊ธฐ์„œ ๋ณด์™„ ๊ฐ€๋Šฅ
16
+ const { tables } = await this.ai.filterTables(question, schemaSummary);
17
+
18
+ if (!tables || tables.length === 0) {
19
+ throw new Error("์งˆ๋ฌธ๊ณผ ๊ด€๋ จ๋œ ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
20
+ }
21
+
22
+ // 3) ์ƒ์„ธ ์Šคํ‚ค๋งˆ ํ•„ํ„ฐ๋ง ๋ฐ SQL ์ƒ์„ฑ
23
+ const detailedSchema = schemaSummary.filter(t => tables.includes(t.tableName));
24
+ const { sql, explanation } = await this.ai.generateSql(
25
+ question,
26
+ detailedSchema,
27
+ process.env.DB_TYPE || 'mysql'
28
+ );
29
+
30
+ // 4) SQL ๊ฒ€์ฆ
31
+ if (!sql.trim().toLowerCase().startsWith("select")) {
32
+ throw new Error("๋ณด์•ˆ ์ •์ฑ…์ƒ SELECT ์ฟผ๋ฆฌ๋งŒ ์‹คํ–‰ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.");
33
+ }
34
+
35
+ // 5) ์ฟผ๋ฆฌ ์‹คํ–‰
36
+ const rows = await this.db.query(sql);
37
+
38
+ return {
39
+ sql,
40
+ explanation,
41
+ data: rows
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,140 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+
5
+ /**
6
+ * 1. .env ํŒŒ์ผ ์ƒ์„ฑ ๋‹ด๋‹น ํ•จ์ˆ˜
7
+ */
8
+ async function createEnvFile(rl) {
9
+ const questions = [
10
+ { key: 'SERVER_PORT', label: '1. ์ปค๋„ฅํ„ฐ ์„œ๋ฒ„ ํฌํŠธ', default: '3000' },
11
+ { key: 'GEMINI_API_KEY', label: '2. Gemini API Key', default: '' },
12
+ { key: 'GEMINI_MODEL', label: '3. ์‚ฌ์šฉํ•  ๋ชจ๋ธ', default: 'gemini-1.5-flash' },
13
+ { key: 'DB_TYPE', label: '4. DB ์ข…๋ฅ˜ (mysql, pg, mariadb)', default: 'mysql' },
14
+ { key: 'DB_HOST', label: '5. DB ํ˜ธ์ŠคํŠธ', default: '127.0.0.1' },
15
+ { key: 'DB_PORT', label: '6. DB ํฌํŠธ', default: '3306' },
16
+ { key: 'DB_USER', label: '7. DB ์‚ฌ์šฉ์ž ๊ณ„์ •', default: 'root' },
17
+ { key: 'DB_PASS', label: '8. DB ๋น„๋ฐ€๋ฒˆํ˜ธ', default: '' },
18
+ { key: 'DB_NAME', label: '9. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ด๋ฆ„', default: '' },
19
+ ];
20
+
21
+ const envPath = path.join(process.cwd(), '.env');
22
+
23
+ if (fs.existsSync(envPath)) {
24
+ const overwrite = await new Promise((resolve) => {
25
+ rl.question('์ด๋ฏธ .env ํŒŒ์ผ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋ฎ์–ด์“ฐ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? (y/n): ', resolve);
26
+ });
27
+ if (overwrite.toLowerCase() !== 'y') {
28
+ console.log('๊ธฐ์กด .env ํŒŒ์ผ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.');
29
+ return true;
30
+ }
31
+ }
32
+
33
+ const answers = {};
34
+ for (const q of questions) {
35
+ const answer = await new Promise((resolve) => {
36
+ rl.question(`> ${q.label} [${q.default}]: `, resolve);
37
+ });
38
+ answers[q.key] = answer.trim() || q.default;
39
+ }
40
+
41
+ const envContent = Object.entries(answers)
42
+ .map(([key, val]) => `${key}=${val}`)
43
+ .join('\n');
44
+
45
+ fs.writeFileSync(envPath, envContent);
46
+ console.log('.env ํŒŒ์ผ ์ƒ์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
47
+ return true;
48
+ }
49
+
50
+ /**
51
+ * 2. ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ(.md) ์ƒ์„ฑ ๋‹ด๋‹น ํ•จ์ˆ˜
52
+ */
53
+ function createPromptFiles() {
54
+ const promptDir = path.join(process.cwd(), 'prompts');
55
+
56
+ if (!fs.existsSync(promptDir)) {
57
+ fs.mkdirSync(promptDir, { recursive: true });
58
+ }
59
+
60
+ const prompts = {
61
+ 'table-filter.md': `## TABLE_FILTER_PROMPT
62
+ ๋‹น์‹ ์€ SQL ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒํ•ด์•ผ ํ•  ํ•„์ˆ˜ ํ…Œ์ด๋ธ”๋“ค๋งŒ ์„ ์ •ํ•˜์„ธ์š”.
63
+
64
+ [์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก]
65
+ {schema_summary}
66
+
67
+ [์‚ฌ์šฉ์ž ์งˆ๋ฌธ]
68
+ "{question}"
69
+
70
+ [์‘๋‹ต ๊ทœ์น™]
71
+ 1. ๋ฐ˜๋“œ์‹œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•˜์„ธ์š”.
72
+ 2. ์งˆ๋ฌธ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ˜๋“œ์‹œ ์ฐธ์กฐํ•ด์•ผ ํ•˜๋Š” ํ…Œ์ด๋ธ” ์ด๋ฆ„๋งŒ 'selected_tables' ๋ฐฐ์—ด์— ๋„ฃ์œผ์„ธ์š”.
73
+ 3. ๋งŒ์•ฝ ์งˆ๋ฌธ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒ์™€ ๊ด€๋ จ์ด ์—†๊ฑฐ๋‚˜, ์ œ๊ณต๋œ ํ…Œ์ด๋ธ” ์ •๋ณด๋กœ ๋‹ตํ•  ์ˆ˜ ์—†๋‹ค๋ฉด 'is_executable'์„ false๋กœ ์„ค์ •ํ•˜์„ธ์š”.
74
+ 4. 'reasoning' ํ•„๋“œ์—๋Š” ์™œ ํ•ด๋‹น ํ…Œ์ด๋ธ”๋“ค์„ ์„ ํƒํ–ˆ๋Š”์ง€ ๊ฐ„๋žตํ•˜๊ฒŒ ์„ค๋ช…ํ•˜์„ธ์š”.
75
+
76
+ [์‘๋‹ต ํฌ๋งท]
77
+ {{
78
+ "reasoning": "์„ ์ • ์ด์œ ๋ฅผ ์—ฌ๊ธฐ์— ์ž‘์„ฑ",
79
+ "selected_tables": ["table1", "table2"],
80
+ "is_executable": true
81
+ }}`,
82
+
83
+ 'query-generator.md': `## SQL_GENERATE_PROMPT
84
+ ๋‹น์‹ ์€ SQL ์ƒ์„ฑ๊ธฐ์ž…๋‹ˆ๋‹ค. ์ œ๊ณต๋œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ๋‹ตํ•˜๋Š” ์ตœ์ ์˜ SQL ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”.
85
+
86
+ [์„ ํƒ๋œ ํ…Œ์ด๋ธ” ์ •๋ณด]
87
+ {detailed_schema}
88
+
89
+ [์‚ฌ์šฉ์ž ์งˆ๋ฌธ]
90
+ "{question}"
91
+
92
+ [๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž…]
93
+ {db_type}
94
+
95
+ [์‘๋‹ต ๊ทœ์น™]
96
+ 1. ๋ฐ˜๋“œ์‹œ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์‘๋‹ตํ•˜์„ธ์š”.
97
+ 2. SQL์€ {db_type} ๋ฌธ๋ฒ•์— ์™„๋ฒฝํ•˜๊ฒŒ ๋งž์•„์•ผ ํ•˜๋ฉฐ, ์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
98
+ 3. ์ฟผ๋ฆฌ๋ฌธ ์•ˆ์— ๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋ก(\`\`\`sql)์„ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”. ์˜ค์ง ์ˆœ์ˆ˜ ๋ฌธ์ž์—ด๋กœ๋งŒ ์ž‘์„ฑํ•˜์„ธ์š”.
99
+ 4. ์‚ฌ์šฉ์ž๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋„๋ก ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์„ค๋ช…(explanation)์„ ์นœ์ ˆํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”.
100
+ 5. ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปฌ๋Ÿผ์ด๋‚˜ ํ…Œ์ด๋ธ”์€ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”.
101
+
102
+ [์‘๋‹ต ํฌ๋งท]
103
+ {{
104
+ "sql": "SELECT ... FROM ... WHERE ...",
105
+ "explanation": "์ด ์ฟผ๋ฆฌ๋Š” ์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์กฐํšŒํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค."
106
+ }}`
107
+ };
108
+
109
+ Object.entries(prompts).forEach(([filename, content]) => {
110
+ const filePath = path.join(promptDir, filename);
111
+ fs.writeFileSync(filePath, content.trim());
112
+ });
113
+
114
+ console.log('ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ ์ƒ์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
115
+ }
116
+
117
+ /**
118
+ * ๋ฉ”์ธ ์‹คํ–‰ ํ•จ์ˆ˜
119
+ */
120
+ export async function runInit() {
121
+ const rl = readline.createInterface({
122
+ input: process.stdin,
123
+ output: process.stdout
124
+ });
125
+
126
+ try {
127
+ console.log('์„ค์ • ์ดˆ๊ธฐํ™”๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.');
128
+
129
+ const success = await createEnvFile(rl);
130
+ if (success) {
131
+ createPromptFiles();
132
+ console.log('๋ชจ๋“  ์ดˆ๊ธฐํ™” ์ž‘์—…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
133
+ console.log('nine-connector ๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”.');
134
+ }
135
+ } catch (err) {
136
+ console.error('์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', err.message);
137
+ } finally {
138
+ rl.close();
139
+ }
140
+ }
@@ -0,0 +1,42 @@
1
+ import * as mariadb from 'mariadb';
2
+
3
+ // [์ค‘์š”] .env์—์„œ ๊ฐ€์ ธ์˜จ ๊ฐ’๋“ค์˜ ์–‘๋ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•˜๊ณ  ํƒ€์ž…์„ ๋งž์ถฅ๋‹ˆ๋‹ค.
4
+ const dbConfig = {
5
+ host: process.env.DB_HOST?.trim(), // ํ˜น์‹œ ๋ชจ๋ฅผ ์ค„๋ฐ”๊ฟˆ/๊ณต๋ฐฑ ์ œ๊ฑฐ
6
+ port: Number(process.env.DB_PORT?.trim()) || 3306, // ํ™•์‹คํ•˜๊ฒŒ ์ˆซ์ž๋กœ ๋ณ€ํ™˜
7
+ user: process.env.DB_USER?.trim(),
8
+ password: process.env.DB_PASS?.trim(), // ํŠน์ˆ˜๋ฌธ์ž๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ trim ํ•„์ˆ˜
9
+ database: process.env.DB_NAME?.trim(),
10
+ connectionLimit: 10,
11
+ connectTimeout: 10000
12
+ };
13
+
14
+ // ๋กœ๊ทธ๋กœ ์‹ค์ œ ๊ฐ’์ด ํ•˜๋“œ์ฝ”๋”ฉ๊ณผ ์™„๋ฒฝํžˆ ์ผ์น˜ํ•˜๋Š”์ง€ ๋Œ€์กฐํ•ด๋ณด์„ธ์š”.
15
+ console.log("--- DB Config Check ---");
16
+ console.log("Host:", `[${dbConfig.host}]`); // ๋Œ€๊ด„ํ˜ธ ์•ˆ์— ๊ณต๋ฐฑ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
17
+ console.log("Port Type:", typeof dbConfig.port); // number์—ฌ์•ผ ํ•จ
18
+ console.log("--- ---------------- ---");
19
+
20
+ const pool = mariadb.createPool(dbConfig);
21
+
22
+ export const getConnection = async () => {
23
+ try {
24
+ return await pool.getConnection();
25
+ } catch (err) {
26
+ console.error("โŒ DB ์—ฐ๊ฒฐ ์‹คํŒจ:", err);
27
+ throw err;
28
+ }
29
+ };
30
+
31
+ export default pool;
32
+
33
+ /**
34
+ const pool = mariadb.createPool({
35
+ host: "mariadb.artygentech.com",
36
+ port: 3306,
37
+ user: "root",
38
+ password: "aglab123!@#",
39
+ database: "aidemo",
40
+ connectionLimit: 10,
41
+ connectTimeout: 10000
42
+ }); */
@@ -0,0 +1,88 @@
1
+ import PoolManager from './PoolManager.js';
2
+ import Dialects from './Dialects.js'; // ์ฟผ๋ฆฌ ์‚ฌ์ „ ์ž„ํฌํŠธ
3
+
4
+ class DatabaseManager {
5
+ constructor(config) {
6
+ this.config = config;
7
+ this.pool = null;
8
+ this.type = config.type?.toLowerCase();
9
+ this.dialect = Dialects[this.type];
10
+
11
+ if (!this.dialect) {
12
+ throw new Error(`โŒ Unsupported Dialect for: ${this.type}`);
13
+ }
14
+ }
15
+
16
+ /**
17
+ * DatabaseFactory์˜ init() ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
18
+ */
19
+ async connect() {
20
+ try {
21
+ // Pool ์ƒ์„ฑ์€ ์ „์ ์œผ๋กœ PoolManager์—๊ฒŒ ๋งก๊น๋‹ˆ๋‹ค.
22
+ this.pool = PoolManager.createPool(this.config);
23
+
24
+ // ๋‹จ์ˆœ ์—ฐ๊ฒฐ ํ™•์ธ ์ฟผ๋ฆฌ
25
+ await this.query('SELECT 1');
26
+ console.log(`โœ… [${this.type}] Database connection successful.`);
27
+ } catch (error) {
28
+ console.error(`โŒ [${this.type}] Connection failed:`, error.message);
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * DB๋ณ„ ๊ฒฐ๊ณผ ํฌ๋งท ์ฐจ์ด๋ฅผ ์—ฌ๊ธฐ์„œ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. (๋ฐ์ดํ„ฐ ๋ฐฐ์—ด๋งŒ ๋ฐ˜ํ™˜)
35
+ */
36
+ // DatabaseManager.js ์ˆ˜์ •
37
+ async query(sql, params = []) {
38
+ if (!this.pool) throw new Error("Database not connected.");
39
+
40
+ const result = await this.pool.query(sql, params);
41
+
42
+ if (this.type === 'postgres') {
43
+ return result.rows;
44
+ } else {
45
+ // ๋งŒ์•ฝ result[0]์ด ๋˜ ๋ฐฐ์—ด์ด๋ผ๋ฉด(mysql2 ์ด์ค‘ ๋ฐฐ์—ด), 0๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ˜ํ™˜
46
+ // ์•„๋‹ˆ๋ผ๋ฉด result๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
47
+ return (Array.isArray(result) && Array.isArray(result[0]))
48
+ ? result[0]
49
+ : result;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * AI ๊ฐ€๊ณต์„ ์œ„ํ•ด ์Šคํ‚ค๋งˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ์กฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
55
+ * ๊ฒฐ๊ณผ ์˜ˆ์‹œ:
56
+ * {
57
+ * "orders": {
58
+ * "description": "์ฃผ๋ฌธ ๊ธฐ๋ณธ ์ •๋ณด ํ…Œ์ด๋ธ”",
59
+ * "columns": [{ "column": "id", "type": "int" }, ...]
60
+ * }
61
+ * }
62
+ */
63
+ async getTableSchema() {
64
+ const rows = await this.query(this.dialect.schemaQuery);
65
+
66
+ return rows.reduce((acc, row) => {
67
+ const { table_name, table_comment, column_name, data_type } = row;
68
+
69
+ // ํ…Œ์ด๋ธ”์ด ์ฒ˜์Œ ๋“ฑ์žฅํ•˜๋ฉด ์ดˆ๊ธฐํ™”
70
+ if (!acc[table_name]) {
71
+ acc[table_name] = {
72
+ description: table_comment || "", // ํ…Œ์ด๋ธ” ์„ค๋ช… ์ถ”๊ฐ€
73
+ columns: []
74
+ };
75
+ }
76
+
77
+ // ์ปฌ๋Ÿผ ์ •๋ณด push
78
+ acc[table_name].columns.push({
79
+ column: column_name,
80
+ type: data_type
81
+ });
82
+
83
+ return acc;
84
+ }, {});
85
+ }
86
+ }
87
+
88
+ export default DatabaseManager;
@@ -0,0 +1,59 @@
1
+ const Dialects = {
2
+ // MySQL ์ถ”๊ฐ€ (MariaDB์™€ ๋™์ผ)
3
+ mysql: {
4
+ schemaQuery: `
5
+ SELECT
6
+ c.TABLE_NAME as table_name,
7
+ t.TABLE_COMMENT as table_comment,
8
+ c.COLUMN_NAME as column_name,
9
+ c.DATA_TYPE as data_type
10
+ FROM information_schema.columns c
11
+ INNER JOIN information_schema.tables t
12
+ ON c.TABLE_NAME = t.TABLE_NAME
13
+ AND c.TABLE_SCHEMA = t.TABLE_SCHEMA
14
+ WHERE c.TABLE_SCHEMA = DATABASE()
15
+ ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`
16
+ },
17
+ mariadb: {
18
+ schemaQuery: `
19
+ SELECT
20
+ c.TABLE_NAME as table_name,
21
+ t.TABLE_COMMENT as table_comment,
22
+ c.COLUMN_NAME as column_name,
23
+ c.DATA_TYPE as data_type
24
+ FROM information_schema.columns c
25
+ INNER JOIN information_schema.tables t
26
+ ON c.TABLE_NAME = t.TABLE_NAME
27
+ AND c.TABLE_SCHEMA = t.TABLE_SCHEMA
28
+ WHERE c.TABLE_SCHEMA = DATABASE()
29
+ ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`
30
+ },
31
+ postgres: {
32
+ schemaQuery: `
33
+ SELECT
34
+ t.table_name,
35
+ obj_description(pgc.oid, 'pg_class') AS table_comment,
36
+ c.column_name,
37
+ c.data_type
38
+ FROM information_schema.tables t
39
+ JOIN information_schema.columns c ON t.table_name = c.table_name
40
+ JOIN pg_class pgc ON t.table_name = pgc.relname
41
+ WHERE t.table_schema = 'public'
42
+ ORDER BY t.table_name, c.ordinal_position`
43
+ },
44
+ oracle: {
45
+ schemaQuery: `
46
+ SELECT
47
+ t.TABLE_NAME as table_name,
48
+ tc.COMMENTS as table_comment,
49
+ c.COLUMN_NAME as column_name,
50
+ c.DATA_TYPE as data_type
51
+ FROM ALL_TABLES t
52
+ JOIN ALL_TAB_COLUMNS c ON t.TABLE_NAME = c.TABLE_NAME AND t.OWNER = c.OWNER
53
+ LEFT JOIN ALL_TAB_COMMENTS tc ON t.TABLE_NAME = tc.TABLE_NAME AND t.OWNER = tc.OWNER
54
+ WHERE t.OWNER = (SELECT USER FROM DUAL) -- ํ˜„์žฌ ์ ‘์†ํ•œ ์Šคํ‚ค๋งˆ๋งŒ ์กฐํšŒ
55
+ ORDER BY t.TABLE_NAME, c.COLUMN_ID`
56
+ }
57
+ };
58
+
59
+ export default Dialects;
@@ -0,0 +1,49 @@
1
+ import * as mariadb from 'mariadb';
2
+ import pg from 'pg';
3
+
4
+ class PoolManager {
5
+ /**
6
+ * DB ํƒ€์ž…์— ๋งž๋Š” ์ปค๋„ฅ์…˜ ํ’€์„ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
7
+ */
8
+ static createPool(config) {
9
+ const type = config?.type?.toLowerCase();
10
+
11
+ switch (type) {
12
+ case 'mariadb':
13
+ case 'mysql':
14
+ return this.#createMariaDBPool(config);
15
+ case 'postgres':
16
+ case 'postgresql':
17
+ return this.#createPostgresPool(config);
18
+ default:
19
+ throw new Error(`[PoolManager] Unsupported DB_TYPE: ${type}`);
20
+ }
21
+ }
22
+
23
+ static #createMariaDBPool(config) {
24
+ return mariadb.createPool({
25
+ host: config.host,
26
+ port: config.port,
27
+ user: config.user,
28
+ password: config.password,
29
+ database: config.database,
30
+ connectionLimit: 10,
31
+ connectTimeout: 10000
32
+ });
33
+ }
34
+
35
+ static #createPostgresPool(config) {
36
+ const { Pool } = pg;
37
+ return new Pool({
38
+ host: config.host,
39
+ port: config.port,
40
+ user: config.user,
41
+ password: config.password,
42
+ database: config.database,
43
+ max: 10,
44
+ idleTimeoutMillis: 30000
45
+ });
46
+ }
47
+ }
48
+
49
+ export default PoolManager;
File without changes
package/src/index.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import express from 'express';
4
+ import cors from 'cors';
5
+ import fs from 'fs';
6
+ import path from 'path'; // path ๋ชจ๋“ˆ ์ถ”๊ฐ€
7
+ import DatabaseManager from './database/core/DatabaseManager.js';
8
+ import { runInit } from './core/init.js';
9
+ import { AIProcessor } from './ai/AIProcessor.js';
10
+ import { AIService } from './ai/AIService.js';
11
+
12
+ /**
13
+ * ์„œ๋ฒ„ ์‹คํ–‰ ๋กœ์ง
14
+ */
15
+ async function bootstrap() {
16
+ // 1. ํ•„์ˆ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ฒดํฌ
17
+ if (!process.env.DB_HOST || !process.env.DB_NAME) {
18
+ console.error("์„ค์ • ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. 'nine-connector init'์„ ๋จผ์ € ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”.");
19
+ process.exit(1);
20
+ }
21
+
22
+ const app = express();
23
+ const SERVER_PORT = process.env.SERVER_PORT || 3000;
24
+
25
+ // 2. DB ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™”
26
+ const db = new DatabaseManager({
27
+ type: process.env.DB_TYPE,
28
+ host: process.env.DB_HOST,
29
+ port: Number(process.env.DB_PORT),
30
+ user: process.env.DB_USER,
31
+ password: process.env.DB_PASS,
32
+ database: process.env.DB_NAME
33
+ });
34
+
35
+ // BigInt ์ง๋ ฌํ™” ์„ค์ •
36
+ BigInt.prototype.toJSON = function() { return this.toString(); };
37
+
38
+ app.use(cors());
39
+ app.use(express.json());
40
+
41
+ // 3. AI ํ”„๋กœ์„ธ์„œ ์ดˆ๊ธฐํ™” ๋ฐ ์ฒด์ธ ๋“ฑ๋ก
42
+ const ai = new AIProcessor(process.env.GEMINI_API_KEY, process.env.GEMINI_MODEL);
43
+
44
+ try {
45
+ // init.js์—์„œ ์ƒ์„ฑํ•œ ๊ฒฝ๋กœ์™€ ์ผ์น˜์‹œํ‚ต๋‹ˆ๋‹ค. (๋ณดํ†ต ๋ฃจํŠธ์˜ prompts ํด๋”)
46
+ const promptPath = path.join(process.cwd(), 'prompts');
47
+
48
+ const tableFilterPrompt = fs.readFileSync(
49
+ path.join(promptPath, 'table-filter.md'), 'utf-8'
50
+ );
51
+ const queryGenPrompt = fs.readFileSync(
52
+ path.join(promptPath, 'query-generator.md'), 'utf-8'
53
+ );
54
+
55
+ ai.registerChain('table-filter', tableFilterPrompt);
56
+ ai.registerChain('query-generator', queryGenPrompt);
57
+
58
+ console.log("๋ชจ๋“  AI ์ฒด์ธ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
59
+ } catch (err) {
60
+ console.error("์ฒด์ธ ๋“ฑ๋ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ (ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ์„ ํ™•์ธํ•˜์„ธ์š”):", err.message);
61
+ process.exit(1);
62
+ }
63
+
64
+ // ์„œ๋น„์Šค ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
65
+ const aiService = new AIService(db, ai);
66
+
67
+ // [API] ์ž์—ฐ์–ด ์งˆ์˜ ์—”๋“œํฌ์ธํŠธ
68
+ app.post('/api/ask', async (req, res) => {
69
+ const { question } = req.body;
70
+ if (!question) {
71
+ return res.status(400).json({ success: false, error: "์งˆ๋ฌธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”." });
72
+ }
73
+
74
+ try {
75
+ const result = await aiService.processNaturalLanguageQuery(question);
76
+ res.json({ success: true, ...result });
77
+ } catch (err) {
78
+ console.error("AI ์งˆ์˜ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", err);
79
+ res.status(500).json({
80
+ success: false,
81
+ error: err.message || "์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ ์ค‘ ์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
82
+ });
83
+ }
84
+ });
85
+
86
+ try {
87
+ await db.connect();
88
+ console.log("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต");
89
+
90
+ // ์Šคํ‚ค๋งˆ ์กฐํšŒ API
91
+ app.get('/api/schema', async (req, res) => {
92
+ try {
93
+ const schema = await db.getTableSchema();
94
+ res.json({ success: true, data: schema });
95
+ } catch (err) {
96
+ res.status(500).json({ success: false, error: "Failed to fetch schema" });
97
+ }
98
+ });
99
+
100
+ // ์ฟผ๋ฆฌ ์‹คํ–‰ API
101
+ app.post('/api/query', async (req, res) => {
102
+ const { sql } = req.body;
103
+ if (!sql || !sql.trim().toLowerCase().startsWith("select")) {
104
+ return res.status(400).json({ success: false, error: "Only SELECT queries are allowed" });
105
+ }
106
+
107
+ try {
108
+ const rows = await db.query(sql);
109
+ res.json({ success: true, data: rows });
110
+ } catch (err) {
111
+ res.status(500).json({ success: false, error: "Database query error" });
112
+ }
113
+ });
114
+
115
+ app.listen(SERVER_PORT, () => {
116
+ console.log(`NineQuery Connector running on http://localhost:${SERVER_PORT}`);
117
+ });
118
+ } catch (error) {
119
+ console.error("์„œ๋ฒ„ ๊ธฐ๋™ ์‹คํŒจ:", error.message);
120
+ process.exit(1);
121
+ }
122
+ }
123
+
124
+ // ----------------------
125
+ // ์‹คํ–‰ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
126
+ // ----------------------
127
+ const args = process.argv.slice(2);
128
+
129
+ if (args.includes('init')) {
130
+ runInit();
131
+ } else {
132
+ bootstrap();
133
+ }