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/stdio.js
CHANGED
|
@@ -35,6 +35,7 @@ var import_path = __toESM(require("path"));
|
|
|
35
35
|
var import_glob = require("glob");
|
|
36
36
|
var import_zod = require("zod");
|
|
37
37
|
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
38
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
|
|
38
39
|
var FilesystemTools = class {
|
|
39
40
|
register(server) {
|
|
40
41
|
server.tool(
|
|
@@ -80,8 +81,16 @@ ${error.stderr ?? ""}`
|
|
|
80
81
|
encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
|
|
81
82
|
},
|
|
82
83
|
async ({ path: filePath, encoding }) => {
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
try {
|
|
85
|
+
const content = await import_promises.default.readFile(filePath, encoding);
|
|
86
|
+
return { content: [{ type: "text", text: content }] };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const e = err;
|
|
89
|
+
if (e.code === "ENOENT") {
|
|
90
|
+
return { content: [{ type: "text", text: `\u274C \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}` }], isError: true };
|
|
91
|
+
}
|
|
92
|
+
return { content: [{ type: "text", text: `\u274C \uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
|
|
93
|
+
}
|
|
85
94
|
}
|
|
86
95
|
);
|
|
87
96
|
server.tool(
|
|
@@ -104,9 +113,17 @@ ${error.stderr ?? ""}`
|
|
|
104
113
|
path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
|
|
105
114
|
},
|
|
106
115
|
async ({ path: dirPath }) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
try {
|
|
117
|
+
const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
|
|
118
|
+
const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
|
|
119
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const e = err;
|
|
122
|
+
if (e.code === "ENOENT") {
|
|
123
|
+
return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dirPath}` }], isError: true };
|
|
124
|
+
}
|
|
125
|
+
return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
|
|
126
|
+
}
|
|
110
127
|
}
|
|
111
128
|
);
|
|
112
129
|
server.tool(
|
|
@@ -119,18 +136,20 @@ ${error.stderr ?? ""}`
|
|
|
119
136
|
},
|
|
120
137
|
async ({ pattern, directory, file_pattern }) => {
|
|
121
138
|
try {
|
|
122
|
-
const { stdout } = await
|
|
123
|
-
|
|
139
|
+
const { stdout } = await execFileAsync(
|
|
140
|
+
"rg",
|
|
141
|
+
["--no-heading", "-n", pattern, directory],
|
|
124
142
|
{ timeout: 1e4 }
|
|
125
143
|
);
|
|
126
144
|
return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
|
|
127
145
|
} catch {
|
|
128
|
-
const
|
|
146
|
+
const safeDirectory = import_path.default.resolve(directory);
|
|
147
|
+
const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
|
|
129
148
|
const results = [];
|
|
130
149
|
for (const file of files.slice(0, 100)) {
|
|
131
150
|
try {
|
|
132
151
|
const content = await import_promises.default.readFile(
|
|
133
|
-
import_path.default.join(
|
|
152
|
+
import_path.default.join(safeDirectory, file),
|
|
134
153
|
"utf-8"
|
|
135
154
|
);
|
|
136
155
|
const lines = content.split("\n");
|
|
@@ -257,6 +276,17 @@ var import_zod2 = require("zod");
|
|
|
257
276
|
var BrowserTools = class {
|
|
258
277
|
browser = null;
|
|
259
278
|
page = null;
|
|
279
|
+
// 동시 요청 시 race condition 방지용 직렬화 락
|
|
280
|
+
lock = Promise.resolve();
|
|
281
|
+
withLock(fn) {
|
|
282
|
+
let release;
|
|
283
|
+
const next = new Promise((r) => {
|
|
284
|
+
release = r;
|
|
285
|
+
});
|
|
286
|
+
const current = this.lock;
|
|
287
|
+
this.lock = this.lock.then(() => next);
|
|
288
|
+
return current.then(() => fn()).finally(() => release());
|
|
289
|
+
}
|
|
260
290
|
async init() {
|
|
261
291
|
try {
|
|
262
292
|
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
@@ -279,22 +309,22 @@ var BrowserTools = class {
|
|
|
279
309
|
"browser_navigate",
|
|
280
310
|
"URL\uB85C \uC774\uB3D9",
|
|
281
311
|
{ url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
|
|
282
|
-
|
|
312
|
+
({ url }) => this.withLock(async () => {
|
|
283
313
|
const page = requirePage();
|
|
284
314
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
285
315
|
return {
|
|
286
316
|
content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
|
|
287
317
|
};
|
|
288
|
-
}
|
|
318
|
+
})
|
|
289
319
|
);
|
|
290
320
|
server.tool(
|
|
291
321
|
"browser_click",
|
|
292
322
|
"\uC694\uC18C \uD074\uB9AD",
|
|
293
323
|
{ selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
|
|
294
|
-
|
|
324
|
+
({ selector }) => this.withLock(async () => {
|
|
295
325
|
await requirePage().click(selector);
|
|
296
326
|
return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
|
|
297
|
-
}
|
|
327
|
+
})
|
|
298
328
|
);
|
|
299
329
|
server.tool(
|
|
300
330
|
"browser_type",
|
|
@@ -304,12 +334,12 @@ var BrowserTools = class {
|
|
|
304
334
|
text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
|
|
305
335
|
clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
|
|
306
336
|
},
|
|
307
|
-
|
|
337
|
+
({ selector, text, clear }) => this.withLock(async () => {
|
|
308
338
|
const page = requirePage();
|
|
309
339
|
if (clear) await page.fill(selector, text);
|
|
310
340
|
else await page.type(selector, text);
|
|
311
341
|
return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
|
|
312
|
-
}
|
|
342
|
+
})
|
|
313
343
|
);
|
|
314
344
|
server.tool(
|
|
315
345
|
"browser_screenshot",
|
|
@@ -318,7 +348,7 @@ var BrowserTools = class {
|
|
|
318
348
|
path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
|
|
319
349
|
full_page: import_zod2.z.boolean().optional().default(false)
|
|
320
350
|
},
|
|
321
|
-
|
|
351
|
+
({ path: path2, full_page }) => this.withLock(async () => {
|
|
322
352
|
const page = requirePage();
|
|
323
353
|
const screenshot = await page.screenshot({
|
|
324
354
|
path: path2 ?? void 0,
|
|
@@ -336,13 +366,13 @@ var BrowserTools = class {
|
|
|
336
366
|
}
|
|
337
367
|
]
|
|
338
368
|
};
|
|
339
|
-
}
|
|
369
|
+
})
|
|
340
370
|
);
|
|
341
371
|
server.tool(
|
|
342
372
|
"browser_snapshot",
|
|
343
373
|
"\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
|
|
344
374
|
{},
|
|
345
|
-
async () => {
|
|
375
|
+
() => this.withLock(async () => {
|
|
346
376
|
const page = requirePage();
|
|
347
377
|
const snapshot = await page.locator("body").ariaSnapshot();
|
|
348
378
|
return {
|
|
@@ -350,29 +380,36 @@ var BrowserTools = class {
|
|
|
350
380
|
{ type: "text", text: snapshot }
|
|
351
381
|
]
|
|
352
382
|
};
|
|
353
|
-
}
|
|
383
|
+
})
|
|
354
384
|
);
|
|
355
385
|
server.tool(
|
|
356
386
|
"browser_evaluate",
|
|
357
387
|
"JavaScript \uC2E4\uD589",
|
|
358
388
|
{ code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
389
|
+
({ code }) => this.withLock(async () => {
|
|
390
|
+
try {
|
|
391
|
+
const result = await requirePage().evaluate(code);
|
|
392
|
+
return {
|
|
393
|
+
content: [
|
|
394
|
+
{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }
|
|
395
|
+
]
|
|
396
|
+
};
|
|
397
|
+
} catch (err) {
|
|
398
|
+
return {
|
|
399
|
+
content: [{ type: "text", text: `\u274C JavaScript \uC2E4\uD589 \uC624\uB958: ${err.message}` }],
|
|
400
|
+
isError: true
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
})
|
|
367
404
|
);
|
|
368
405
|
server.tool(
|
|
369
406
|
"browser_pdf",
|
|
370
407
|
"\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
|
|
371
408
|
{ path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
|
|
372
|
-
|
|
409
|
+
({ path: path2 }) => this.withLock(async () => {
|
|
373
410
|
await requirePage().pdf({ path: path2 });
|
|
374
411
|
return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path2}` }] };
|
|
375
|
-
}
|
|
412
|
+
})
|
|
376
413
|
);
|
|
377
414
|
}
|
|
378
415
|
};
|
|
@@ -385,7 +422,11 @@ var import_util2 = require("util");
|
|
|
385
422
|
var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
|
|
386
423
|
async function readNotebook(filePath) {
|
|
387
424
|
const raw = await import_promises2.default.readFile(filePath, "utf-8");
|
|
388
|
-
|
|
425
|
+
try {
|
|
426
|
+
return JSON.parse(raw);
|
|
427
|
+
} catch {
|
|
428
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
|
|
429
|
+
}
|
|
389
430
|
}
|
|
390
431
|
async function writeNotebook(filePath, nb) {
|
|
391
432
|
await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
|
|
@@ -480,15 +521,21 @@ var NotebookTools = class {
|
|
|
480
521
|
execution_count: cellType === "code" ? null : void 0
|
|
481
522
|
};
|
|
482
523
|
let actualIndex;
|
|
524
|
+
let warning = "";
|
|
483
525
|
if (position === void 0 || position === null) {
|
|
484
526
|
nb.cells.push(newCell);
|
|
485
527
|
actualIndex = nb.cells.length - 1;
|
|
528
|
+
} else if (position > nb.cells.length) {
|
|
529
|
+
nb.cells.push(newCell);
|
|
530
|
+
actualIndex = nb.cells.length - 1;
|
|
531
|
+
warning = ` (\uACBD\uACE0: position ${position}\uC774 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uC5EC \uB05D(index: ${actualIndex})\uC5D0 \uCD94\uAC00\uB428)`;
|
|
486
532
|
} else {
|
|
487
|
-
|
|
488
|
-
|
|
533
|
+
const clamped = Math.max(0, position);
|
|
534
|
+
nb.cells.splice(clamped, 0, newCell);
|
|
535
|
+
actualIndex = clamped;
|
|
489
536
|
}
|
|
490
537
|
await writeNotebook(filePath, nb);
|
|
491
|
-
return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
|
|
538
|
+
return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})${warning}` }] };
|
|
492
539
|
}
|
|
493
540
|
);
|
|
494
541
|
server.tool(
|
|
@@ -516,13 +563,13 @@ var import_child_process3 = require("child_process");
|
|
|
516
563
|
var import_util3 = require("util");
|
|
517
564
|
var import_zod4 = require("zod");
|
|
518
565
|
var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
|
|
566
|
+
var screenRecordPid = null;
|
|
519
567
|
function platform() {
|
|
520
568
|
if (process.platform === "darwin") return "mac";
|
|
521
569
|
if (process.platform === "win32") return "win";
|
|
522
570
|
return "linux";
|
|
523
571
|
}
|
|
524
572
|
var DeviceTools = class {
|
|
525
|
-
screenRecordPid = null;
|
|
526
573
|
register(server) {
|
|
527
574
|
server.tool(
|
|
528
575
|
"screen_capture",
|
|
@@ -532,15 +579,26 @@ var DeviceTools = class {
|
|
|
532
579
|
},
|
|
533
580
|
async ({ output_path }) => {
|
|
534
581
|
const p = platform();
|
|
582
|
+
const isTmp = !output_path;
|
|
535
583
|
const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
|
|
536
584
|
const cmd = {
|
|
537
585
|
mac: `screencapture -x "${tmpPath}"`,
|
|
538
586
|
win: `nircmd.exe savescreenshot "${tmpPath}"`,
|
|
539
587
|
linux: `scrot "${tmpPath}"`
|
|
540
588
|
}[p];
|
|
541
|
-
|
|
542
|
-
|
|
589
|
+
try {
|
|
590
|
+
await execAsync3(cmd);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
|
|
593
|
+
}
|
|
594
|
+
const { readFileSync, unlinkSync } = await import("fs");
|
|
543
595
|
const data = readFileSync(tmpPath).toString("base64");
|
|
596
|
+
if (isTmp) {
|
|
597
|
+
try {
|
|
598
|
+
unlinkSync(tmpPath);
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
544
602
|
return {
|
|
545
603
|
content: [{ type: "image", data, mimeType: "image/png" }]
|
|
546
604
|
};
|
|
@@ -554,15 +612,31 @@ var DeviceTools = class {
|
|
|
554
612
|
},
|
|
555
613
|
async ({ output_path }) => {
|
|
556
614
|
const p = platform();
|
|
615
|
+
const isTmp = !output_path;
|
|
557
616
|
const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
|
|
558
617
|
const cmd = {
|
|
559
618
|
mac: `imagesnap "${tmpPath}"`,
|
|
560
619
|
win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
|
|
561
620
|
linux: `fswebcam -r 1280x720 "${tmpPath}"`
|
|
562
621
|
}[p];
|
|
563
|
-
|
|
564
|
-
|
|
622
|
+
try {
|
|
623
|
+
await execAsync3(cmd);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
const e = err;
|
|
626
|
+
return {
|
|
627
|
+
content: [{ type: "text", text: `\u274C \uCE74\uBA54\uB77C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
|
|
628
|
+
\uC6D0\uC778: ${e.message}
|
|
629
|
+
|
|
630
|
+
\uCE74\uBA54\uB77C\uAC00 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.` }],
|
|
631
|
+
isError: true
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const { readFileSync, unlinkSync } = await import("fs");
|
|
565
635
|
const data = readFileSync(tmpPath).toString("base64");
|
|
636
|
+
if (isTmp) try {
|
|
637
|
+
unlinkSync(tmpPath);
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
566
640
|
return {
|
|
567
641
|
content: [{ type: "image", data, mimeType: "image/jpeg" }]
|
|
568
642
|
};
|
|
@@ -577,11 +651,17 @@ var DeviceTools = class {
|
|
|
577
651
|
},
|
|
578
652
|
async ({ title, message }) => {
|
|
579
653
|
const p = platform();
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
654
|
+
let cmd;
|
|
655
|
+
if (p === "win") {
|
|
656
|
+
const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
|
|
657
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
658
|
+
cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
|
|
659
|
+
} else {
|
|
660
|
+
cmd = {
|
|
661
|
+
mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
|
|
662
|
+
linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
|
|
663
|
+
}[p] ?? "";
|
|
664
|
+
}
|
|
585
665
|
await execAsync3(cmd);
|
|
586
666
|
return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
|
|
587
667
|
}
|
|
@@ -622,7 +702,7 @@ var DeviceTools = class {
|
|
|
622
702
|
async ({ action, output_path }) => {
|
|
623
703
|
const p = platform();
|
|
624
704
|
if (action === "start") {
|
|
625
|
-
if (
|
|
705
|
+
if (screenRecordPid) {
|
|
626
706
|
return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
|
|
627
707
|
}
|
|
628
708
|
const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
|
|
@@ -630,17 +710,18 @@ var DeviceTools = class {
|
|
|
630
710
|
const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
|
|
631
711
|
const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
|
|
632
712
|
child.unref();
|
|
633
|
-
|
|
634
|
-
return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${
|
|
713
|
+
screenRecordPid = child.pid ?? null;
|
|
714
|
+
return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
|
|
635
715
|
} else {
|
|
636
|
-
if (!
|
|
716
|
+
if (!screenRecordPid) {
|
|
637
717
|
return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
|
|
638
718
|
}
|
|
639
719
|
try {
|
|
640
|
-
process.kill(
|
|
720
|
+
process.kill(screenRecordPid, "SIGINT");
|
|
721
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
641
722
|
} catch {
|
|
642
723
|
}
|
|
643
|
-
|
|
724
|
+
screenRecordPid = null;
|
|
644
725
|
return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
|
|
645
726
|
}
|
|
646
727
|
}
|
|
@@ -659,12 +740,15 @@ var DeviceTools = class {
|
|
|
659
740
|
} catch {
|
|
660
741
|
}
|
|
661
742
|
}
|
|
662
|
-
const res = await fetch("
|
|
743
|
+
const res = await fetch("http://ip-api.com/json/");
|
|
663
744
|
const data = await res.json();
|
|
745
|
+
if (data.status !== "success") {
|
|
746
|
+
throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
|
|
747
|
+
}
|
|
664
748
|
return {
|
|
665
749
|
content: [{
|
|
666
750
|
type: "text",
|
|
667
|
-
text: `\uC704\uB3C4: ${data.
|
|
751
|
+
text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
|
|
668
752
|
}]
|
|
669
753
|
};
|
|
670
754
|
}
|