junis 0.2.0 → 0.2.3
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/dist/cli/index.js +448 -129
- package/dist/server/mcp.js +135 -51
- package/dist/server/stdio.js +135 -51
- package/package.json +1 -1
package/dist/server/mcp.js
CHANGED
|
@@ -46,6 +46,7 @@ var import_path = __toESM(require("path"));
|
|
|
46
46
|
var import_glob = require("glob");
|
|
47
47
|
var import_zod = require("zod");
|
|
48
48
|
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
49
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
|
|
49
50
|
var FilesystemTools = class {
|
|
50
51
|
register(server) {
|
|
51
52
|
server.tool(
|
|
@@ -91,8 +92,16 @@ ${error.stderr ?? ""}`
|
|
|
91
92
|
encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
|
|
92
93
|
},
|
|
93
94
|
async ({ path: filePath, encoding }) => {
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
try {
|
|
96
|
+
const content = await import_promises.default.readFile(filePath, encoding);
|
|
97
|
+
return { content: [{ type: "text", text: content }] };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const e = err;
|
|
100
|
+
if (e.code === "ENOENT") {
|
|
101
|
+
return { content: [{ type: "text", text: `\u274C \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}` }], isError: true };
|
|
102
|
+
}
|
|
103
|
+
return { content: [{ type: "text", text: `\u274C \uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
|
|
104
|
+
}
|
|
96
105
|
}
|
|
97
106
|
);
|
|
98
107
|
server.tool(
|
|
@@ -115,9 +124,17 @@ ${error.stderr ?? ""}`
|
|
|
115
124
|
path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
|
|
116
125
|
},
|
|
117
126
|
async ({ path: dirPath }) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
try {
|
|
128
|
+
const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
|
|
129
|
+
const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
|
|
130
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const e = err;
|
|
133
|
+
if (e.code === "ENOENT") {
|
|
134
|
+
return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dirPath}` }], isError: true };
|
|
135
|
+
}
|
|
136
|
+
return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
|
|
137
|
+
}
|
|
121
138
|
}
|
|
122
139
|
);
|
|
123
140
|
server.tool(
|
|
@@ -130,18 +147,20 @@ ${error.stderr ?? ""}`
|
|
|
130
147
|
},
|
|
131
148
|
async ({ pattern, directory, file_pattern }) => {
|
|
132
149
|
try {
|
|
133
|
-
const { stdout } = await
|
|
134
|
-
|
|
150
|
+
const { stdout } = await execFileAsync(
|
|
151
|
+
"rg",
|
|
152
|
+
["--no-heading", "-n", pattern, directory],
|
|
135
153
|
{ timeout: 1e4 }
|
|
136
154
|
);
|
|
137
155
|
return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
|
|
138
156
|
} catch {
|
|
139
|
-
const
|
|
157
|
+
const safeDirectory = import_path.default.resolve(directory);
|
|
158
|
+
const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
|
|
140
159
|
const results = [];
|
|
141
160
|
for (const file of files.slice(0, 100)) {
|
|
142
161
|
try {
|
|
143
162
|
const content = await import_promises.default.readFile(
|
|
144
|
-
import_path.default.join(
|
|
163
|
+
import_path.default.join(safeDirectory, file),
|
|
145
164
|
"utf-8"
|
|
146
165
|
);
|
|
147
166
|
const lines = content.split("\n");
|
|
@@ -268,6 +287,17 @@ var import_zod2 = require("zod");
|
|
|
268
287
|
var BrowserTools = class {
|
|
269
288
|
browser = null;
|
|
270
289
|
page = null;
|
|
290
|
+
// 동시 요청 시 race condition 방지용 직렬화 락
|
|
291
|
+
lock = Promise.resolve();
|
|
292
|
+
withLock(fn) {
|
|
293
|
+
let release;
|
|
294
|
+
const next = new Promise((r) => {
|
|
295
|
+
release = r;
|
|
296
|
+
});
|
|
297
|
+
const current = this.lock;
|
|
298
|
+
this.lock = this.lock.then(() => next);
|
|
299
|
+
return current.then(() => fn()).finally(() => release());
|
|
300
|
+
}
|
|
271
301
|
async init() {
|
|
272
302
|
try {
|
|
273
303
|
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
@@ -290,22 +320,22 @@ var BrowserTools = class {
|
|
|
290
320
|
"browser_navigate",
|
|
291
321
|
"URL\uB85C \uC774\uB3D9",
|
|
292
322
|
{ url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
|
|
293
|
-
|
|
323
|
+
({ url }) => this.withLock(async () => {
|
|
294
324
|
const page = requirePage();
|
|
295
325
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
296
326
|
return {
|
|
297
327
|
content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
|
|
298
328
|
};
|
|
299
|
-
}
|
|
329
|
+
})
|
|
300
330
|
);
|
|
301
331
|
server.tool(
|
|
302
332
|
"browser_click",
|
|
303
333
|
"\uC694\uC18C \uD074\uB9AD",
|
|
304
334
|
{ selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
|
|
305
|
-
|
|
335
|
+
({ selector }) => this.withLock(async () => {
|
|
306
336
|
await requirePage().click(selector);
|
|
307
337
|
return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
|
|
308
|
-
}
|
|
338
|
+
})
|
|
309
339
|
);
|
|
310
340
|
server.tool(
|
|
311
341
|
"browser_type",
|
|
@@ -315,12 +345,12 @@ var BrowserTools = class {
|
|
|
315
345
|
text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
|
|
316
346
|
clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
|
|
317
347
|
},
|
|
318
|
-
|
|
348
|
+
({ selector, text, clear }) => this.withLock(async () => {
|
|
319
349
|
const page = requirePage();
|
|
320
350
|
if (clear) await page.fill(selector, text);
|
|
321
351
|
else await page.type(selector, text);
|
|
322
352
|
return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
|
|
323
|
-
}
|
|
353
|
+
})
|
|
324
354
|
);
|
|
325
355
|
server.tool(
|
|
326
356
|
"browser_screenshot",
|
|
@@ -329,7 +359,7 @@ var BrowserTools = class {
|
|
|
329
359
|
path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
|
|
330
360
|
full_page: import_zod2.z.boolean().optional().default(false)
|
|
331
361
|
},
|
|
332
|
-
|
|
362
|
+
({ path: path2, full_page }) => this.withLock(async () => {
|
|
333
363
|
const page = requirePage();
|
|
334
364
|
const screenshot = await page.screenshot({
|
|
335
365
|
path: path2 ?? void 0,
|
|
@@ -347,13 +377,13 @@ var BrowserTools = class {
|
|
|
347
377
|
}
|
|
348
378
|
]
|
|
349
379
|
};
|
|
350
|
-
}
|
|
380
|
+
})
|
|
351
381
|
);
|
|
352
382
|
server.tool(
|
|
353
383
|
"browser_snapshot",
|
|
354
384
|
"\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
|
|
355
385
|
{},
|
|
356
|
-
async () => {
|
|
386
|
+
() => this.withLock(async () => {
|
|
357
387
|
const page = requirePage();
|
|
358
388
|
const snapshot = await page.locator("body").ariaSnapshot();
|
|
359
389
|
return {
|
|
@@ -361,29 +391,36 @@ var BrowserTools = class {
|
|
|
361
391
|
{ type: "text", text: snapshot }
|
|
362
392
|
]
|
|
363
393
|
};
|
|
364
|
-
}
|
|
394
|
+
})
|
|
365
395
|
);
|
|
366
396
|
server.tool(
|
|
367
397
|
"browser_evaluate",
|
|
368
398
|
"JavaScript \uC2E4\uD589",
|
|
369
399
|
{ code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
400
|
+
({ code }) => this.withLock(async () => {
|
|
401
|
+
try {
|
|
402
|
+
const result = await requirePage().evaluate(code);
|
|
403
|
+
return {
|
|
404
|
+
content: [
|
|
405
|
+
{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }
|
|
406
|
+
]
|
|
407
|
+
};
|
|
408
|
+
} catch (err) {
|
|
409
|
+
return {
|
|
410
|
+
content: [{ type: "text", text: `\u274C JavaScript \uC2E4\uD589 \uC624\uB958: ${err.message}` }],
|
|
411
|
+
isError: true
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
})
|
|
378
415
|
);
|
|
379
416
|
server.tool(
|
|
380
417
|
"browser_pdf",
|
|
381
418
|
"\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
|
|
382
419
|
{ path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
|
|
383
|
-
|
|
420
|
+
({ path: path2 }) => this.withLock(async () => {
|
|
384
421
|
await requirePage().pdf({ path: path2 });
|
|
385
422
|
return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path2}` }] };
|
|
386
|
-
}
|
|
423
|
+
})
|
|
387
424
|
);
|
|
388
425
|
}
|
|
389
426
|
};
|
|
@@ -396,7 +433,11 @@ var import_util2 = require("util");
|
|
|
396
433
|
var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
|
|
397
434
|
async function readNotebook(filePath) {
|
|
398
435
|
const raw = await import_promises2.default.readFile(filePath, "utf-8");
|
|
399
|
-
|
|
436
|
+
try {
|
|
437
|
+
return JSON.parse(raw);
|
|
438
|
+
} catch {
|
|
439
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
|
|
440
|
+
}
|
|
400
441
|
}
|
|
401
442
|
async function writeNotebook(filePath, nb) {
|
|
402
443
|
await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
|
|
@@ -491,15 +532,21 @@ var NotebookTools = class {
|
|
|
491
532
|
execution_count: cellType === "code" ? null : void 0
|
|
492
533
|
};
|
|
493
534
|
let actualIndex;
|
|
535
|
+
let warning = "";
|
|
494
536
|
if (position === void 0 || position === null) {
|
|
495
537
|
nb.cells.push(newCell);
|
|
496
538
|
actualIndex = nb.cells.length - 1;
|
|
539
|
+
} else if (position > nb.cells.length) {
|
|
540
|
+
nb.cells.push(newCell);
|
|
541
|
+
actualIndex = nb.cells.length - 1;
|
|
542
|
+
warning = ` (\uACBD\uACE0: position ${position}\uC774 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uC5EC \uB05D(index: ${actualIndex})\uC5D0 \uCD94\uAC00\uB428)`;
|
|
497
543
|
} else {
|
|
498
|
-
|
|
499
|
-
|
|
544
|
+
const clamped = Math.max(0, position);
|
|
545
|
+
nb.cells.splice(clamped, 0, newCell);
|
|
546
|
+
actualIndex = clamped;
|
|
500
547
|
}
|
|
501
548
|
await writeNotebook(filePath, nb);
|
|
502
|
-
return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
|
|
549
|
+
return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})${warning}` }] };
|
|
503
550
|
}
|
|
504
551
|
);
|
|
505
552
|
server.tool(
|
|
@@ -527,13 +574,13 @@ var import_child_process3 = require("child_process");
|
|
|
527
574
|
var import_util3 = require("util");
|
|
528
575
|
var import_zod4 = require("zod");
|
|
529
576
|
var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
|
|
577
|
+
var screenRecordPid = null;
|
|
530
578
|
function platform() {
|
|
531
579
|
if (process.platform === "darwin") return "mac";
|
|
532
580
|
if (process.platform === "win32") return "win";
|
|
533
581
|
return "linux";
|
|
534
582
|
}
|
|
535
583
|
var DeviceTools = class {
|
|
536
|
-
screenRecordPid = null;
|
|
537
584
|
register(server) {
|
|
538
585
|
server.tool(
|
|
539
586
|
"screen_capture",
|
|
@@ -543,15 +590,26 @@ var DeviceTools = class {
|
|
|
543
590
|
},
|
|
544
591
|
async ({ output_path }) => {
|
|
545
592
|
const p = platform();
|
|
593
|
+
const isTmp = !output_path;
|
|
546
594
|
const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
|
|
547
595
|
const cmd = {
|
|
548
596
|
mac: `screencapture -x "${tmpPath}"`,
|
|
549
597
|
win: `nircmd.exe savescreenshot "${tmpPath}"`,
|
|
550
598
|
linux: `scrot "${tmpPath}"`
|
|
551
599
|
}[p];
|
|
552
|
-
|
|
553
|
-
|
|
600
|
+
try {
|
|
601
|
+
await execAsync3(cmd);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
|
|
604
|
+
}
|
|
605
|
+
const { readFileSync, unlinkSync } = await import("fs");
|
|
554
606
|
const data = readFileSync(tmpPath).toString("base64");
|
|
607
|
+
if (isTmp) {
|
|
608
|
+
try {
|
|
609
|
+
unlinkSync(tmpPath);
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
}
|
|
555
613
|
return {
|
|
556
614
|
content: [{ type: "image", data, mimeType: "image/png" }]
|
|
557
615
|
};
|
|
@@ -565,15 +623,31 @@ var DeviceTools = class {
|
|
|
565
623
|
},
|
|
566
624
|
async ({ output_path }) => {
|
|
567
625
|
const p = platform();
|
|
626
|
+
const isTmp = !output_path;
|
|
568
627
|
const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
|
|
569
628
|
const cmd = {
|
|
570
629
|
mac: `imagesnap "${tmpPath}"`,
|
|
571
630
|
win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
|
|
572
631
|
linux: `fswebcam -r 1280x720 "${tmpPath}"`
|
|
573
632
|
}[p];
|
|
574
|
-
|
|
575
|
-
|
|
633
|
+
try {
|
|
634
|
+
await execAsync3(cmd);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
const e = err;
|
|
637
|
+
return {
|
|
638
|
+
content: [{ type: "text", text: `\u274C \uCE74\uBA54\uB77C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
|
|
639
|
+
\uC6D0\uC778: ${e.message}
|
|
640
|
+
|
|
641
|
+
\uCE74\uBA54\uB77C\uAC00 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.` }],
|
|
642
|
+
isError: true
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const { readFileSync, unlinkSync } = await import("fs");
|
|
576
646
|
const data = readFileSync(tmpPath).toString("base64");
|
|
647
|
+
if (isTmp) try {
|
|
648
|
+
unlinkSync(tmpPath);
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
577
651
|
return {
|
|
578
652
|
content: [{ type: "image", data, mimeType: "image/jpeg" }]
|
|
579
653
|
};
|
|
@@ -588,11 +662,17 @@ var DeviceTools = class {
|
|
|
588
662
|
},
|
|
589
663
|
async ({ title, message }) => {
|
|
590
664
|
const p = platform();
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
665
|
+
let cmd;
|
|
666
|
+
if (p === "win") {
|
|
667
|
+
const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
|
|
668
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
669
|
+
cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
|
|
670
|
+
} else {
|
|
671
|
+
cmd = {
|
|
672
|
+
mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
|
|
673
|
+
linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
|
|
674
|
+
}[p] ?? "";
|
|
675
|
+
}
|
|
596
676
|
await execAsync3(cmd);
|
|
597
677
|
return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
|
|
598
678
|
}
|
|
@@ -633,7 +713,7 @@ var DeviceTools = class {
|
|
|
633
713
|
async ({ action, output_path }) => {
|
|
634
714
|
const p = platform();
|
|
635
715
|
if (action === "start") {
|
|
636
|
-
if (
|
|
716
|
+
if (screenRecordPid) {
|
|
637
717
|
return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
|
|
638
718
|
}
|
|
639
719
|
const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
|
|
@@ -641,17 +721,18 @@ var DeviceTools = class {
|
|
|
641
721
|
const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
|
|
642
722
|
const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
|
|
643
723
|
child.unref();
|
|
644
|
-
|
|
645
|
-
return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${
|
|
724
|
+
screenRecordPid = child.pid ?? null;
|
|
725
|
+
return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
|
|
646
726
|
} else {
|
|
647
|
-
if (!
|
|
727
|
+
if (!screenRecordPid) {
|
|
648
728
|
return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
|
|
649
729
|
}
|
|
650
730
|
try {
|
|
651
|
-
process.kill(
|
|
731
|
+
process.kill(screenRecordPid, "SIGINT");
|
|
732
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
652
733
|
} catch {
|
|
653
734
|
}
|
|
654
|
-
|
|
735
|
+
screenRecordPid = null;
|
|
655
736
|
return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
|
|
656
737
|
}
|
|
657
738
|
}
|
|
@@ -670,12 +751,15 @@ var DeviceTools = class {
|
|
|
670
751
|
} catch {
|
|
671
752
|
}
|
|
672
753
|
}
|
|
673
|
-
const res = await fetch("
|
|
754
|
+
const res = await fetch("http://ip-api.com/json/");
|
|
674
755
|
const data = await res.json();
|
|
756
|
+
if (data.status !== "success") {
|
|
757
|
+
throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
|
|
758
|
+
}
|
|
675
759
|
return {
|
|
676
760
|
content: [{
|
|
677
761
|
type: "text",
|
|
678
|
-
text: `\uC704\uB3C4: ${data.
|
|
762
|
+
text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
|
|
679
763
|
}]
|
|
680
764
|
};
|
|
681
765
|
}
|