diff --git a/LICENSE b/LICENSE
index b384abc..fed9f87 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 hyungyunlim
+Copyright (c) 2026 hyungyunlim
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/main.js b/main.js
deleted file mode 100644
index f3eca6b..0000000
--- a/main.js
+++ /dev/null
@@ -1,14383 +0,0 @@
-/*
-THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
-if you want to view the source, please visit the github repository of this plugin
-*/
-
-var __defProp = Object.defineProperty;
-var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
-var __getOwnPropNames = Object.getOwnPropertyNames;
-var __hasOwnProp = Object.prototype.hasOwnProperty;
-var __esm = (fn, res) => function __init() {
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
-};
-var __export = (target, all) => {
- for (var name in all)
- __defProp(target, name, { get: all[name], enumerable: true });
-};
-var __copyProps = (to, from, except, desc) => {
- if (from && typeof from === "object" || typeof from === "function") {
- for (let key of __getOwnPropNames(from))
- if (!__hasOwnProp.call(to, key) && key !== except)
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
- }
- return to;
-};
-var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
-
-// src/constants/aiPrompts.ts
-var AI_PROMPTS;
-var init_aiPrompts = __esm({
- "src/constants/aiPrompts.ts"() {
- AI_PROMPTS = {
- analysisSystem: `\uB2F9\uC2E0\uC740 \uD55C\uAD6D\uC5B4 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC804\uBB38\uAC00\uC785\uB2C8\uB2E4. \uC8FC\uC5B4\uC9C4 \uD14D\uC2A4\uD2B8\uC640 \uB9DE\uCDA4\uBC95 \uC624\uB958\uB4E4\uC744 \uBD84\uC11D\uD558\uC5EC \uAC00\uC7A5 \uC801\uC808\uD55C \uC218\uC815\uC0AC\uD56D\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694.
-
-\uB2E4\uC74C \uADDC\uCE59\uC744 \uB530\uB77C\uC8FC\uC138\uC694:
-1. \uBB38\uB9E5\uC5D0 \uAC00\uC7A5 \uC801\uD569\uD55C \uC218\uC815\uC548\uC744 \uC120\uD0DD\uD558\uC138\uC694
-2. \uACE0\uC720\uBA85\uC0AC, URL, \uC774\uBA54\uC77C, \uC804\uBB38\uC6A9\uC5B4\uB294 \uC608\uC678\uCC98\uB9AC\uB97C \uACE0\uB824\uD558\uC138\uC694
-3. \uC560\uB9E4\uD55C \uACBD\uC6B0\uB098 \uC6D0\uBB38\uC774 \uC801\uC808\uD55C \uACBD\uC6B0 \uC6D0\uBCF8\uC720\uC9C0\uB97C \uC120\uD0DD\uD558\uC138\uC694
-4. \uAC01 \uC120\uD0DD\uC5D0 \uB300\uD55C \uC2E0\uB8B0\uB3C4(0-100)\uC640 \uAC04\uB2E8\uD55C \uC774\uC720\uB97C \uC81C\uACF5\uD558\uC138\uC694
-
-\u{1F3A8} UI \uC0C1\uD0DC \uC0C9\uC0C1 \uAC00\uC774\uB4DC:
-- \u{1F534} \uBE68\uAC04\uC0C9: \uC624\uB958 \uC0C1\uD0DC (\uC6D0\uBCF8 \uC624\uB958 \uD14D\uC2A4\uD2B8)
-- \u{1F7E2} \uCD08\uB85D\uC0C9: \uC218\uC815 \uC81C\uC548 \uC0C1\uD0DC (AI/\uC2DC\uC2A4\uD15C \uC81C\uC548 \uC218\uC815\uC548)
-- \u{1F535} \uD30C\uB780\uC0C9: \uC608\uC678\uCC98\uB9AC \uC0C1\uD0DC (\uD5A5\uD6C4 \uAC80\uC0AC \uC81C\uC678)
-- \u{1F7E0} \uC8FC\uD669\uC0C9: \uC6D0\uBCF8\uC720\uC9C0 \uC0C1\uD0DC (\uC774\uBC88\uB9CC \uC720\uC9C0, \uB2E4\uC74C\uC5D0 \uB2E4\uC2DC \uAC80\uC0AC)
-- \u{1F7E3} \uBCF4\uB77C\uC0C9: \uC0AC\uC6A9\uC790\uD3B8\uC9D1 \uC0C1\uD0DC (\uC0AC\uC6A9\uC790\uAC00 CMD+E/\uC6B0\uD074\uB9AD/\uCE74\uB4DC\uD074\uB9AD\uC73C\uB85C \uC9C1\uC811 \uD3B8\uC9D1\uD55C \uD14D\uC2A4\uD2B8)
-
-\u{1F4CB} \uC608\uC678\uCC98\uB9AC vs \uC6D0\uBCF8\uC720\uC9C0 \uAC00\uC774\uB4DC\uB77C\uC778:
-
-\u{1F535} \uC608\uC678\uCC98\uB9AC (isExceptionProcessed: true) - \uD5A5\uD6C4 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC\uC5D0\uC11C \uC81C\uC678
-\u2705 \uC801\uC6A9 \uB300\uC0C1:
-- \uC804\uBB38\uC6A9\uC5B4/\uBE0C\uB79C\uB4DC\uBA85: React, GitHub, Instagram, \uCE74\uCE74\uC624\uD1A1, \uC2AC\uB799
-- \uD504\uB85C\uADF8\uB798\uBC0D \uC6A9\uC5B4: API, URL, JSON, TypeScript
-- \uACE0\uC720\uD55C \uC11C\uBE44\uC2A4\uBA85: \uB124\uC774\uBC84\uC6F9\uD230, \uC720\uD29C\uBE0C, \uC544\uB9C8\uC874\uC6F9\uC11C\uBE44\uC2A4
-- \uC678\uAD6D\uC5B4 \uD45C\uAE30: JavaScript, Kubernetes, Docker
-- \uC778\uBA85/\uC9C0\uBA85: \uAE40\uCCA0\uC218, \uB274\uC695, \uD30C\uB9AC
-
-\u{1F7E1} \uC6D0\uBCF8\uC720\uC9C0 (isExceptionProcessed: false) - \uC774\uBC88\uC5D0\uB9CC \uC720\uC9C0, \uB2E4\uC74C\uC5D0 \uB2E4\uC2DC \uAC80\uC0AC
-\u2705 \uC801\uC6A9 \uB300\uC0C1:
-- \uC77C\uBC18 \uBA85\uC0AC\uC758 \uC790\uC5F0\uC2A4\uB7EC\uC6B4 \uD45C\uD604: "\uC2DC\uAC04", "\uC0AC\uB78C", "\uC74C\uC2DD"
-- \uBB38\uB9E5\uC0C1 \uC801\uC808\uD55C \uD45C\uD604\uC774\uC9C0\uB9CC \uD655\uC2E0\uC774 \uC5C6\uB294 \uACBD\uC6B0
-- \uB9DE\uCDA4\uBC95 \uADDC\uCE59\uC774 \uC560\uB9E4\uD55C \uACBD\uC6B0
-- \uC791\uC131\uC790\uC758 \uC758\uB3C4\uAC00 \uBA85\uD655\uD574 \uBCF4\uC774\uB294 \uD45C\uD604
-
-\u{1F7E3} \uC0AC\uC6A9\uC790\uD3B8\uC9D1 \uC0C1\uD0DC \uCC98\uB9AC \uAC00\uC774\uB4DC:
-- **\uC0AC\uC6A9\uC790\uAC00 \uC774\uBBF8 \uC9C1\uC811 \uD3B8\uC9D1\uD55C \uD14D\uC2A4\uD2B8\uB294 \uC0AC\uC6A9\uC790\uC758 \uC758\uB3C4\uB97C \uCD5C\uB300\uD55C \uC874\uC911\uD558\uC138\uC694**
-- \uC0AC\uC6A9\uC790 \uD3B8\uC9D1 \uD14D\uC2A4\uD2B8\uAC00 \uBB38\uBC95\uC801\uC73C\uB85C \uC62C\uBC14\uB974\uB2E4\uBA74 \uADF8\uB300\uB85C \uC720\uC9C0 \uAD8C\uC7A5
-- \uBA85\uBC31\uD55C \uC624\uD0C0\uAC00 \uC788\uB354\uB77C\uB3C4 \uC0AC\uC6A9\uC790 \uC758\uB3C4\uB97C \uACE0\uB824\uD558\uC5EC \uC2E0\uC911\uD558\uAC8C \uD310\uB2E8
-- \uC0AC\uC6A9\uC790 \uD3B8\uC9D1\uC774 \uBD88\uC644\uC804\uD574 \uBCF4\uC774\uBA74 \uBE44\uC2B7\uD55C \uBC29\uD5A5\uC758 \uC218\uC815\uC548 \uC81C\uC548 \uACE0\uB824
-
-\u274C \uAD6C\uBD84 \uC6D0\uCE59:
-- \uACE0\uC720\uBA85\uC0AC\uC774\uBA74\uC11C \uC7AC\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC6A9\uC5B4 \u2192 \uC608\uC678\uCC98\uB9AC
-- \uC77C\uBC18\uC801\uC778 \uB2E8\uC5B4\uB098 \uD45C\uD604 \u2192 \uC6D0\uBCF8\uC720\uC9C0
-
-\u26A0\uFE0F \uC911\uC694\uD55C \uC751\uB2F5 \uADDC\uCE59:
-- selectedValue\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uC81C\uACF5\uB41C \uC218\uC815\uC548 \uC911 \uD558\uB098 \uB610\uB294 \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uB9CC \uC785\uB825\uD558\uC138\uC694
-- "\uC6D0\uBCF8\uC720\uC9C0", "\uC608\uC678\uCC98\uB9AC" \uAC19\uC740 \uBA85\uB839\uC5B4\uB97C \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694
-- \uC6D0\uBCF8\uC744 \uC720\uC9C0\uD558\uB824\uBA74 \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uB97C selectedValue\uC5D0 \uC785\uB825\uD558\uC138\uC694
-- \uC608\uC678\uCC98\uB9AC\uD558\uB824\uBA74 \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uB97C selectedValue\uC5D0 \uC785\uB825\uD558\uACE0 isExceptionProcessed\uB97C true\uB85C \uC124\uC815\uD558\uC138\uC694
-
-\u{1F534} \uC815\uD655\uD55C \uD14D\uC2A4\uD2B8 \uB9E4\uCE6D \uC8FC\uC758\uC0AC\uD56D:
-- "\uC9C0\uD0AC"\uACFC "\uC9C0 \uD0AC"\uC740 \uC644\uC804\uD788 \uB2E4\uB978 \uB2E8\uC5B4\uC785\uB2C8\uB2E4
-- \uC6D0\uBCF8\uC744 \uC720\uC9C0\uD558\uB824\uBA74 \uC815\uD655\uD788 "\uC9C0\uD0AC"\uC744 \uC120\uD0DD\uD558\uC138\uC694 (\uB744\uC5B4\uC4F0\uAE30 \uC5C6\uC74C)
-- \uAD04\uD638\uB098 \uC124\uBA85\uC744 \uCD94\uAC00\uD558\uC9C0 \uB9C8\uC138\uC694: "\uD734\uACE0" (\u25CB) vs "\uD734\uACE0(Hugo)" (\u2717)
-- \uACF5\uBC31 \uD558\uB098\uC758 \uCC28\uC774\uB3C4 \uC758\uBBF8\uAC00 \uC644\uC804\uD788 \uB2EC\uB77C\uC9C8 \uC218 \uC788\uC2B5\uB2C8\uB2E4
-
-\uC120\uD0DD \uBC29\uBC95:
-1. \uC218\uC815\uC774 \uD544\uC694\uD55C \uACBD\uC6B0: \uC81C\uACF5\uB41C \uC218\uC815\uC548 \uC911 \uD558\uB098\uB97C selectedValue\uC5D0 \uC785\uB825
-2. \uC6D0\uBCF8\uC744 \uC720\uC9C0\uD558\uB294 \uACBD\uC6B0: \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uB97C \uC815\uD655\uD788 selectedValue\uC5D0 \uC785\uB825, isExceptionProcessed: false
-3. \uC608\uC678\uCC98\uB9AC\uD558\uB294 \uACBD\uC6B0: \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uB97C \uC815\uD655\uD788 selectedValue\uC5D0 \uC785\uB825, isExceptionProcessed: true
-
-\u26A0\uFE0F \uC751\uB2F5 \uD615\uC2DD:
-- \uC624\uC9C1 JSON \uBC30\uC5F4\uB9CC \uC751\uB2F5\uD558\uC138\uC694. \uB2E4\uB978 \uD14D\uC2A4\uD2B8\uB098 \uC124\uBA85\uC740 \uD3EC\uD568\uD558\uC9C0 \uB9C8\uC138\uC694.
-- \uB9C8\uD06C\uB2E4\uC6B4 \uCF54\uB4DC \uBE14\uB85D\uC744 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.
-
-\uC751\uB2F5 \uD615\uC2DD \uC608\uC2DC:
-[
- {
- "correctionIndex": 0,
- "selectedValue": "\uB530\uB77C",
- "isExceptionProcessed": false,
- "confidence": 90,
- "reasoning": "\uBB38\uB9E5\uC0C1 \uC6D0\uBCF8\uC774 \uC801\uC808\uD55C \uD45C\uD604"
- },
- {
- "correctionIndex": 1,
- "selectedValue": "\uC2AC\uB799",
- "isExceptionProcessed": true,
- "confidence": 100,
- "reasoning": "\uBE0C\uB79C\uB4DC\uBA85\uC73C\uB85C \uC608\uC678\uCC98\uB9AC\uD558\uC5EC \uD5A5\uD6C4 \uAC80\uC0AC \uC81C\uC678"
- },
- {
- "correctionIndex": 2,
- "selectedValue": "\uC2DC\uAC04",
- "isExceptionProcessed": false,
- "confidence": 85,
- "reasoning": "\uC77C\uBC18 \uBA85\uC0AC\uB85C \uC6D0\uBCF8\uC720\uC9C0, \uB2E4\uC74C\uC5D0 \uB2E4\uC2DC \uAC80\uC0AC"
- }
-]`,
- analysisUser: (originalText, corrections) => `\uC6D0\uBB38: "${originalText}"
-
-\uBC1C\uACAC\uB41C \uB9DE\uCDA4\uBC95 \uC624\uB958\uB4E4:
-${corrections.map(
- (correction, index) => `${index}. "${correction.original}" \u2192 \uC218\uC815\uC548: [${correction.corrected.join(", ")}] (\uC124\uBA85: ${correction.help})`
- ).join("\n")}
-
-\uC704 \uC624\uB958\uB4E4\uC5D0 \uB300\uD574 \uBB38\uB9E5\uC744 \uACE0\uB824\uD558\uC5EC \uAC00\uC7A5 \uC801\uC808\uD55C \uC120\uD0DD\uC744 \uD574\uC8FC\uC138\uC694.`,
- analysisUserWithContext: (correctionContexts) => `\uCD1D ${correctionContexts.length}\uAC1C\uC758 \uB9DE\uCDA4\uBC95 \uC624\uB958\uB4E4\uACFC \uC8FC\uBCC0 \uBB38\uB9E5:
-
-${correctionContexts.map((ctx, index) => {
- let contextInfo = `${index}. \uC624\uB958: "${ctx.original}"
- \uBB38\uB9E5: "${ctx.fullContext}"
- \uC218\uC815\uC548: [${ctx.corrected.join(", ")}]
- \uC124\uBA85: ${ctx.help}`;
- if (ctx.currentState && ctx.currentValue) {
- const stateNames = {
- "error": "\u{1F534} \uC624\uB958",
- "corrected": "\u{1F7E2} \uC218\uC815",
- "exception-processed": "\u{1F535} \uC608\uC678\uCC98\uB9AC",
- "original-kept": "\u{1F7E0} \uC6D0\uBCF8\uC720\uC9C0",
- "user-edited": "\u{1F7E3} \uC0AC\uC6A9\uC790\uD3B8\uC9D1"
- };
- const stateName = stateNames[ctx.currentState] || `\u{1F518} ${ctx.currentState}`;
- contextInfo += `
- \uD604\uC7AC \uC0C1\uD0DC: ${stateName} (\uAC12: "${ctx.currentValue}")`;
- }
- return contextInfo + "\n \n";
- }).join("")}\u26A0\uFE0F \uC911\uC694 \uC751\uB2F5 \uADDC\uCE59:
-1. \uC704\uC758 \uBAA8\uB4E0 ${correctionContexts.length}\uAC1C \uC624\uB958\uC5D0 \uB300\uD574 \uBC18\uB4DC\uC2DC \uBD84\uC11D \uACB0\uACFC\uB97C \uC81C\uACF5\uD574\uC8FC\uC138\uC694.
-2. correctionIndex\uB294 \uBC18\uB4DC\uC2DC 0\uBD80\uD130 ${correctionContexts.length - 1}\uAE4C\uC9C0\uC758 \uC21C\uC11C\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.
-3. selectedValue\uB294 \uBC18\uB4DC\uC2DC \uC81C\uACF5\uB41C \uC218\uC815\uC548 \uC911 \uD558\uB098 \uB610\uB294 \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uC640 \uC815\uD655\uD788 \uC77C\uCE58\uD574\uC57C \uD569\uB2C8\uB2E4.
-4. \uD2B9\uC218\uBB38\uC790(**, ~, - \uB4F1)\uC640 \uACF5\uBC31/\uB744\uC5B4\uC4F0\uAE30\uB97C \uC815\uD655\uD788 \uBCF5\uC0AC\uD574\uC11C \uC0AC\uC6A9\uD558\uC138\uC694.
-5. \uC6D0\uBCF8 \uC720\uC9C0 \uC2DC\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uB97C \uC815\uD655\uD788 \uC785\uB825\uD558\uC138\uC694.
-6. \uB204\uB77D\uB41C \uC624\uB958\uAC00 \uC788\uC73C\uBA74 \uC548 \uB429\uB2C8\uB2E4.
-
-\u26A0\uFE0F \uC815\uD655\uD55C \uB9E4\uCE6D \uC608\uC2DC:
-- \uC624\uB958: "\uC9C0\uD0AC" \u2192 \uC218\uC815\uC548: ["\uC9C0 \uD0AC", "\uC9C0\uD0A8"] \u2192 \uC6D0\uBCF8 \uC720\uC9C0 \uC2DC selectedValue: "\uC9C0\uD0AC" (\uC6D0\uBCF8 \uADF8\uB300\uB85C)
-- \uC624\uB958: "\uC5B4" \u2192 \uC218\uC815\uC548: ["**\uC5B4", "**\uC544"] \u2192 selectedValue: "**\uC5B4" (\uD2B9\uC218\uBB38\uC790 \uD3EC\uD568 \uC815\uD655\uD788)
-- \uC624\uB958: "\uCD1D" \u2192 \uC218\uC815\uC548: ["** \uCD1D", "**\uCD1D"] \u2192 selectedValue: "** \uCD1D" (\uACF5\uBC31 \uD3EC\uD568 \uC815\uD655\uD788)`,
- // 새로운 형태소 기반 프롬프트 ⭐ NEW
- analysisUserWithMorphemes: (correctionContexts, morphemeInfo) => {
- let prompt = `\uCD1D ${correctionContexts.length}\uAC1C\uC758 \uB9DE\uCDA4\uBC95 \uC624\uB958 \uBD84\uC11D:
-
-${correctionContexts.map((ctx, index) => {
- let contextInfo = `${index}. \uC624\uB958: "${ctx.original}"
- \uC218\uC815\uC548: [${ctx.corrected.join(", ")}]
- \uC124\uBA85: ${ctx.help}
- \uBB38\uB9E5: "${ctx.fullContext}"`;
- if (ctx.currentState && ctx.currentValue) {
- const stateNames = {
- "error": "\u{1F534} \uC624\uB958",
- "corrected": "\u{1F7E2} \uC218\uC815",
- "exception-processed": "\u{1F535} \uC608\uC678\uCC98\uB9AC",
- "original-kept": "\u{1F7E0} \uC6D0\uBCF8\uC720\uC9C0",
- "user-edited": "\u{1F7E3} \uC0AC\uC6A9\uC790\uD3B8\uC9D1"
- };
- const stateName = stateNames[ctx.currentState] || `\u{1F518} ${ctx.currentState}`;
- contextInfo += `
- \uD604\uC7AC \uC0C1\uD0DC: ${stateName} (\uAC12: "${ctx.currentValue}")`;
- }
- if (ctx.isLikelyProperNoun && ctx.sentenceContext) {
- contextInfo += `
- \u{1F4CD} \uACE0\uC720\uBA85\uC0AC \uAC00\uB2A5\uC131 \uB192\uC74C - \uC804\uCCB4 \uBB38\uC7A5: "${ctx.sentenceContext}"`;
- }
- return contextInfo;
- }).join("\n\n")}`;
- if (morphemeInfo && morphemeInfo.tokens && morphemeInfo.tokens.length > 0) {
- const coreTokens = morphemeInfo.tokens.slice(0, 10);
- const morphemeData = coreTokens.map((token) => {
- var _a;
- const mainTag = ((_a = token.morphemes[0]) == null ? void 0 : _a.tag) || "UNK";
- return `${token.text.content}(${mainTag})`;
- }).join(", ");
- prompt += `
-
-\u{1F4CB} \uD488\uC0AC \uC815\uBCF4: ${morphemeData}
-\u{1F4A1} \uD488\uC0AC\uB97C \uACE0\uB824\uD55C \uBB38\uBC95\uC801 \uAD50\uC815\uC744 \uC120\uD0DD\uD558\uC138\uC694.`;
- }
- const hasProperNouns = correctionContexts.some((ctx) => ctx.isLikelyProperNoun);
- if (hasProperNouns) {
- prompt += `
-
-\u{1F3F7}\uFE0F \uACE0\uC720\uBA85\uC0AC \uCC98\uB9AC \uAC00\uC774\uB4DC:
-
-\u{1F535} \uC608\uC678\uCC98\uB9AC \uAD8C\uC7A5 (isExceptionProcessed: true):
-- \uBE0C\uB79C\uB4DC\uBA85/\uC11C\uBE44\uC2A4\uBA85: \uC2AC\uB799, \uC778\uC2A4\uD0C0\uADF8\uB7A8, \uCE74\uCE74\uC624\uD1A1, \uB124\uC774\uBC84\uC6F9\uD230
-- \uD504\uB85C\uADF8\uB798\uBC0D \uC6A9\uC5B4: \uB9AC\uC561\uD2B8, \uAE43\uD5C8\uBE0C, \uD0C0\uC785\uC2A4\uD06C\uB9BD\uD2B8, \uCFE0\uBC84\uB124\uD2F0\uC2A4
-- \uC18C\uD504\uD2B8\uC6E8\uC5B4 \uB3C4\uAD6C: \uB3C4\uCEE4, \uC820\uD0A8\uC2A4, \uC8FC\uD53C\uD130, \uC140\uB808\uB2C8\uC6C0
-- \uC804\uBB38 \uC6A9\uC5B4: \uB370\uC774\uD130\uBCA0\uC774\uC2A4, \uC6F9\uC0AC\uC774\uD2B8, \uB9C8\uD06C\uB2E4\uC6B4, \uC5D4\uB4DC\uD3EC\uC778\uD2B8
-
-\u{1F7E1} \uC6D0\uBCF8\uC720\uC9C0 \uAD8C\uC7A5 (isExceptionProcessed: false):
-- \uC77C\uBC18 \uC2DC\uAC04 \uBA85\uC0AC: "\uC2DC\uAC04", "\uB0A0\uC9DC", "\uC6D4\uC694\uC77C", "\uC624\uC804"
-- \uC77C\uBC18 \uC704\uCE58 \uBA85\uC0AC: "\uAC74\uBB3C", "\uC0AC\uBB34\uC2E4", "\uD68C\uC758\uC2E4", "\uC9D1"
-- \uC77C\uBC18 \uC0AC\uB78C \uBA85\uC0AC: "\uAC1C\uBC1C\uC790", "\uB514\uC790\uC774\uB108", "\uC0AC\uC6A9\uC790", "\uACE0\uAC1D"
-- \uC77C\uBC18 \uAC1D\uCCB4 \uBA85\uC0AC: "\uD30C\uC77C", "\uD3F4\uB354", "\uBB38\uC11C", "\uC774\uBBF8\uC9C0"
-
-\u{1F4A1} \uD310\uB2E8 \uAE30\uC900:
-- \uC7AC\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uACE0\uC720\uD55C \uC6A9\uC5B4 \u2192 \uC608\uC678\uCC98\uB9AC
-- \uC77C\uBC18\uC801\uC778 \uBA85\uC0AC\uB098 \uD45C\uD604 \u2192 \uC6D0\uBCF8\uC720\uC9C0
-- \uBB38\uC7A5 \uB9E5\uB77D\uC744 \uACE0\uB824\uD558\uC5EC \uC2E0\uC911\uD558\uAC8C \uC120\uD0DD\uD558\uC138\uC694.`;
- }
- prompt += `
-
-\u26A0\uFE0F \uC911\uC694 \uC751\uB2F5 \uADDC\uCE59:
-1. \uC704\uC758 \uBAA8\uB4E0 ${correctionContexts.length}\uAC1C \uC624\uB958\uC5D0 \uB300\uD574 \uBC18\uB4DC\uC2DC \uBD84\uC11D \uACB0\uACFC\uB97C \uC81C\uACF5\uD574\uC8FC\uC138\uC694.
-2. correctionIndex\uB294 \uBC18\uB4DC\uC2DC 0\uBD80\uD130 ${correctionContexts.length - 1}\uAE4C\uC9C0\uC758 \uC21C\uC11C\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.
-3. selectedValue\uB294 \uBC18\uB4DC\uC2DC \uC81C\uACF5\uB41C \uC218\uC815\uC548 \uC911 \uD558\uB098 \uB610\uB294 \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uC640 \uC815\uD655\uD788 \uC77C\uCE58\uD574\uC57C \uD569\uB2C8\uB2E4.
-4. \u{1F534} \uC815\uD655\uD55C \uD14D\uC2A4\uD2B8 \uB9E4\uCE6D: "\uC9C0\uD0AC"\u2260"\uC9C0 \uD0AC", \uAD04\uD638 \uCD94\uAC00 \uAE08\uC9C0 ("\uD734\uACE0"\u2260"\uD734\uACE0(Hugo)"), \uC6D0\uBCF8 \uC720\uC9C0 \uC2DC \uC815\uD655\uD55C \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uB9CC \uC0AC\uC6A9
-5. \uD615\uD0DC\uC18C \uC815\uBCF4\uC640 \uBB38\uC7A5 \uB9E5\uB77D\uC744 \uC885\uD569\uC801\uC73C\uB85C \uACE0\uB824\uD558\uC5EC \uD310\uB2E8\uD558\uC138\uC694.
-6. \uACE0\uC720\uBA85\uC0AC\uB294 \uC608\uC678\uCC98\uB9AC(isExceptionProcessed: true) \uC6B0\uC120 \uACE0\uB824\uD558\uC138\uC694.
-7. \uB204\uB77D\uB41C \uC624\uB958\uAC00 \uC788\uC73C\uBA74 \uC548 \uB429\uB2C8\uB2E4.`;
- return prompt;
- }
- };
- }
-});
-
-// src/constants/aiModels.ts
-var aiModels_exports = {};
-__export(aiModels_exports, {
- AI_PROMPTS: () => AI_PROMPTS,
- AI_PROVIDER_DEFAULTS: () => AI_PROVIDER_DEFAULTS,
- ANTHROPIC_MODELS: () => ANTHROPIC_MODELS,
- API_ENDPOINTS: () => API_ENDPOINTS,
- DEFAULT_AI_SETTINGS: () => DEFAULT_AI_SETTINGS,
- GOOGLE_MODELS: () => GOOGLE_MODELS,
- MODEL_PREFIXES: () => MODEL_PREFIXES,
- MODEL_TOKEN_LIMITS: () => MODEL_TOKEN_LIMITS,
- OLLAMA_MODELS: () => OLLAMA_MODELS,
- OPENAI_MODELS: () => OPENAI_MODELS,
- RECOMMENDED_MODELS_FOR_KOREAN: () => RECOMMENDED_MODELS_FOR_KOREAN,
- getCurrentModelInfo: () => getCurrentModelInfo
-});
-function getCurrentModelInfo(aiSettings) {
- if (!aiSettings || !aiSettings.enabled) {
- return {
- provider: "N/A",
- model: "N/A",
- displayName: "AI \uAE30\uB2A5 \uBE44\uD65C\uC131\uD654"
- };
- }
- const provider = aiSettings.provider || "openai";
- const model = aiSettings[`${provider}Model`] || AI_PROVIDER_DEFAULTS[provider];
- const providerDisplayNames = {
- openai: "OpenAI",
- anthropic: "Anthropic",
- google: "Google",
- ollama: "Ollama (\uB85C\uCEEC)"
- };
- const modelDisplayNames = {
- // OpenAI
- "gpt-4o": "GPT-4o",
- "gpt-4o-mini": "GPT-4o Mini",
- "gpt-4-turbo": "GPT-4 Turbo",
- "gpt-4": "GPT-4",
- "gpt-3.5-turbo": "GPT-3.5 Turbo",
- "o1-preview": "o1 Preview",
- "o1-mini": "o1 Mini",
- // Anthropic
- "claude-3-5-sonnet-20241022": "Claude 3.5 Sonnet",
- "claude-3-5-haiku-20241022": "Claude 3.5 Haiku",
- "claude-3-opus-20240229": "Claude 3 Opus",
- "claude-3-sonnet-20240229": "Claude 3 Sonnet",
- "claude-3-haiku-20240307": "Claude 3 Haiku",
- // Google
- "gemini-1.5-pro": "Gemini 1.5 Pro",
- "gemini-1.5-flash": "Gemini 1.5 Flash",
- "gemini-1.5-flash-8b": "Gemini 1.5 Flash 8B",
- "gemini-1.0-pro": "Gemini 1.0 Pro",
- // Ollama (일반적인 패턴으로 표시)
- "llama3.2:3b": "Llama 3.2 (3B)",
- "llama3.2:1b": "Llama 3.2 (1B)",
- "llama3.1:8b": "Llama 3.1 (8B)",
- "mistral:7b": "Mistral (7B)",
- "qwen2:7b": "Qwen 2 (7B)"
- };
- const providerName = providerDisplayNames[provider] || provider.toUpperCase();
- const modelName = modelDisplayNames[model] || model;
- return {
- provider: providerName,
- model: modelName,
- displayName: `${providerName} ${modelName}`
- };
-}
-var AI_PROVIDER_DEFAULTS, OPENAI_MODELS, ANTHROPIC_MODELS, GOOGLE_MODELS, OLLAMA_MODELS, API_ENDPOINTS, MODEL_PREFIXES, RECOMMENDED_MODELS_FOR_KOREAN, MODEL_TOKEN_LIMITS, DEFAULT_AI_SETTINGS;
-var init_aiModels = __esm({
- "src/constants/aiModels.ts"() {
- init_aiPrompts();
- AI_PROVIDER_DEFAULTS = {
- openai: "gpt-4o-mini",
- anthropic: "claude-3-haiku-20240307",
- google: "gemini-1.5-flash",
- ollama: "llama3.2:3b"
- };
- OPENAI_MODELS = [
- "gpt-4o",
- "gpt-4o-mini",
- "gpt-4-turbo",
- "gpt-4",
- "gpt-3.5-turbo",
- "gpt-3.5-turbo-16k",
- "o1-preview",
- "o1-mini"
- ];
- ANTHROPIC_MODELS = [
- "claude-3-5-sonnet-20241022",
- "claude-3-5-haiku-20241022",
- "claude-3-opus-20240229",
- "claude-3-sonnet-20240229",
- "claude-3-haiku-20240307"
- ];
- GOOGLE_MODELS = [
- "gemini-1.5-pro",
- "gemini-1.5-flash",
- "gemini-1.5-flash-8b",
- "gemini-1.0-pro"
- ];
- OLLAMA_MODELS = [
- "llama3.2:3b",
- "llama3.2:1b",
- "llama3.1:8b",
- "mistral:7b",
- "qwen2:7b"
- ];
- API_ENDPOINTS = {
- openai: {
- base: "https://api.openai.com/v1",
- chat: "https://api.openai.com/v1/chat/completions",
- models: "https://api.openai.com/v1/models"
- },
- anthropic: {
- base: "https://api.anthropic.com",
- messages: "https://api.anthropic.com/v1/messages"
- },
- google: {
- base: "https://generativelanguage.googleapis.com/v1",
- generateContent: (model) => `https://generativelanguage.googleapis.com/v1/models/${model}:generateContent`
- }
- };
- MODEL_PREFIXES = {
- openai: ["gpt-", "o1-", "text-", "davinci-", "curie-", "babbage-", "ada-"],
- anthropic: ["claude-"],
- google: ["gemini-", "palm-", "chat-bison", "text-bison"]
- };
- RECOMMENDED_MODELS_FOR_KOREAN = [
- "gpt-4o",
- "gpt-4o-mini",
- "claude-3-5-sonnet-20241022",
- "claude-3-5-haiku-20241022",
- "gemini-1.5-pro",
- "gemini-1.5-flash"
- ];
- MODEL_TOKEN_LIMITS = {
- // OpenAI 모델들
- "gpt-4o": 4096,
- "gpt-4o-mini": 16384,
- "gpt-4-turbo": 4096,
- "gpt-4": 4096,
- "gpt-3.5-turbo": 4096,
- "gpt-3.5-turbo-16k": 4096,
- "o1-preview": 32768,
- "o1-mini": 65536,
- // Anthropic 모델들 (매우 높은 한계)
- "claude-3-5-sonnet-20241022": 8192,
- "claude-3-5-haiku-20241022": 8192,
- "claude-3-opus-20240229": 4096,
- "claude-3-sonnet-20240229": 4096,
- "claude-3-haiku-20240307": 4096,
- // Google 모델들
- "gemini-1.5-pro": 8192,
- "gemini-1.5-flash": 8192,
- "gemini-1.5-flash-8b": 8192,
- "gemini-1.0-pro": 2048,
- // Ollama 모델들 (설정 가능하지만 보수적으로)
- "llama3.2:3b": 2048,
- "llama3.2:1b": 2048,
- "llama3.1:8b": 2048,
- "mistral:7b": 2048,
- "qwen2:7b": 2048
- };
- DEFAULT_AI_SETTINGS = {
- enabled: false,
- // 기본적으로 비활성화
- provider: "openai",
- openaiApiKey: "",
- anthropicApiKey: "",
- googleApiKey: "",
- ollamaEndpoint: "http://localhost:11434",
- model: "gpt-4o-mini",
- maxTokens: 2e3,
- // 기본값 (모델별 제한 내에서 자동 조정됨)
- temperature: 0.1,
- // 낮은 값으로 설정하여 일관된 결과 도출
- showTokenWarning: true,
- // 기본적으로 토큰 경고 활성화
- tokenWarningThreshold: 3e3
- // 3000 토큰 이상일 때 경고
- };
- }
-});
-
-// src/utils/logger.ts
-var logger_exports = {};
-__export(logger_exports, {
- Logger: () => Logger
-});
-var Logger;
-var init_logger = __esm({
- "src/utils/logger.ts"() {
- Logger = class {
- static isDevelopment() {
- return true;
- }
- /**
- * 현재 로그 레벨이 출력 가능한지 확인합니다
- */
- static shouldLog(level) {
- return this.logLevelPriority[level] >= this.logLevelPriority[this.currentLogLevel];
- }
- /**
- * 일반 정보 로그
- */
- static log(message, ...args) {
- if (!this.shouldLog("INFO"))
- return;
- this.writeLog("INFO", message, args.length > 0 ? args : void 0);
- if (this.isDevelopment()) {
- console.log(`[Korean Grammar Assistant] ${message}`, ...args);
- }
- }
- /**
- * 디버그 로그 (개발 모드에서만)
- */
- static debug(message, ...args) {
- if (!this.shouldLog("DEBUG"))
- return;
- this.writeLog("DEBUG", message, args.length > 0 ? args : void 0);
- if (this.isDevelopment()) {
- console.debug(`[Korean Grammar Assistant DEBUG] ${message}`, ...args);
- }
- }
- /**
- * 경고 로그
- */
- static warn(message, ...args) {
- if (!this.shouldLog("WARN"))
- return;
- this.writeLog("WARN", message, args.length > 0 ? args : void 0);
- if (this.isDevelopment()) {
- console.warn(`[Korean Grammar Assistant WARN] ${message}`, ...args);
- }
- }
- /**
- * 에러 로그 (프로덕션에서도 표시)
- */
- static error(message, ...args) {
- if (!this.shouldLog("ERROR"))
- return;
- this.writeLog("ERROR", message, args.length > 0 ? args : void 0);
- if (this.isDevelopment()) {
- console.error(`[Korean Grammar Assistant ERROR] ${message}`, ...args);
- } else {
- console.error(`[Korean Grammar Assistant] Error: ${message}`);
- }
- }
- /**
- * 성능 측정 시작
- */
- static startTimer(label) {
- const startTime = performance.now();
- return () => {
- const endTime = performance.now();
- const duration = Math.round(endTime - startTime);
- this.debug(`\u23F1\uFE0F ${label}: ${duration}ms`);
- return duration;
- };
- }
- /**
- * 비동기 함수의 성능을 측정합니다
- */
- static async measureAsync(label, fn) {
- const endTimer = this.startTimer(label);
- try {
- const result = await fn();
- endTimer();
- return result;
- } catch (error) {
- endTimer();
- this.error(`${label} \uC2E4\uD589 \uC911 \uC624\uB958:`, error);
- throw error;
- }
- }
- /**
- * 그룹 로깅 시작 (개발 모드에서만)
- */
- static group(label) {
- if (this.isDevelopment()) {
- console.group(`\u{1F538} ${label}`);
- }
- }
- /**
- * 그룹 로깅 종료 (개발 모드에서만)
- */
- static groupEnd() {
- if (this.isDevelopment()) {
- console.groupEnd();
- }
- }
- /**
- * 로그 히스토리를 반환합니다
- */
- static getHistory(level) {
- if (level) {
- return this.logHistory.filter((entry) => entry.level === level);
- }
- return [...this.logHistory];
- }
- /**
- * 로그 히스토리를 클리어합니다
- */
- static clearHistory() {
- this.logHistory = [];
- }
- /**
- * 로그 통계를 반환합니다
- */
- static getStats() {
- const stats = { DEBUG: 0, INFO: 0, WARN: 0, ERROR: 0, total: 0 };
- for (const entry of this.logHistory) {
- stats[entry.level]++;
- stats.total++;
- }
- return stats;
- }
- /**
- * 메모리 사용량 모니터링 (근사치)
- */
- static getMemoryUsage() {
- const historySize = this.logHistory.length;
- const estimatedBytes = this.logHistory.reduce((total, entry) => {
- return total + entry.message.length * 2 + // UTF-16 가정
- (entry.data ? JSON.stringify(entry.data).length * 2 : 0) + 32;
- }, 0);
- return { historySize, estimatedBytes };
- }
- /**
- * 히스토리 저장 활성화/비활성화
- */
- static setHistoryEnabled(enabled) {
- this.enableHistory = enabled;
- if (!enabled) {
- this.clearHistory();
- }
- }
- /**
- * 로그 레벨을 설정합니다 (성능 최적화)
- * @param level - 최소 출력 로그 레벨
- */
- static setLogLevel(level) {
- this.currentLogLevel = level;
- this.debug(`\uB85C\uADF8 \uB808\uBCA8\uC774 ${level}\uB85C \uC124\uC815\uB428`);
- }
- /**
- * 현재 로그 레벨을 반환합니다
- */
- static getLogLevel() {
- return this.currentLogLevel;
- }
- /**
- * 프로덕션 환경에 최적화된 설정을 적용합니다
- */
- static configureForProduction() {
- this.setLogLevel("WARN");
- this.setHistoryEnabled(false);
- this.debug("\uD504\uB85C\uB355\uC158 \uBAA8\uB4DC \uB85C\uAE45 \uC124\uC815 \uC801\uC6A9\uB428");
- }
- /**
- * 개발 환경에 최적화된 설정을 적용합니다
- */
- static configureForDevelopment() {
- this.setLogLevel("DEBUG");
- this.setHistoryEnabled(true);
- this.debug("\uAC1C\uBC1C \uBAA8\uB4DC \uB85C\uAE45 \uC124\uC815 \uC801\uC6A9\uB428");
- }
- /**
- * 실제 로그 작성 (히스토리 관리)
- */
- static writeLog(level, message, data) {
- if (!this.enableHistory || !this.shouldLog(level))
- return;
- const logEntry = {
- timestamp: new Date(),
- level,
- message,
- data
- };
- this.logHistory.push(logEntry);
- if (this.logHistory.length > this.maxLogHistory) {
- this.logHistory = this.logHistory.slice(-this.maxLogHistory);
- }
- }
- };
- Logger.logHistory = [];
- Logger.maxLogHistory = 1e3;
- Logger.enableHistory = true;
- Logger.currentLogLevel = "INFO";
- // 기본 로그 레벨
- // 로그 레벨 우선순위 (낮을수록 중요)
- Logger.logLevelPriority = {
- "DEBUG": 0,
- "INFO": 1,
- "WARN": 2,
- "ERROR": 3
- };
- }
-});
-
-// src/services/settings.ts
-function loadApiConfig() {
- try {
- if (typeof require !== "undefined") {
- const fs = require("fs");
- const path = require("path");
- const configPath = path.join(__dirname, "../../api-config.json");
- if (fs.existsSync(configPath)) {
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
- Logger.debug("\uB85C\uCEEC API \uC124\uC815 \uD30C\uC77C\uC744 \uB85C\uB4DC\uD588\uC2B5\uB2C8\uB2E4.");
- return config;
- }
- }
- } catch (error) {
- Logger.debug("\uB85C\uCEEC API \uC124\uC815 \uD30C\uC77C\uC744 \uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uAE30\uBCF8\uAC12\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.");
- }
- return {
- apiKey: "",
- // 사용자가 직접 입력해야 함
- apiHost: "bareun-api.junlim.org",
- apiPort: 443,
- ignoredWords: [],
- ai: DEFAULT_AI_SETTINGS,
- filterSingleCharErrors: true,
- // 기본적으로 한 글자 오류 필터링 활성화
- inlineMode: DEFAULT_INLINE_MODE_SETTINGS
- };
-}
-var DEFAULT_INLINE_MODE_SETTINGS, DEFAULT_SETTINGS, SettingsService;
-var init_settings = __esm({
- "src/services/settings.ts"() {
- init_aiModels();
- init_logger();
- DEFAULT_INLINE_MODE_SETTINGS = {
- enabled: false,
- underlineStyle: "wavy",
- underlineColor: "#ff0000",
- // 🎯 새로운 통합 툴팁 설정 (플랫폼별 자동 최적화)
- tooltipTrigger: "auto",
- // 기본값: 플랫폼에 따라 자동 선택
- // 🔧 레거시 설정 (하위 호환성, 추후 제거 예정)
- showTooltipOnHover: true,
- showTooltipOnClick: true
- };
- DEFAULT_SETTINGS = loadApiConfig();
- SettingsService = class {
- /**
- * 설정 유효성을 검사합니다.
- * @param settings 검사할 설정
- * @returns 유효성 검사 결과
- */
- static validateSettings(settings) {
- const errors = [];
- if (!settings.apiKey || settings.apiKey.trim() === "") {
- errors.push("API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- }
- if (!settings.apiHost || settings.apiHost.trim() === "") {
- errors.push("API \uD638\uC2A4\uD2B8\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- }
- if (!settings.apiPort || settings.apiPort <= 0 || settings.apiPort > 65535) {
- errors.push("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD3EC\uD2B8 \uBC88\uD638\uC785\uB2C8\uB2E4.");
- }
- return {
- isValid: errors.length === 0,
- errors
- };
- }
- /**
- * 설정을 기본값과 병합합니다.
- * @param userSettings 사용자 설정
- * @returns 병합된 설정
- */
- static mergeWithDefaults(userSettings) {
- const mergedSettings = Object.assign({}, DEFAULT_SETTINGS, userSettings);
- if (userSettings.ai) {
- mergedSettings.ai = Object.assign({}, DEFAULT_AI_SETTINGS, userSettings.ai);
- } else {
- mergedSettings.ai = Object.assign({}, DEFAULT_AI_SETTINGS);
- }
- if (userSettings.inlineMode) {
- mergedSettings.inlineMode = Object.assign({}, DEFAULT_INLINE_MODE_SETTINGS, userSettings.inlineMode);
- } else {
- mergedSettings.inlineMode = Object.assign({}, DEFAULT_INLINE_MODE_SETTINGS);
- }
- if (userSettings.filterSingleCharErrors === void 0) {
- mergedSettings.filterSingleCharErrors = DEFAULT_SETTINGS.filterSingleCharErrors;
- }
- return mergedSettings;
- }
- /**
- * API 엔드포인트 URL을 생성합니다.
- * @param settings 플러그인 설정
- * @returns API URL
- */
- static buildApiUrl(settings) {
- const protocol = settings.apiPort === 443 ? "https" : "http";
- const port = settings.apiPort === 443 || settings.apiPort === 80 ? "" : `:${settings.apiPort}`;
- return `${protocol}://${settings.apiHost}${port}/bareun/api/v1/correct-error`;
- }
- };
- }
-});
-
-// src/services/errorHandler.ts
-var errorHandler_exports = {};
-__export(errorHandler_exports, {
- ErrorHandlerService: () => ErrorHandlerService
-});
-var import_obsidian, ErrorHandlerService;
-var init_errorHandler = __esm({
- "src/services/errorHandler.ts"() {
- import_obsidian = require("obsidian");
- init_logger();
- ErrorHandlerService = class {
- /**
- * 에러를 분석하고 적절한 대응 방안을 제공합니다
- */
- static analyzeError(error, context) {
- var _a, _b, _c, _d, _e, _f, _g, _h;
- Logger.error("\uC5D0\uB7EC \uBD84\uC11D \uC2DC\uC791:", { error, context });
- if (error.name === "TypeError" && error.message.includes("fetch")) {
- return {
- type: "NETWORK_ERROR",
- message: "Network request failed",
- userMessage: "\uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694",
- suggestion: "\uB124\uD2B8\uC6CC\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
- retryable: true,
- details: error
- };
- }
- if (((_a = error.message) == null ? void 0 : _a.includes("API \uD0A4")) || ((_b = error.message) == null ? void 0 : _b.includes("api-key"))) {
- return {
- type: "API_KEY_ERROR",
- message: "Invalid API key",
- userMessage: "API \uD0A4\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
- suggestion: "\uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC5D0\uC11C \uC62C\uBC14\uB978 Bareun.ai API \uD0A4\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.",
- retryable: false,
- details: error
- };
- }
- if ((_c = error.message) == null ? void 0 : _c.includes("API \uC694\uCCAD \uC2E4\uD328")) {
- const statusMatch = error.message.match(/(\d{3})/);
- const status = statusMatch ? parseInt(statusMatch[1]) : 0;
- switch (Math.floor(status / 100)) {
- case 4:
- if (status === 401 || status === 403) {
- return {
- type: "API_KEY_ERROR",
- message: `Authentication failed: ${status}`,
- userMessage: "API \uD0A4 \uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
- suggestion: "API \uD0A4\uAC00 \uC62C\uBC14\uB978\uC9C0 \uD655\uC778\uD558\uACE0, \uB9CC\uB8CC\uB418\uC9C0 \uC54A\uC558\uB294\uC9C0 \uD655\uC778\uD574\uC8FC\uC138\uC694.",
- retryable: false,
- details: { status, error }
- };
- } else if (status === 429) {
- return {
- type: "API_RATE_LIMIT",
- message: "Rate limit exceeded",
- userMessage: "API \uC0AC\uC6A9\uB7C9 \uD55C\uB3C4\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4",
- suggestion: "\uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694. \uB108\uBB34 \uB9CE\uC740 \uC694\uCCAD\uC744 \uBCF4\uB0B4\uACE0 \uC788\uC2B5\uB2C8\uB2E4.",
- retryable: true,
- details: { status, error }
- };
- } else {
- return {
- type: "API_SERVER_ERROR",
- message: `Client error: ${status}`,
- userMessage: "\uC694\uCCAD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
- suggestion: "\uD14D\uC2A4\uD2B8 \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
- retryable: false,
- details: { status, error }
- };
- }
- case 5:
- return {
- type: "API_SERVER_ERROR",
- message: `Server error: ${status}`,
- userMessage: "\uC11C\uBC84\uC5D0 \uC77C\uC2DC\uC801\uC778 \uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
- suggestion: "\uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694. \uBB38\uC81C\uAC00 \uACC4\uC18D\uB418\uBA74 Bareun.ai \uC11C\uBE44\uC2A4 \uC0C1\uD0DC\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.",
- retryable: true,
- details: { status, error }
- };
- default:
- return {
- type: "API_SERVER_ERROR",
- message: `HTTP error: ${status}`,
- userMessage: "API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
- suggestion: "\uB124\uD2B8\uC6CC\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
- retryable: true,
- details: { status, error }
- };
- }
- }
- if (((_d = error.message) == null ? void 0 : _d.includes("\uD0C0\uC784\uC544\uC6C3")) || ((_e = error.message) == null ? void 0 : _e.includes("timeout"))) {
- return {
- type: "TIMEOUT_ERROR",
- message: "Request timeout",
- userMessage: "\uC694\uCCAD \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
- suggestion: "\uB124\uD2B8\uC6CC\uD06C\uAC00 \uB290\uB9AC\uAC70\uB098 \uD14D\uC2A4\uD2B8\uAC00 \uB108\uBB34 \uAE41\uB2C8\uB2E4. \uD14D\uC2A4\uD2B8\uB97C \uB098\uB204\uC5B4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
- retryable: true,
- details: error
- };
- }
- if (error.name === "SyntaxError" || ((_f = error.message) == null ? void 0 : _f.includes("JSON"))) {
- return {
- type: "PARSE_ERROR",
- message: "Failed to parse response",
- userMessage: "\uC11C\uBC84 \uC751\uB2F5\uC744 \uCC98\uB9AC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
- suggestion: "\uC11C\uBC84\uC5D0 \uC77C\uC2DC\uC801\uC778 \uBB38\uC81C\uAC00 \uC788\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
- retryable: true,
- details: error
- };
- }
- if (((_g = error.message) == null ? void 0 : _g.includes("\uC720\uD6A8")) || ((_h = error.message) == null ? void 0 : _h.includes("validation"))) {
- return {
- type: "VALIDATION_ERROR",
- message: "Validation failed",
- userMessage: "\uC785\uB825 \uB370\uC774\uD130\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
- suggestion: "\uD14D\uC2A4\uD2B8 \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
- retryable: false,
- details: error
- };
- }
- return {
- type: "UNKNOWN_ERROR",
- message: error.message || "Unknown error occurred",
- userMessage: "\uC608\uC0C1\uCE58 \uBABB\uD55C \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
- suggestion: "\uBB38\uC81C\uAC00 \uACC4\uC18D\uB418\uBA74 \uD50C\uB7EC\uADF8\uC778\uC744 \uB2E4\uC2DC \uB85C\uB4DC\uD558\uAC70\uB098 Obsidian\uC744 \uC7AC\uC2DC\uC791\uD574\uC8FC\uC138\uC694.",
- retryable: true,
- details: error
- };
- }
- /**
- * 에러를 처리하고 사용자에게 적절한 피드백을 제공합니다
- */
- static handleError(error, context) {
- const errorInfo = this.analyzeError(error, context);
- Logger.error("\uC5D0\uB7EC \uCC98\uB9AC:", {
- type: errorInfo.type,
- context,
- retryable: errorInfo.retryable
- });
- this.showUserNotification(errorInfo);
- return errorInfo;
- }
- /**
- * 자동 재시도가 가능한 함수를 실행합니다
- */
- static async withRetry(fn, context, config = {}) {
- const retryConfig = { ...this.DEFAULT_RETRY_CONFIG, ...config };
- const retryKey = `${context}_${Date.now()}`;
- let lastError;
- let attempt = 0;
- while (attempt <= retryConfig.maxRetries) {
- try {
- if (attempt > 0) {
- Logger.debug(`\uC7AC\uC2DC\uB3C4 ${attempt}/${retryConfig.maxRetries}:`, { context });
- }
- const result = await fn();
- if (attempt > 0) {
- Logger.debug("\uC7AC\uC2DC\uB3C4 \uC131\uACF5:", { context, attempt });
- }
- this.retryCount.delete(retryKey);
- return result;
- } catch (error) {
- lastError = error;
- attempt++;
- const errorInfo = this.analyzeError(error, context);
- if (!errorInfo.retryable) {
- Logger.error("\uC7AC\uC2DC\uB3C4 \uBD88\uAC00\uB2A5\uD55C \uC5D0\uB7EC:", { type: errorInfo.type, context });
- throw error;
- }
- if (attempt > retryConfig.maxRetries) {
- Logger.error("\uCD5C\uB300 \uC7AC\uC2DC\uB3C4 \uD69F\uC218 \uB3C4\uB2EC:", { context, attempt });
- break;
- }
- const delay = Math.min(
- retryConfig.baseDelay * Math.pow(retryConfig.backoffFactor, attempt - 1),
- retryConfig.maxDelay
- );
- Logger.warn(`\uC7AC\uC2DC\uB3C4 \uB300\uAE30 \uC911:`, { context, attempt, delay: `${delay}ms` });
- if (attempt === 1) {
- new import_obsidian.Notice(`\uC5F0\uACB0 \uBB38\uC81C\uB85C \uC7AC\uC2DC\uB3C4 \uC911... (${attempt}/${retryConfig.maxRetries})`, 2e3);
- }
- await this.sleep(delay);
- }
- }
- const finalErrorInfo = this.handleError(lastError, `${context} (\uC7AC\uC2DC\uB3C4 ${retryConfig.maxRetries}\uD68C \uC2E4\uD328)`);
- throw lastError;
- }
- /**
- * 사용자에게 친화적인 알림을 표시합니다
- */
- static showUserNotification(errorInfo) {
- const { type, userMessage, suggestion, retryable } = errorInfo;
- const icons = {
- NETWORK_ERROR: "\u{1F310}",
- API_KEY_ERROR: "\u{1F511}",
- API_RATE_LIMIT: "\u23F1\uFE0F",
- API_SERVER_ERROR: "\u{1F527}",
- TIMEOUT_ERROR: "\u23F0",
- PARSE_ERROR: "\u{1F4C4}",
- VALIDATION_ERROR: "\u26A0\uFE0F",
- UNKNOWN_ERROR: "\u2753"
- };
- const icon = icons[type] || "\u274C";
- const retryText = retryable ? " (\uC790\uB3D9 \uC7AC\uC2DC\uB3C4 \uC911)" : "";
- new import_obsidian.Notice(`${icon} ${userMessage}${retryText}`, 4e3);
- if (suggestion) {
- setTimeout(() => {
- new import_obsidian.Notice(`\u{1F4A1} ${suggestion}`, 6e3);
- }, 500);
- }
- }
- /**
- * 에러 통계를 반환합니다
- */
- static getErrorStats() {
- const stats = Logger.getHistory("ERROR");
- const errorStats = {
- NETWORK_ERROR: 0,
- API_KEY_ERROR: 0,
- API_RATE_LIMIT: 0,
- API_SERVER_ERROR: 0,
- TIMEOUT_ERROR: 0,
- PARSE_ERROR: 0,
- VALIDATION_ERROR: 0,
- UNKNOWN_ERROR: 0
- };
- for (const log of stats) {
- if (log.message.includes("\uB124\uD2B8\uC6CC\uD06C") || log.message.includes("fetch")) {
- errorStats.NETWORK_ERROR++;
- } else if (log.message.includes("API \uD0A4") || log.message.includes("\uD0A4")) {
- errorStats.API_KEY_ERROR++;
- } else if (log.message.includes("rate limit") || log.message.includes("429")) {
- errorStats.API_RATE_LIMIT++;
- } else if (log.message.includes("\uD0C0\uC784\uC544\uC6C3") || log.message.includes("timeout")) {
- errorStats.TIMEOUT_ERROR++;
- } else if (log.message.includes("JSON") || log.message.includes("parse")) {
- errorStats.PARSE_ERROR++;
- } else if (log.message.includes("API") || log.message.includes("\uC11C\uBC84")) {
- errorStats.API_SERVER_ERROR++;
- } else {
- errorStats.UNKNOWN_ERROR++;
- }
- }
- return errorStats;
- }
- /**
- * 비동기 대기 헬퍼
- */
- static sleep(ms) {
- return new Promise((resolve) => setTimeout(resolve, ms));
- }
- /**
- * 재시도 상태 초기화
- */
- static clearRetryState() {
- this.retryCount.clear();
- Logger.debug("\uC7AC\uC2DC\uB3C4 \uC0C1\uD0DC \uCD08\uAE30\uD654\uB428");
- }
- };
- ErrorHandlerService.DEFAULT_RETRY_CONFIG = {
- maxRetries: 3,
- baseDelay: 1e3,
- maxDelay: 1e4,
- backoffFactor: 2
- };
- ErrorHandlerService.retryCount = /* @__PURE__ */ new Map();
- }
-});
-
-// src/utils/tokenEstimator.ts
-function estimateTokenCount(text) {
- if (!text)
- return 0;
- const koreanChars = (text.match(/[\u3131-\u3163\uac00-\ud7a3]/g) || []).length;
- const englishChars = (text.match(/[a-zA-Z]/g) || []).length;
- const otherChars = text.length - koreanChars - englishChars;
- const estimatedTokens = Math.ceil(
- koreanChars * 1.8 + // 한국어 문자
- englishChars * 0.25 + // 영어 문자 (4글자당 1토큰)
- otherChars * 0.5
- // 기타 문자 (공백, 구두점 등)
- );
- return estimatedTokens;
-}
-function estimateAnalysisTokenUsage(correctionContexts) {
- const systemPromptTokens = 150;
- let userPromptTokens = 50;
- userPromptTokens += correctionContexts.length * 20;
- correctionContexts.forEach((ctx) => {
- userPromptTokens += estimateTokenCount(ctx.fullContext);
- userPromptTokens += estimateTokenCount(ctx.original);
- userPromptTokens += estimateTokenCount(ctx.corrected.join(", "));
- userPromptTokens += estimateTokenCount(ctx.help);
- });
- const inputTokens = systemPromptTokens + userPromptTokens;
- const estimatedOutputTokens = correctionContexts.length * 75;
- return {
- inputTokens,
- estimatedOutputTokens,
- totalEstimated: inputTokens + estimatedOutputTokens
- };
-}
-function estimateCost(tokens, provider) {
- const costs = {
- openai: {
- "gpt-4o": { input: 2.5, output: 10 },
- // per 1M tokens (USD)
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
- "gpt-4-turbo": { input: 10, output: 30 },
- "gpt-4": { input: 30, output: 60 }
- },
- anthropic: {
- "claude-3-5-sonnet-20241022": { input: 3, output: 15 },
- "claude-3-5-haiku-20241022": { input: 0.25, output: 1.25 }
- },
- google: {
- "gemini-1.5-pro": { input: 1.25, output: 5 },
- "gemini-1.5-flash": { input: 0.075, output: 0.3 }
- }
- };
- const avgCostPer1M = 2;
- const estimatedCostUSD = tokens / 1e6 * avgCostPer1M;
- const exchangeRate = 1350;
- const estimatedCostKRW = estimatedCostUSD * exchangeRate;
- if (estimatedCostUSD < 1e-3) {
- return "< $0.001 (< \u20A91)";
- } else if (estimatedCostUSD < 0.01) {
- return `~$${estimatedCostUSD.toFixed(4)} (~\u20A9${estimatedCostKRW.toFixed(0)})`;
- } else {
- return `~$${estimatedCostUSD.toFixed(3)} (~\u20A9${estimatedCostKRW.toFixed(0)})`;
- }
-}
-var init_tokenEstimator = __esm({
- "src/utils/tokenEstimator.ts"() {
- }
-});
-
-// src/api/openai-client.ts
-var import_obsidian5, OpenAIClient;
-var init_openai_client = __esm({
- "src/api/openai-client.ts"() {
- import_obsidian5 = require("obsidian");
- init_aiModels();
- init_logger();
- OpenAIClient = class {
- constructor(apiKey) {
- this.apiKey = apiKey;
- }
- async fetchModels() {
- if (!this.apiKey) {
- return [];
- }
- try {
- const response = await (0, import_obsidian5.requestUrl)({
- url: API_ENDPOINTS.openai.models,
- method: "GET",
- headers: {
- "Authorization": `Bearer ${this.apiKey}`,
- "Content-Type": "application/json"
- }
- });
- if (response.status === 200) {
- const models = response.json.data.map((model) => model.id).filter(
- (id) => MODEL_PREFIXES.openai.some((prefix) => id.startsWith(prefix))
- ).sort();
- Logger.debug(`${models.length}\uAC1C \uBAA8\uB378\uC744 \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.`);
- return models;
- }
- } catch (error) {
- Logger.error("\uBAA8\uB378 \uBAA9\uB85D \uAC00\uC838\uC624\uAE30 \uC2E4\uD328:", error);
- }
- return [];
- }
- async chat(messages, maxTokens, model) {
- if (!this.apiKey) {
- throw new Error("OpenAI API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- }
- if (!this.apiKey.startsWith("sk-")) {
- throw new Error('OpenAI API \uD0A4 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. "sk-"\uB85C \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.');
- }
- Logger.debug("chat \uC694\uCCAD \uC2DC\uC791:", {
- model,
- maxTokens,
- messagesCount: messages.length,
- apiKeyPrefix: this.apiKey.substring(0, 7) + "..."
- });
- const requestBody = {
- model,
- messages,
- max_tokens: maxTokens,
- temperature: 0.1
- };
- Logger.debug("\uC694\uCCAD \uB370\uC774\uD130:", {
- url: API_ENDPOINTS.openai.chat,
- model,
- messagesCount: messages.length,
- maxTokens,
- bodySize: JSON.stringify(requestBody).length
- });
- let response;
- try {
- response = await (0, import_obsidian5.requestUrl)({
- url: API_ENDPOINTS.openai.chat,
- method: "POST",
- headers: {
- "Authorization": `Bearer ${this.apiKey}`,
- "Content-Type": "application/json"
- },
- body: JSON.stringify(requestBody)
- });
- } catch (error) {
- Logger.error("requestUrl \uC624\uB958:", error);
- Logger.error("\uC694\uCCAD \uC815\uBCF4:", {
- url: API_ENDPOINTS.openai.chat,
- model,
- hasApiKey: !!this.apiKey,
- bodySize: JSON.stringify(requestBody).length
- });
- throw error;
- }
- if (response.status === 200) {
- return response.json.choices[0].message.content.trim();
- } else {
- Logger.error("API \uC751\uB2F5 \uC624\uB958:", {
- status: response.status,
- text: response.text,
- json: response.json
- });
- throw new Error(`OpenAI API \uC624\uB958: ${response.status} - ${response.text || JSON.stringify(response.json)}`);
- }
- }
- };
- }
-});
-
-// src/api/anthropic-client.ts
-var import_obsidian6, AnthropicClient;
-var init_anthropic_client = __esm({
- "src/api/anthropic-client.ts"() {
- import_obsidian6 = require("obsidian");
- init_aiModels();
- init_logger();
- AnthropicClient = class {
- constructor(apiKey) {
- this.apiKey = apiKey;
- }
- async chat(messages, maxTokens, model) {
- if (!this.apiKey) {
- throw new Error("Anthropic API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- }
- const systemMessage = messages.find((m) => m.role === "system");
- const userMessages = messages.filter((m) => m.role !== "system");
- const response = await (0, import_obsidian6.requestUrl)({
- url: API_ENDPOINTS.anthropic.messages,
- method: "POST",
- headers: {
- "x-api-key": this.apiKey,
- "Content-Type": "application/json",
- "anthropic-version": "2023-06-01"
- },
- body: JSON.stringify({
- model,
- max_tokens: maxTokens,
- temperature: 0.1,
- system: (systemMessage == null ? void 0 : systemMessage.content) || "",
- messages: userMessages
- })
- });
- if (response.status === 200) {
- return response.json.content[0].text.trim();
- } else {
- Logger.error("[Anthropic] API \uC751\uB2F5 \uC624\uB958:", {
- status: response.status,
- text: response.text,
- json: response.json
- });
- throw new Error(`Anthropic API \uC624\uB958: ${response.status} - ${response.text || JSON.stringify(response.json)}`);
- }
- }
- };
- }
-});
-
-// src/api/google-client.ts
-var import_obsidian7, GoogleClient;
-var init_google_client = __esm({
- "src/api/google-client.ts"() {
- import_obsidian7 = require("obsidian");
- init_aiModels();
- init_logger();
- GoogleClient = class {
- constructor(apiKey) {
- this.apiKey = apiKey;
- }
- async chat(messages, maxTokens, model) {
- var _a, _b, _c, _d;
- if (!this.apiKey) {
- throw new Error("Google API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- }
- const systemMessage = messages.find((m) => m.role === "system");
- const userMessages = messages.filter((m) => m.role !== "system");
- if (systemMessage && userMessages.length > 0) {
- userMessages[0].content = `${systemMessage.content}
-
-${userMessages[0].content}`;
- }
- const contents = userMessages.map((message) => ({
- role: message.role === "assistant" ? "model" : "user",
- parts: [{ text: message.content }]
- }));
- const response = await (0, import_obsidian7.requestUrl)({
- url: `${API_ENDPOINTS.google.generateContent(model)}?key=${this.apiKey}`,
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- contents,
- generationConfig: {
- maxOutputTokens: maxTokens,
- temperature: 0.1
- }
- })
- });
- if (response.status === 200) {
- const candidate = (_a = response.json.candidates) == null ? void 0 : _a[0];
- if ((_d = (_c = (_b = candidate == null ? void 0 : candidate.content) == null ? void 0 : _b.parts) == null ? void 0 : _c[0]) == null ? void 0 : _d.text) {
- return candidate.content.parts[0].text.trim();
- }
- Logger.error("[Google] API \uC751\uB2F5 \uD615\uC2DD \uC624\uB958:", response.json);
- throw new Error("Google API \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
- } else {
- Logger.error("[Google] API \uC751\uB2F5 \uC624\uB958:", {
- status: response.status,
- text: response.text,
- json: response.json
- });
- throw new Error(`Google API \uC624\uB958: ${response.status} - ${response.text || JSON.stringify(response.json)}`);
- }
- }
- };
- }
-});
-
-// src/api/ollama-client.ts
-var import_obsidian8, OllamaClient;
-var init_ollama_client = __esm({
- "src/api/ollama-client.ts"() {
- import_obsidian8 = require("obsidian");
- OllamaClient = class {
- constructor(endpoint) {
- this.endpoint = endpoint;
- }
- async chat(messages, maxTokens, model) {
- if (!this.endpoint) {
- throw new Error("Ollama \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- }
- let prompt = "";
- messages.forEach((message) => {
- if (message.role === "system") {
- prompt += `System: ${message.content}
-
-`;
- } else if (message.role === "user") {
- prompt += `Human: ${message.content}
-
-`;
- } else if (message.role === "assistant") {
- prompt += `Assistant: ${message.content}
-
-`;
- }
- });
- prompt += "Assistant: ";
- const response = await (0, import_obsidian8.requestUrl)({
- url: `${this.endpoint}/api/generate`,
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- model,
- prompt,
- options: {
- num_predict: maxTokens,
- temperature: 0.1
- },
- stream: false
- })
- });
- if (response.status === 200) {
- return response.json.response.trim();
- } else {
- throw new Error(`Ollama API \uC624\uB958: ${response.status} - ${response.text}`);
- }
- }
- };
- }
-});
-
-// src/api/clientFactory.ts
-var clientFactory_exports = {};
-__export(clientFactory_exports, {
- AIClientFactory: () => AIClientFactory
-});
-var AIClientFactory;
-var init_clientFactory = __esm({
- "src/api/clientFactory.ts"() {
- init_openai_client();
- init_anthropic_client();
- init_google_client();
- init_ollama_client();
- init_logger();
- AIClientFactory = class {
- static createClient(settings) {
- const provider = settings.provider;
- const apiKey = this.getApiKey(settings);
- Logger.debug("\uD074\uB77C\uC774\uC5B8\uD2B8 \uC0DD\uC131:", {
- provider,
- hasApiKey: !!apiKey,
- apiKeyLength: apiKey ? apiKey.length : 0,
- model: settings.model
- });
- switch (provider) {
- case "openai":
- return new OpenAIClient(settings.openaiApiKey);
- case "anthropic":
- return new AnthropicClient(settings.anthropicApiKey);
- case "google":
- return new GoogleClient(settings.googleApiKey);
- case "ollama":
- return new OllamaClient(settings.ollamaEndpoint);
- default:
- throw new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 AI \uC81C\uACF5\uC790\uC785\uB2C8\uB2E4: ${provider}`);
- }
- }
- static async fetchModels(settings) {
- const client = this.createClient(settings);
- if (settings.provider === "ollama" || settings.provider === "anthropic") {
- return [];
- }
- if (!client.fetchModels) {
- return [];
- }
- try {
- return await client.fetchModels();
- } catch (error) {
- Logger.error(`${settings.provider} \uBAA8\uB378 \uBAA9\uB85D \uAC00\uC838\uC624\uAE30 \uC2E4\uD328:`, error);
- return [];
- }
- }
- static getApiKey(settings) {
- switch (settings.provider) {
- case "openai":
- return settings.openaiApiKey;
- case "anthropic":
- return settings.anthropicApiKey;
- case "google":
- return settings.googleApiKey;
- case "ollama":
- return "";
- default:
- return "";
- }
- }
- static hasValidApiKey(settings) {
- if (settings.provider === "ollama") {
- return !!settings.ollamaEndpoint;
- }
- return !!this.getApiKey(settings);
- }
- };
- }
-});
-
-// src/services/aiAnalysisService.ts
-var aiAnalysisService_exports = {};
-__export(aiAnalysisService_exports, {
- AIAnalysisService: () => AIAnalysisService
-});
-var AIAnalysisService;
-var init_aiAnalysisService = __esm({
- "src/services/aiAnalysisService.ts"() {
- init_aiModels();
- init_tokenEstimator();
- init_logger();
- AIAnalysisService = class {
- constructor(settings) {
- this.settings = settings;
- }
- /**
- * AI 클라이언트 팩토리를 지연 로딩합니다 (성능 최적화)
- * @private
- */
- async getClientFactory() {
- const { AIClientFactory: AIClientFactory2 } = await Promise.resolve().then(() => (init_clientFactory(), clientFactory_exports));
- return AIClientFactory2;
- }
- /**
- * API 키 유효성을 확인합니다 (lazy loading 팩토리 사용)
- * @private
- */
- async hasValidApiKey(settings) {
- const ClientFactory = await this.getClientFactory();
- return ClientFactory.hasValidApiKey(settings);
- }
- /**
- * 각 오류에 대한 컨텍스트를 추출합니다.
- */
- extractCorrectionContexts(request, morphemeInfo) {
- const { originalText, corrections, contextWindow = 50, currentStates, editor, file, enhancedContext = true } = request;
- const contexts = [];
- corrections.forEach((correction, index) => {
- const errorIndex = originalText.indexOf(correction.original);
- if (errorIndex === -1) {
- Logger.warn(`\uC624\uB958 \uD14D\uC2A4\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C: "${correction.original}"`);
- contexts.push({
- correctionIndex: index,
- original: correction.original,
- corrected: correction.corrected,
- help: correction.help,
- contextBefore: "",
- contextAfter: "",
- fullContext: correction.original
- });
- return;
- }
- const startIndex = Math.max(0, errorIndex - contextWindow);
- const endIndex = Math.min(originalText.length, errorIndex + correction.original.length + contextWindow);
- const contextBefore = originalText.slice(startIndex, errorIndex);
- const contextAfter = originalText.slice(errorIndex + correction.original.length, endIndex);
- const fullContext = originalText.slice(startIndex, endIndex);
- const stateInfo = currentStates ? currentStates[index] : void 0;
- const context = {
- correctionIndex: index,
- original: correction.original,
- corrected: correction.corrected,
- help: correction.help,
- contextBefore: contextBefore.trim(),
- contextAfter: contextAfter.trim(),
- fullContext: fullContext.trim(),
- currentState: stateInfo == null ? void 0 : stateInfo.state,
- currentValue: stateInfo == null ? void 0 : stateInfo.value
- };
- if (enhancedContext && editor) {
- try {
- const enhancedInfo = this.extractEnhancedContext(
- editor,
- file,
- originalText,
- correction,
- errorIndex,
- morphemeInfo
- );
- if (enhancedInfo.isLikelyProperNoun) {
- context.sentenceContext = enhancedInfo.sentenceContext;
- context.isLikelyProperNoun = true;
- context.documentType = enhancedInfo.documentType;
- Logger.debug(`\u{1F50D} \uACE0\uC720\uBA85\uC0AC \uAC10\uC9C0: "${correction.original}" - \uAC10\uC9C0 \uBC29\uBC95: ${enhancedInfo.detectionMethod} - \uBB38\uC7A5 \uCEE8\uD14D\uC2A4\uD2B8 \uCD94\uAC00`);
- }
- } catch (error) {
- Logger.warn("\uD5A5\uC0C1\uB41C \uCEE8\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uC2E4\uD328:", error);
- }
- }
- contexts.push(context);
- });
- return contexts;
- }
- /**
- * 형태소 분석 결과 전체를 로깅합니다.
- */
- logMorphemeAnalysis(morphemeInfo, corrections) {
- Logger.debug("\u{1F4CB} \uD615\uD0DC\uC18C \uBD84\uC11D \uACB0\uACFC \uC694\uC57D:");
- if (!morphemeInfo || !morphemeInfo.sentences) {
- Logger.warn("\uD615\uD0DC\uC18C \uBD84\uC11D \uB370\uC774\uD130\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC74C");
- return;
- }
- const totalSentences = morphemeInfo.sentences.length;
- const totalTokens = morphemeInfo.sentences.reduce((sum, sentence) => sum + (sentence.tokens ? sentence.tokens.length : 0), 0);
- Logger.debug(` \uCD1D ${totalSentences}\uAC1C \uBB38\uC7A5, ${totalTokens}\uAC1C \uD1A0\uD070 \uBD84\uC11D\uB428`);
- const properNouns = [];
- const foreignWords = [];
- const allTokens = [];
- morphemeInfo.sentences.forEach((sentence, sentenceIdx) => {
- if (!sentence.tokens)
- return;
- sentence.tokens.forEach((token) => {
- var _a, _b;
- const tokenText = ((_a = token.text) == null ? void 0 : _a.content) || "";
- const tags = ((_b = token.morphemes) == null ? void 0 : _b.map((m) => m.tag)) || [];
- allTokens.push({ text: tokenText, tags });
- if (tags.some((tag) => ["NNP"].includes(tag))) {
- if (!properNouns.includes(tokenText)) {
- properNouns.push(tokenText);
- }
- }
- if (tags.some((tag) => ["SL", "SH", "SN"].includes(tag))) {
- if (!foreignWords.includes(tokenText)) {
- foreignWords.push(tokenText);
- }
- }
- });
- });
- if (properNouns.length > 0) {
- Logger.debug(` \u{1F3F7}\uFE0F \uACE0\uC720\uBA85\uC0AC (NNP): [${properNouns.map((noun) => `"${noun}"`).join(", ")}]`);
- }
- if (foreignWords.length > 0) {
- Logger.debug(` \u{1F310} \uC678\uAD6D\uC5B4/\uD2B9\uC218\uC5B4 (SL/SH/SN): [${foreignWords.map((word) => `"${word}"`).join(", ")}]`);
- }
- const errorWords = corrections.map((c) => c.original);
- const matchedErrors = errorWords.filter(
- (word) => properNouns.includes(word) || foreignWords.includes(word)
- );
- if (matchedErrors.length > 0) {
- Logger.debug(` \u2728 \uB9DE\uCDA4\uBC95 \uC624\uB958 \uC911 \uD615\uD0DC\uC18C \uBD84\uC11D\uC73C\uB85C \uAC10\uC9C0\uB41C \uACE0\uC720\uBA85\uC0AC/\uC678\uAD6D\uC5B4: [${matchedErrors.map((word) => `"${word}"`).join(", ")}]`);
- } else {
- Logger.debug(" \u2753 \uB9DE\uCDA4\uBC95 \uC624\uB958 \uC911 \uD615\uD0DC\uC18C \uBD84\uC11D\uC73C\uB85C \uACE0\uC720\uBA85\uC0AC/\uC678\uAD6D\uC5B4\uB85C \uBD84\uB958\uB41C \uB2E8\uC5B4 \uC5C6\uC74C");
- }
- Logger.debug("\uC0C1\uC138 \uD1A0\uD070 \uC815\uBCF4 (\uCC98\uC74C 10\uAC1C):");
- allTokens.slice(0, 10).forEach((token, idx) => {
- Logger.debug(` ${idx + 1}. "${token.text}" \u2192 [${token.tags.join(", ")}]`);
- });
- if (allTokens.length > 10) {
- Logger.debug(` ... \uBC0F ${allTokens.length - 10}\uAC1C \uD1A0\uD070 \uB354 \uC788\uC74C`);
- }
- }
- /**
- * 형태소 분석 결과에서 고유명사를 감지합니다.
- */
- isProperNounFromMorphemes(text, morphemeInfo) {
- if (!morphemeInfo || !morphemeInfo.sentences)
- return false;
- for (const sentence of morphemeInfo.sentences) {
- if (!sentence.tokens)
- continue;
- for (const token of sentence.tokens) {
- if (!token.text || !token.morphemes)
- continue;
- if (token.text.content === text) {
- for (const morpheme of token.morphemes) {
- const tag = morpheme.tag;
- if (["NNP", "SL", "SH", "SN"].includes(tag)) {
- const tagDescriptions = {
- "NNP": "\uACE0\uC720\uBA85\uC0AC",
- "SL": "\uC678\uAD6D\uC5B4",
- "SH": "\uD55C\uC790",
- "SN": "\uC22B\uC790"
- };
- const tagDescription = tagDescriptions[tag] || tag;
- Logger.debug(`\u{1F3F7}\uFE0F \uD615\uD0DC\uC18C \uACE0\uC720\uBA85\uC0AC \uAC10\uC9C0: "${text}" - \uD488\uC0AC: ${tag}(${tagDescription})`);
- return true;
- }
- }
- }
- }
- }
- return false;
- }
- /**
- * Obsidian Editor를 활용한 향상된 컨텍스트 추출
- */
- extractEnhancedContext(editor, file, originalText, correction, errorIndex, morphemeInfo) {
- const errorPosition = editor.offsetToPos(errorIndex);
- const sentenceContext = this.extractCurrentSentence(editor, errorPosition);
- const documentType = typeof file === "object" && file !== null && "extension" in file ? file.extension : "unknown";
- let isLikelyProperNoun = false;
- let detectionMethod = "";
- if (morphemeInfo) {
- const morphemeDetected = this.isProperNounFromMorphemes(correction.original, morphemeInfo);
- const patternDetected = this.detectProperNounPatterns(correction.original, sentenceContext);
- if (morphemeDetected) {
- isLikelyProperNoun = true;
- detectionMethod = "\uD615\uD0DC\uC18C \uBD84\uC11D";
- } else if (patternDetected) {
- isLikelyProperNoun = true;
- detectionMethod = "\uD328\uD134 \uB9E4\uCE6D";
- }
- } else {
- const patternDetected = this.detectProperNounPatterns(correction.original, sentenceContext);
- if (patternDetected) {
- isLikelyProperNoun = true;
- detectionMethod = "\uD328\uD134 \uB9E4\uCE6D (\uD615\uD0DC\uC18C \uBD84\uC11D \uC5C6\uC74C)";
- }
- }
- return {
- sentenceContext,
- isLikelyProperNoun,
- documentType,
- detectionMethod
- };
- }
- /**
- * 현재 문장을 추출합니다.
- */
- extractCurrentSentence(editor, position) {
- const currentLine = editor.getLine(position.line);
- const sentenceEndPattern = /[.!?。!?]/g;
- let sentenceStart = 0;
- for (let i = position.ch - 1; i >= 0; i--) {
- if (sentenceEndPattern.test(currentLine[i])) {
- sentenceStart = i + 1;
- break;
- }
- }
- let sentenceEnd = currentLine.length;
- for (let i = position.ch; i < currentLine.length; i++) {
- if (sentenceEndPattern.test(currentLine[i])) {
- sentenceEnd = i + 1;
- break;
- }
- }
- return currentLine.slice(sentenceStart, sentenceEnd).trim();
- }
- /**
- * 패턴 기반 고유명사 감지 (형태소 분석이 없을 때 폴백)
- */
- detectProperNounPatterns(text, sentenceContext) {
- const patterns = [
- { pattern: /^[A-Z][a-z]+/, name: "\uC601\uC5B4 \uACE0\uC720\uBA85\uC0AC" },
- // GitHub, React 등
- { pattern: /^[A-Z]{2,}$/, name: "\uC601\uC5B4 \uC57D\uC5B4" },
- // API, URL, HTTP
- { pattern: /\w+님$/, name: "\uC874\uCE6D" },
- // 김철수님
- { pattern: /^[가-힣]{2,4}[시도군구]$/, name: "\uC9C0\uBA85" },
- // 서울시, 부산광역시
- { pattern: /\d{4}년/, name: "\uC5F0\uB3C4" },
- // 2018년
- { pattern: /^[가-힣]+\.(js|ts|py|css|html|md)$/, name: "\uD30C\uC77C\uBA85" }
- // 파일명
- ];
- for (const { pattern, name } of patterns) {
- if (pattern.test(text)) {
- Logger.debug(`\u{1F50D} \uD328\uD134 \uACE0\uC720\uBA85\uC0AC \uAC10\uC9C0: "${text}" - \uD328\uD134: ${name}`);
- return true;
- }
- }
- return false;
- }
- /**
- * AI 분석에 필요한 토큰 사용량을 추정합니다.
- */
- estimateTokenUsage(request) {
- const correctionContexts = this.extractCorrectionContexts(request);
- const tokenUsage = estimateAnalysisTokenUsage(correctionContexts);
- const cost = estimateCost(tokenUsage.totalEstimated, this.settings.provider);
- return {
- ...tokenUsage,
- estimatedCost: cost
- };
- }
- /**
- * 최적의 배치 크기를 계산합니다.
- * ⭐ JSON 잘림 방지를 위해 보수적 배치 크기 적용
- */
- calculateOptimalBatchSize(correctionContexts, hasMorphemeInfo = false) {
- if (correctionContexts.length === 0)
- return 5;
- const avgContextLength = correctionContexts.reduce((sum, ctx) => sum + ctx.fullContext.length, 0) / correctionContexts.length;
- const systemPromptLength = AI_PROMPTS.analysisSystem.length;
- const maxInputTokens = this.getModelMaxInputTokens(this.settings.model);
- let optimalSize = 6;
- if (avgContextLength < 50) {
- optimalSize = 8;
- } else if (avgContextLength < 100) {
- optimalSize = 6;
- } else if (avgContextLength < 200) {
- optimalSize = 4;
- } else {
- optimalSize = 3;
- }
- if (hasMorphemeInfo) {
- optimalSize = Math.max(3, optimalSize - 1);
- }
- Logger.debug(`JSON \uC798\uB9BC \uBC29\uC9C0 \uBC30\uCE58 \uD06C\uAE30: \uD3C9\uADE0 \uCEE8\uD14D\uC2A4\uD2B8 ${avgContextLength}\uC790, \uD615\uD0DC\uC18C: ${hasMorphemeInfo} \u2192 ${optimalSize}\uAC1C\uC529 \uCC98\uB9AC`);
- return Math.min(optimalSize, 8);
- }
- /**
- * 모델별 최대 입력 토큰을 가져옵니다 (대략적).
- */
- getModelMaxInputTokens(model) {
- const outputLimit = this.getModelMaxTokens(model);
- return outputLimit * 10;
- }
- /**
- * 오류들을 배치로 나누어 처리합니다.
- */
- createBatches(correctionContexts, maxBatchSize = 10) {
- const batches = [];
- for (let i = 0; i < correctionContexts.length; i += maxBatchSize) {
- batches.push(correctionContexts.slice(i, i + maxBatchSize));
- }
- return batches;
- }
- /**
- * 단일 배치를 처리합니다.
- */
- async processBatch(batch, batchIndex, totalBatches, client, adjustedMaxTokens, model, morphemeInfo) {
- Logger.debug(`\uBC30\uCE58 ${batchIndex + 1}/${totalBatches} \uCC98\uB9AC \uC911 (${batch.length}\uAC1C \uC624\uB958)`);
- const systemPrompt = AI_PROMPTS.analysisSystem;
- const userPrompt = morphemeInfo ? AI_PROMPTS.analysisUserWithMorphemes(batch, morphemeInfo) : AI_PROMPTS.analysisUserWithContext(batch);
- if (morphemeInfo) {
- Logger.debug(`\uD615\uD0DC\uC18C \uC815\uBCF4\uC640 \uD568\uAED8 AI \uBD84\uC11D \uC9C4\uD589 (\uD1A0\uD070 \uC808\uC57D \uBAA8\uB4DC)`);
- const totalTokens = morphemeInfo.sentences.reduce((sum, s) => {
- var _a;
- return sum + (((_a = s.tokens) == null ? void 0 : _a.length) || 0);
- }, 0);
- Logger.debug(`\uD615\uD0DC\uC18C \uD1A0\uD070 \uC218: ${totalTokens}\uAC1C`);
- }
- const messages = [
- { role: "system", content: systemPrompt },
- { role: "user", content: userPrompt }
- ];
- const response = await client.chat(messages, adjustedMaxTokens, model);
- Logger.debug(`\uBC30\uCE58 ${batchIndex + 1} \uC751\uB2F5 \uC218\uC2E0:`, response.substring(0, 100) + "...");
- return this.parseAIResponse(response, batch);
- }
- /**
- * AI를 사용하여 맞춤법 오류를 분석하고 최적의 수정사항을 제안합니다.
- * ⭐ NEW: 형태소 정보 통합 지원
- */
- async analyzeCorrections(request, morphemeInfo) {
- var _a;
- Logger.debug("analyzeCorrections \uC2DC\uC791:", {
- enabled: this.settings.enabled,
- provider: this.settings.provider,
- model: this.settings.model,
- correctionsCount: request.corrections.length
- });
- if (morphemeInfo) {
- this.logMorphemeAnalysis(morphemeInfo, request.corrections);
- }
- if (!this.settings.enabled) {
- throw new Error("AI \uAE30\uB2A5\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
- }
- if (!await this.hasValidApiKey(this.settings)) {
- const provider = this.settings.provider;
- const keyName = provider === "openai" ? "OpenAI API \uD0A4" : provider === "anthropic" ? "Anthropic API \uD0A4" : provider === "google" ? "Google API \uD0A4" : provider === "ollama" ? "Ollama \uC5D4\uB4DC\uD3EC\uC778\uD2B8" : "API \uD0A4";
- throw new Error(`${keyName}\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC5D0\uC11C ${provider} \uC81C\uACF5\uC790\uC758 ${keyName}\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.`);
- }
- const ClientFactory = await this.getClientFactory();
- if (!this.settings.model || this.settings.model.trim() === "") {
- throw new Error(`\uBAA8\uB378\uC774 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC5D0\uC11C ${this.settings.provider} \uBAA8\uB378\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694.`);
- }
- const client = ClientFactory.createClient(this.settings);
- try {
- const allContexts = this.extractCorrectionContexts(request, morphemeInfo);
- const contextsToAnalyze = allContexts.filter(
- (ctx) => ctx.currentState !== "original-kept" && ctx.currentState !== "exception-processed"
- );
- const alreadyResolvedContexts = allContexts.filter(
- (ctx) => ctx.currentState === "original-kept" || ctx.currentState === "exception-processed"
- );
- Logger.debug(`\uBD84\uC11D \uB300\uC0C1: ${contextsToAnalyze.length}\uAC1C, \uC774\uBBF8 \uCC98\uB9AC\uB428: ${alreadyResolvedContexts.length}\uAC1C`);
- let aiResults = [];
- if (contextsToAnalyze.length > 0) {
- const maxBatchSize = this.calculateOptimalBatchSize(contextsToAnalyze, !!morphemeInfo);
- Logger.debug("\uBD84\uC11D \uC694\uCCAD \uC804\uC1A1 \uC911...", {
- provider: this.settings.provider,
- model: this.settings.model,
- totalCorrections: contextsToAnalyze.length,
- batchSize: maxBatchSize,
- estimatedBatches: Math.ceil(contextsToAnalyze.length / maxBatchSize),
- contextWindow: request.contextWindow || 50,
- maxTokens: this.settings.maxTokens,
- apiKeySet: !!this.getApiKey()
- });
- const batches = this.createBatches(contextsToAnalyze, maxBatchSize);
- Logger.debug(`${batches.length}\uAC1C \uBC30\uCE58\uB85C \uBD84\uD560\uD558\uC5EC \uCC98\uB9AC\uD569\uB2C8\uB2E4.`);
- const adjustedMaxTokens = this.adjustTokensForModel(this.settings.maxTokens, this.settings.model);
- if (morphemeInfo) {
- const totalTokens = morphemeInfo.sentences.reduce((sum, s) => {
- var _a2;
- return sum + (((_a2 = s.tokens) == null ? void 0 : _a2.length) || 0);
- }, 0);
- Logger.debug("\uD615\uD0DC\uC18C \uC815\uBCF4 \uD65C\uC6A9 AI \uBD84\uC11D \uC2DC\uC791:", {
- tokensCount: totalTokens,
- sentences: ((_a = morphemeInfo.sentences) == null ? void 0 : _a.length) || 0
- });
- }
- for (let i = 0; i < batches.length; i++) {
- try {
- if (request.onProgress) {
- const progressMsg = morphemeInfo ? `AI + \uD615\uD0DC\uC18C \uBD84\uC11D \uC911... (${Math.round((i + 1) / batches.length * 100)}%)` : `AI \uBD84\uC11D \uC911... (${Math.round((i + 1) / batches.length * 100)}%)`;
- request.onProgress(i + 1, batches.length, progressMsg);
- }
- const batchResults = await this.processBatch(
- batches[i],
- i,
- batches.length,
- client,
- adjustedMaxTokens,
- this.settings.model,
- morphemeInfo
- // ⭐ NEW: 형태소 정보 전달
- );
- aiResults.push(...batchResults);
- if (i < batches.length - 1) {
- await new Promise((resolve) => setTimeout(resolve, 1500));
- }
- } catch (error) {
- Logger.error(`\uBC30\uCE58 ${i + 1} \uCC98\uB9AC \uC2E4\uD328:`, error);
- }
- }
- Logger.log(`AI \uBD84\uC11D \uC644\uB8CC: ${aiResults.length}\uAC1C \uACB0\uACFC \uC218\uC9D1\uB428`);
- }
- const resolvedResults = alreadyResolvedContexts.map((ctx) => ({
- correctionIndex: ctx.correctionIndex,
- selectedValue: ctx.currentValue || ctx.original,
- isExceptionProcessed: ctx.currentState === "exception-processed",
- isOriginalKept: ctx.currentState === "original-kept",
- confidence: 100,
- reasoning: "\uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uC120\uD0DD\uD55C \uD56D\uBAA9\uC785\uB2C8\uB2E4."
- }));
- const allResults = [...aiResults, ...resolvedResults];
- allResults.sort((a, b) => a.correctionIndex - b.correctionIndex);
- Logger.debug(`\uCD5C\uC885 \uCC98\uB9AC \uC644\uB8CC: ${allResults.length}\uAC1C \uACB0\uACFC \uBC18\uD658`);
- return allResults;
- } catch (error) {
- Logger.error("\uBD84\uC11D \uC911 \uC624\uB958 \uBC1C\uC0DD:", error);
- throw new Error(`AI \uBD84\uC11D \uC2E4\uD328: ${error.message}`);
- }
- }
- /**
- * AI 응답 항목의 타입 가드
- */
- isValidAIResponseItem(item) {
- return typeof item === "object" && item !== null && "correctionIndex" in item && "selectedValue" in item;
- }
- /**
- * AI 응답을 파싱하여 구조화된 결과로 변환합니다.
- */
- parseAIResponse(response, correctionContexts) {
- try {
- let parsedResponse;
- let cleanedResponse = response.trim();
- const codeBlockMatch = cleanedResponse.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
- if (codeBlockMatch) {
- cleanedResponse = codeBlockMatch[1].trim();
- }
- let jsonString = "";
- const jsonArrayMatch = cleanedResponse.match(/\[[\s\S]*\]/);
- if (jsonArrayMatch) {
- jsonString = jsonArrayMatch[0];
- } else {
- jsonString = cleanedResponse;
- }
- if (!jsonString.endsWith("]") && jsonString.includes("[")) {
- Logger.warn("JSON\uC774 \uC798\uB9B0 \uAC83\uC73C\uB85C \uBCF4\uC784, \uAC15\uD654\uB41C \uBCF5\uAD6C \uC2DC\uB3C4");
- let lastCompleteObjectIndex = -1;
- let braceCount = 0;
- let inString = false;
- let escapeNext = false;
- for (let i = 0; i < jsonString.length; i++) {
- const char = jsonString[i];
- if (escapeNext) {
- escapeNext = false;
- continue;
- }
- if (char === "\\") {
- escapeNext = true;
- continue;
- }
- if (char === '"') {
- inString = !inString;
- continue;
- }
- if (!inString) {
- if (char === "{") {
- braceCount++;
- } else if (char === "}") {
- braceCount--;
- if (braceCount === 0) {
- lastCompleteObjectIndex = i;
- }
- }
- }
- }
- if (lastCompleteObjectIndex > 0) {
- jsonString = jsonString.substring(0, lastCompleteObjectIndex + 1) + "]";
- Logger.debug("\uACE0\uAE09 JSON \uBCF5\uAD6C \uC644\uB8CC");
- } else {
- const lastBraceIndex = jsonString.lastIndexOf("}");
- if (lastBraceIndex > 0) {
- jsonString = jsonString.substring(0, lastBraceIndex + 1) + "]";
- Logger.debug("\uAE30\uBCF8 JSON \uBCF5\uAD6C \uC644\uB8CC");
- }
- }
- }
- Logger.debug("\uD30C\uC2F1\uD560 JSON (\uCCAB 200\uC790):", jsonString.substring(0, 200) + (jsonString.length > 200 ? "..." : ""));
- try {
- parsedResponse = JSON.parse(jsonString);
- } catch (parseError) {
- Logger.warn("\uCD08\uAE30 JSON \uD30C\uC2F1 \uC2E4\uD328, \uCD94\uAC00 \uBCF5\uAD6C \uC2DC\uB3C4:", parseError);
- let fixedJson = jsonString.replace(/,\s*$/, "");
- if (!fixedJson.endsWith("]")) {
- fixedJson += "]";
- }
- try {
- parsedResponse = JSON.parse(fixedJson);
- Logger.debug("\uC27C\uD45C \uC81C\uAC70\uB85C JSON \uBCF5\uAD6C \uC131\uACF5");
- } catch (secondError) {
- const lastCommaIndex = jsonString.lastIndexOf(",");
- if (lastCommaIndex > 0) {
- const cutJson = jsonString.substring(0, lastCommaIndex) + "]";
- try {
- parsedResponse = JSON.parse(cutJson);
- Logger.debug("\uBD88\uC644\uC804 \uAC1D\uCCB4 \uC81C\uAC70\uB85C JSON \uBCF5\uAD6C \uC131\uACF5");
- } catch (thirdError) {
- throw parseError;
- }
- } else {
- throw parseError;
- }
- }
- }
- const results = [];
- for (const item of parsedResponse) {
- if (!this.isValidAIResponseItem(item)) {
- Logger.warn("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 AI \uC751\uB2F5 \uD56D\uBAA9:", item);
- continue;
- }
- const batchIndex = typeof item.correctionIndex === "number" ? item.correctionIndex : parseInt(String(item.correctionIndex));
- if (isNaN(batchIndex) || batchIndex < 0 || batchIndex >= correctionContexts.length) {
- Logger.warn("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 batchIndex:", batchIndex);
- continue;
- }
- const context = correctionContexts[batchIndex];
- const originalCorrectionIndex = context.correctionIndex;
- let selectedValue = item.selectedValue || "";
- const validOptions = [...context.corrected, context.original];
- Logger.debug(`AI \uC120\uD0DD \uBD84\uC11D - \uC624\uB958 "${context.original}":`);
- Logger.debug(` AI \uC120\uD0DD\uAC12: "${selectedValue}"`);
- Logger.debug(` \uC720\uD6A8\uD55C \uC635\uC158\uB4E4: [${validOptions.map((opt) => `"${opt}"`).join(", ")}]`);
- Logger.debug(` \uCD94\uCC9C \uC774\uC720: "${item.reasoning}"`);
- if (!validOptions.includes(selectedValue)) {
- Logger.warn(`\u{1F534} AI\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC12\uC744 \uC120\uD0DD\uD588\uC2B5\uB2C8\uB2E4: "${selectedValue}"`);
- if (selectedValue === "\uC6D0\uBCF8\uC720\uC9C0" || selectedValue === "\uC608\uC678\uCC98\uB9AC" || !selectedValue) {
- selectedValue = context.original;
- Logger.debug(`"${item.selectedValue}"\uB97C \uC6D0\uBCF8 "${context.original}"\uB85C \uBCC0\uACBD`);
- } else {
- const matchedOption = this.findBestMatch(selectedValue, validOptions);
- if (matchedOption) {
- Logger.warn(`\u26A0\uFE0F AI \uC120\uD0DD \uBD88\uC77C\uCE58: "${selectedValue}" \u2192 "${matchedOption}" (\uC790\uB3D9 \uB9E4\uCE6D)`);
- selectedValue = matchedOption;
- } else {
- Logger.error(`\u274C \uB9E4\uCE6D \uC2E4\uD328 - \uC6D0\uBCF8\uC73C\uB85C \uB300\uCCB4: "${selectedValue}" \u2192 "${context.original}"`);
- selectedValue = context.original;
- }
- }
- } else {
- Logger.debug(`\u2705 AI \uC120\uD0DD\uAC12\uC774 \uC720\uD6A8\uD568: "${selectedValue}"`);
- }
- const isOriginalSelected = selectedValue === context.original;
- const isOriginalKept = isOriginalSelected && !item.isExceptionProcessed;
- const confidence = typeof item.confidence === "number" ? item.confidence : typeof item.confidence === "string" ? parseInt(item.confidence) : 0;
- results.push({
- correctionIndex: originalCorrectionIndex,
- selectedValue,
- isExceptionProcessed: item.isExceptionProcessed || false,
- isOriginalKept,
- confidence: Math.max(0, Math.min(100, confidence)),
- reasoning: item.reasoning || "\uC774\uC720\uAC00 \uC81C\uACF5\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."
- });
- }
- Logger.debug(`\uD30C\uC2F1 \uC644\uB8CC: ${results.length}\uAC1C\uC758 \uACB0\uACFC \uCD94\uCD9C\uB428`);
- const processedIndexes = new Set(results.map((r) => r.correctionIndex));
- const missingContexts = correctionContexts.filter((ctx) => !processedIndexes.has(ctx.correctionIndex));
- if (missingContexts.length > 0) {
- Logger.warn(`\uB204\uB77D\uB41C \uC624\uB958\uB4E4 (\uC6D0\uBCF8 \uC778\uB371\uC2A4): ${missingContexts.map((c) => c.correctionIndex).join(", ")}`);
- missingContexts.forEach((context) => {
- const defaultValue = context.corrected[0] || context.original;
- const isDefaultOriginal = defaultValue === context.original;
- results.push({
- correctionIndex: context.correctionIndex,
- selectedValue: defaultValue,
- isExceptionProcessed: false,
- isOriginalKept: isDefaultOriginal,
- confidence: 50,
- reasoning: "AI \uBD84\uC11D\uC5D0\uC11C \uB204\uB77D\uB418\uC5B4 \uAE30\uBCF8\uAC12\uC73C\uB85C \uC124\uC815\uB428"
- });
- });
- results.sort((a, b) => a.correctionIndex - b.correctionIndex);
- }
- return results;
- } catch (error) {
- Logger.error("\uC751\uB2F5 \uD30C\uC2F1 \uC624\uB958:", error);
- Logger.error("\uC6D0\uBCF8 \uC751\uB2F5 (\uCCAB 500\uC790):", response.substring(0, 500));
- if (error instanceof SyntaxError) {
- throw new Error(`JSON \uD615\uC2DD \uC624\uB958: ${error.message}. AI \uC751\uB2F5\uC774 \uC62C\uBC14\uB978 JSON \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4.`);
- } else {
- throw new Error(`AI \uC751\uB2F5 \uD30C\uC2F1 \uC2E4\uD328: ${error.message}`);
- }
- }
- }
- /**
- * 사용 가능한 모델 목록을 가져옵니다.
- */
- async fetchAvailableModels() {
- try {
- const ClientFactory = await this.getClientFactory();
- return await ClientFactory.fetchModels(this.settings);
- } catch (error) {
- Logger.error("\uBAA8\uB378 \uBAA9\uB85D \uAC00\uC838\uC624\uAE30 \uC2E4\uD328:", error);
- return [];
- }
- }
- /**
- * AI 설정을 업데이트합니다.
- */
- updateSettings(newSettings) {
- this.settings = newSettings;
- }
- /**
- * AI 서비스가 사용 가능한지 확인합니다.
- */
- async isAvailable() {
- return this.settings.enabled && await this.hasValidApiKey(this.settings);
- }
- /**
- * 현재 설정된 제공자 및 모델 정보를 반환합니다.
- */
- getProviderInfo() {
- return {
- provider: this.settings.provider,
- model: this.settings.model,
- available: this.settings.enabled
- // 기본적인 활성화 상태만 체크
- };
- }
- /**
- * 현재 AI 설정을 반환합니다.
- */
- getSettings() {
- return this.settings;
- }
- /**
- * 모델별 최대 출력 토큰을 가져옵니다.
- */
- getModelMaxTokens(model) {
- return MODEL_TOKEN_LIMITS[model] || 2048;
- }
- /**
- * 요청할 토큰 수를 모델 제한에 맞게 조정합니다.
- */
- adjustTokensForModel(requestedTokens, model) {
- const modelLimit = this.getModelMaxTokens(model);
- if (requestedTokens > modelLimit) {
- Logger.warn(`\uC694\uCCAD\uB41C \uD1A0\uD070 \uC218(${requestedTokens})\uAC00 \uBAA8\uB378 \uC81C\uD55C(${modelLimit})\uC744 \uCD08\uACFC\uD569\uB2C8\uB2E4. ${modelLimit}\uB85C \uC870\uC815\uD569\uB2C8\uB2E4.`);
- return modelLimit;
- }
- return requestedTokens;
- }
- /**
- * 현재 제공자의 API 키를 반환합니다.
- */
- getApiKey() {
- switch (this.settings.provider) {
- case "openai":
- return this.settings.openaiApiKey;
- case "anthropic":
- return this.settings.anthropicApiKey;
- case "google":
- return this.settings.googleApiKey;
- case "ollama":
- return this.settings.ollamaEndpoint;
- default:
- return "";
- }
- }
- /**
- * AI가 반환한 값과 가장 유사한 유효한 옵션을 찾습니다.
- */
- findBestMatch(aiValue, validOptions) {
- if (validOptions.includes(aiValue)) {
- return aiValue;
- }
- const cleanAiValue = aiValue.replace(/[\s\*\~\-\+\[\]]/g, "");
- for (const option of validOptions) {
- const cleanOption = option.replace(/[\s\*\~\-\+\[\]]/g, "");
- if (cleanAiValue === cleanOption) {
- return option;
- }
- if (cleanAiValue.includes(cleanOption) || cleanOption.includes(cleanAiValue)) {
- return option;
- }
- }
- let bestMatch = null;
- let bestScore = Infinity;
- for (const option of validOptions) {
- const distance = this.levenshteinDistance(aiValue, option);
- const similarity = 1 - distance / Math.max(aiValue.length, option.length);
- if (similarity >= 0.7 && distance < bestScore) {
- bestMatch = option;
- bestScore = distance;
- }
- }
- return bestMatch;
- }
- /**
- * 두 문자열 간의 편집 거리를 계산합니다.
- */
- levenshteinDistance(str1, str2) {
- const matrix = [];
- const n = str2.length;
- const m = str1.length;
- if (n === 0)
- return m;
- if (m === 0)
- return n;
- for (let i = 0; i <= n; i++) {
- matrix[i] = [i];
- }
- for (let j = 0; j <= m; j++) {
- matrix[0][j] = j;
- }
- for (let i = 1; i <= n; i++) {
- for (let j = 1; j <= m; j++) {
- if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
- matrix[i][j] = matrix[i - 1][j - 1];
- } else {
- matrix[i][j] = Math.min(
- matrix[i - 1][j - 1] + 1,
- matrix[i][j - 1] + 1,
- matrix[i - 1][j] + 1
- );
- }
- }
- }
- return matrix[n][m];
- }
- };
- }
-});
-
-// src/services/advancedSettingsService.ts
-var advancedSettingsService_exports = {};
-__export(advancedSettingsService_exports, {
- AdvancedSettingsService: () => AdvancedSettingsService
-});
-var import_obsidian11, AdvancedSettingsService;
-var init_advancedSettingsService = __esm({
- "src/services/advancedSettingsService.ts"() {
- import_obsidian11 = require("obsidian");
- init_settings();
- init_logger();
- AdvancedSettingsService = class {
- /**
- * 설정을 포괄적으로 검증합니다
- */
- static validateSettings(settings) {
- const result = {
- isValid: true,
- errors: [],
- warnings: [],
- suggestions: []
- };
- if (!settings.apiKey || settings.apiKey.trim() === "") {
- result.errors.push("API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
- result.isValid = false;
- } else if (settings.apiKey.length < 10) {
- result.warnings.push("API \uD0A4\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4. \uC62C\uBC14\uB978 \uD0A4\uC778\uC9C0 \uD655\uC778\uD574\uC8FC\uC138\uC694");
- }
- if (!settings.apiHost || settings.apiHost.trim() === "") {
- result.errors.push("API \uD638\uC2A4\uD2B8\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
- result.isValid = false;
- } else {
- try {
- new URL(`https://${settings.apiHost}`);
- } catch (e) {
- result.errors.push("API \uD638\uC2A4\uD2B8 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
- result.isValid = false;
- }
- }
- if (settings.apiPort < 1 || settings.apiPort > 65535) {
- result.errors.push("API \uD3EC\uD2B8\uB294 1-65535 \uBC94\uC704\uC5EC\uC57C \uD569\uB2C8\uB2E4");
- result.isValid = false;
- }
- if (settings.ai) {
- if (settings.ai.enabled) {
- const hasApiKey = settings.ai.openaiApiKey || settings.ai.anthropicApiKey || settings.ai.googleApiKey || settings.ai.ollamaEndpoint;
- if (!hasApiKey) {
- result.errors.push("AI \uAE30\uB2A5\uC774 \uD65C\uC131\uD654\uB418\uC5C8\uC9C0\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5\uD55C API \uD0A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
- result.isValid = false;
- }
- if (settings.ai.maxTokens < 100) {
- result.warnings.push("\uCD5C\uB300 \uD1A0\uD070 \uC218\uAC00 \uB108\uBB34 \uC801\uC2B5\uB2C8\uB2E4. \uBCF5\uC7A1\uD55C \uBD84\uC11D\uC5D0 \uC81C\uD55C\uC774 \uC788\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
- } else if (settings.ai.maxTokens > 8e3) {
- result.warnings.push("\uCD5C\uB300 \uD1A0\uD070 \uC218\uAC00 \uB9E4\uC6B0 \uD07D\uB2C8\uB2E4. \uBE44\uC6A9\uC774 \uB9CE\uC774 \uBC1C\uC0DD\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
- }
- if (settings.ai.provider === "ollama" && !settings.ai.ollamaEndpoint) {
- result.errors.push("Ollama \uD504\uB85C\uBC14\uC774\uB354 \uC120\uD0DD \uC2DC \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
- result.isValid = false;
- }
- }
- }
- if (settings.ignoredWords && settings.ignoredWords.length > 1e3) {
- result.warnings.push(`\uC608\uC678 \uB2E8\uC5B4\uAC00 ${settings.ignoredWords.length}\uAC1C\uB85C \uB9E4\uC6B0 \uB9CE\uC2B5\uB2C8\uB2E4. \uC131\uB2A5\uC5D0 \uC601\uD5A5\uC744 \uC904 \uC218 \uC788\uC2B5\uB2C8\uB2E4`);
- }
- if (settings.ignoredWords && settings.ignoredWords.length > 100) {
- result.suggestions.push("\uC608\uC678 \uB2E8\uC5B4\uAC00 \uB9CE\uC2B5\uB2C8\uB2E4. \uC815\uAE30\uC801\uC73C\uB85C \uBD88\uD544\uC694\uD55C \uB2E8\uC5B4\uB97C \uC815\uB9AC\uD558\uB294 \uAC83\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4");
- }
- if (settings.ai && settings.ai.enabled && settings.ai.maxTokens > 4e3) {
- result.suggestions.push("AI \uD1A0\uD070 \uC218\uB97C \uC904\uC774\uBA74 \uC751\uB2F5 \uC18D\uB3C4\uAC00 \uD5A5\uC0C1\uB429\uB2C8\uB2E4");
- }
- Logger.debug("\uC124\uC815 \uAC80\uC99D \uC644\uB8CC:", {
- isValid: result.isValid,
- errorsCount: result.errors.length,
- warningsCount: result.warnings.length
- });
- return result;
- }
- /**
- * 설정을 백업합니다
- */
- static backupSettings(settings, reason = "\uC218\uB3D9 \uBC31\uC5C5") {
- const backup = {
- timestamp: Date.now(),
- version: "0.2.0",
- // 현재 플러그인 버전
- settings: JSON.parse(JSON.stringify(settings)),
- // 깊은 복사
- reason
- };
- this.backups.unshift(backup);
- if (this.backups.length > this.MAX_BACKUPS) {
- this.backups = this.backups.slice(0, this.MAX_BACKUPS);
- }
- Logger.debug("\uC124\uC815 \uBC31\uC5C5 \uC0DD\uC131:", { reason, backupCount: this.backups.length });
- }
- /**
- * 백업에서 설정을 복원합니다
- */
- static restoreSettings(backupIndex) {
- if (backupIndex < 0 || backupIndex >= this.backups.length) {
- Logger.error("\uC798\uBABB\uB41C \uBC31\uC5C5 \uC778\uB371\uC2A4:", { backupIndex, availableBackups: this.backups.length });
- return null;
- }
- const backup = this.backups[backupIndex];
- const restoredSettings = JSON.parse(JSON.stringify(backup.settings));
- Logger.debug("\uC124\uC815 \uBCF5\uC6D0:", {
- backupTimestamp: new Date(backup.timestamp).toISOString(),
- reason: backup.reason
- });
- return restoredSettings;
- }
- /**
- * 백업 목록을 반환합니다
- */
- static getBackups() {
- return this.backups.map((backup, index) => ({
- index,
- timestamp: new Date(backup.timestamp).toLocaleString(),
- reason: backup.reason,
- version: backup.version,
- age: this.formatAge(Date.now() - backup.timestamp)
- }));
- }
- /**
- * 설정 프로파일을 생성합니다
- */
- static createProfile(name, description, settings) {
- const profile = {
- id: this.generateId(),
- name: name.trim(),
- description: description.trim(),
- settings: JSON.parse(JSON.stringify(settings)),
- isDefault: false,
- createdAt: Date.now(),
- lastUsed: Date.now()
- };
- this.profiles.push(profile);
- Logger.debug("\uC124\uC815 \uD504\uB85C\uD30C\uC77C \uC0DD\uC131:", { name, id: profile.id });
- return profile.id;
- }
- /**
- * 설정 프로파일을 적용합니다
- */
- static applyProfile(profileId, currentSettings) {
- const profile = this.profiles.find((p) => p.id === profileId);
- if (!profile) {
- Logger.error("\uD504\uB85C\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C:", { profileId });
- return currentSettings;
- }
- const mergedSettings = this.mergeSettings(currentSettings, profile.settings);
- profile.lastUsed = Date.now();
- Logger.debug("\uC124\uC815 \uD504\uB85C\uD30C\uC77C \uC801\uC6A9:", { name: profile.name, id: profileId });
- return mergedSettings;
- }
- /**
- * 프로파일 목록을 반환합니다
- */
- static getProfiles() {
- return this.profiles.map((profile) => ({
- id: profile.id,
- name: profile.name,
- description: profile.description,
- isDefault: profile.isDefault,
- createdAt: new Date(profile.createdAt).toLocaleString(),
- lastUsed: new Date(profile.lastUsed).toLocaleString()
- }));
- }
- /**
- * 프로파일을 삭제합니다
- */
- static deleteProfile(profileId) {
- const index = this.profiles.findIndex((p) => p.id === profileId);
- if (index === -1) {
- return false;
- }
- const profile = this.profiles[index];
- if (profile.isDefault) {
- Logger.debug("\uAE30\uBCF8 \uD504\uB85C\uD30C\uC77C\uC740 \uC0AD\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4:", { profileId });
- return false;
- }
- this.profiles.splice(index, 1);
- Logger.debug("\uC124\uC815 \uD504\uB85C\uD30C\uC77C \uC0AD\uC81C:", { name: profile.name, id: profileId });
- return true;
- }
- /**
- * 설정 변경을 추적합니다
- */
- static trackChange(field, oldValue, newValue, reason) {
- const change = {
- timestamp: Date.now(),
- field,
- oldValue: JSON.parse(JSON.stringify(oldValue)),
- newValue: JSON.parse(JSON.stringify(newValue)),
- reason
- };
- this.changeHistory.unshift(change);
- if (this.changeHistory.length > this.MAX_HISTORY) {
- this.changeHistory = this.changeHistory.slice(0, this.MAX_HISTORY);
- }
- Logger.debug("\uC124\uC815 \uBCC0\uACBD \uCD94\uC801:", { field, reason });
- }
- /**
- * 변경 히스토리를 반환합니다
- */
- static getChangeHistory(limit = 20) {
- return this.changeHistory.slice(0, limit).map((change) => ({
- timestamp: new Date(change.timestamp).toLocaleString(),
- field: change.field,
- oldValue: this.stringifyValue(change.oldValue),
- newValue: this.stringifyValue(change.newValue),
- reason: change.reason,
- age: this.formatAge(Date.now() - change.timestamp)
- }));
- }
- /**
- * 설정을 기본값으로 재설정합니다
- */
- static resetToDefaults(currentSettings) {
- this.backupSettings(currentSettings, "\uAE30\uBCF8\uAC12 \uC7AC\uC124\uC815 \uC804 \uBC31\uC5C5");
- const defaultSettings = {
- apiKey: "",
- apiHost: "bareun-api.junlim.org",
- apiPort: 443,
- ignoredWords: [],
- ai: {
- enabled: false,
- provider: "openai",
- openaiApiKey: "",
- anthropicApiKey: "",
- googleApiKey: "",
- ollamaEndpoint: "http://localhost:11434",
- model: "gpt-4o-mini",
- maxTokens: 2e3,
- temperature: 0.3,
- showTokenWarning: true,
- tokenWarningThreshold: 1500
- },
- filterSingleCharErrors: true,
- inlineMode: DEFAULT_INLINE_MODE_SETTINGS
- };
- Logger.debug("\uC124\uC815\uC744 \uAE30\uBCF8\uAC12\uC73C\uB85C \uC7AC\uC124\uC815");
- return defaultSettings;
- }
- /**
- * 설정을 내보냅니다 (JSON 형태)
- */
- static exportSettings(settings) {
- const exportData = {
- version: "0.2.0",
- timestamp: Date.now(),
- settings,
- metadata: {
- exportedBy: "Korean Grammar Assistant",
- platform: import_obsidian11.Platform.isMacOS ? "macOS" : import_obsidian11.Platform.isWin ? "Windows" : import_obsidian11.Platform.isLinux ? "Linux" : import_obsidian11.Platform.isIosApp ? "iOS" : import_obsidian11.Platform.isAndroidApp ? "Android" : "Unknown",
- isMobile: import_obsidian11.Platform.isMobile
- }
- };
- return JSON.stringify(exportData, null, 2);
- }
- /**
- * 설정을 가져옵니다 (JSON에서)
- */
- static importSettings(jsonData) {
- try {
- const importData = JSON.parse(jsonData);
- if (!importData.settings) {
- return { success: false, error: "\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC124\uC815 \uD30C\uC77C\uC785\uB2C8\uB2E4" };
- }
- const settings = importData.settings;
- const validation = this.validateSettings(settings);
- if (!validation.isValid) {
- return {
- success: false,
- error: `\uC124\uC815 \uC720\uD6A8\uC131 \uAC80\uC0AC \uC2E4\uD328: ${validation.errors.join(", ")}`
- };
- }
- Logger.debug("\uC124\uC815 \uAC00\uC838\uC624\uAE30 \uC131\uACF5:", { version: importData.version });
- return { success: true, settings };
- } catch (error) {
- Logger.error("\uC124\uC815 \uAC00\uC838\uC624\uAE30 \uC2E4\uD328:", error);
- return { success: false, error: "\uC124\uC815 \uD30C\uC77C\uC744 \uD30C\uC2F1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" };
- }
- }
- /**
- * 성능 최적화 제안을 생성합니다
- */
- static getOptimizationSuggestions(settings) {
- var _a;
- const suggestions = [];
- if ((_a = settings.ai) == null ? void 0 : _a.enabled) {
- if (settings.ai.maxTokens > 4e3) {
- suggestions.push({
- type: "cost",
- title: "AI \uD1A0\uD070 \uC218 \uCD5C\uC801\uD654",
- description: "\uB192\uC740 \uD1A0\uD070 \uC218\uB294 \uBE44\uC6A9 \uC99D\uAC00\uC640 \uC751\uB2F5 \uC9C0\uC5F0\uC744 \uC77C\uC73C\uD0AC \uC218 \uC788\uC2B5\uB2C8\uB2E4",
- impact: "medium",
- action: "\uD1A0\uD070 \uC218\uB97C 2000-3000\uC73C\uB85C \uC870\uC815\uD558\uB294 \uAC83\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4"
- });
- }
- if (!settings.ai.openaiApiKey && !settings.ai.anthropicApiKey && !settings.ai.googleApiKey && !settings.ai.ollamaEndpoint) {
- suggestions.push({
- type: "usability",
- title: "AI API \uD0A4 \uC124\uC815",
- description: "AI \uAE30\uB2A5\uC774 \uD65C\uC131\uD654\uB418\uC5C8\uC9C0\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5\uD55C API \uD0A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
- impact: "high",
- action: "\uCD5C\uC18C \uD558\uB098\uC758 AI \uC11C\uBE44\uC2A4 API \uD0A4\uB97C \uC124\uC815\uD574\uC8FC\uC138\uC694"
- });
- }
- }
- if (settings.ignoredWords.length > 500) {
- suggestions.push({
- type: "performance",
- title: "\uC608\uC678 \uB2E8\uC5B4 \uC815\uB9AC",
- description: "\uB108\uBB34 \uB9CE\uC740 \uC608\uC678 \uB2E8\uC5B4\uB294 \uAC80\uC0AC \uC131\uB2A5\uC744 \uC800\uD558\uC2DC\uD0AC \uC218 \uC788\uC2B5\uB2C8\uB2E4",
- impact: "medium",
- action: "\uBD88\uD544\uC694\uD55C \uC608\uC678 \uB2E8\uC5B4\uB97C \uC815\uB9AC\uD558\uB294 \uAC83\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4"
- });
- }
- return suggestions;
- }
- /**
- * 설정을 병합합니다
- */
- static mergeSettings(base, override) {
- const merged = JSON.parse(JSON.stringify(base));
- if (override.apiKey !== void 0)
- merged.apiKey = override.apiKey;
- if (override.apiHost !== void 0)
- merged.apiHost = override.apiHost;
- if (override.apiPort !== void 0)
- merged.apiPort = override.apiPort;
- if (override.ignoredWords !== void 0)
- merged.ignoredWords = override.ignoredWords;
- if (override.filterSingleCharErrors !== void 0)
- merged.filterSingleCharErrors = override.filterSingleCharErrors;
- if (override.ai !== void 0) {
- merged.ai = { ...merged.ai, ...override.ai };
- }
- if (override.inlineMode !== void 0) {
- merged.inlineMode = { ...merged.inlineMode, ...override.inlineMode };
- }
- return merged;
- }
- /**
- * 고유 ID를 생성합니다
- */
- static generateId() {
- return Date.now().toString(36) + Math.random().toString(36).substr(2);
- }
- /**
- * 값을 문자열로 변환합니다
- */
- static stringifyValue(value) {
- if (typeof value === "string") {
- return value.length > 50 ? value.substring(0, 50) + "..." : value;
- }
- if (Array.isArray(value)) {
- return `\uBC30\uC5F4 (${value.length}\uAC1C \uD56D\uBAA9)`;
- }
- if (typeof value === "object" && value !== null) {
- return `\uAC1D\uCCB4 (${Object.keys(value).length}\uAC1C \uC18D\uC131)`;
- }
- return String(value);
- }
- /**
- * 시간 차이를 포맷합니다
- */
- static formatAge(ms) {
- const seconds = Math.floor(ms / 1e3);
- const minutes = Math.floor(seconds / 60);
- const hours = Math.floor(minutes / 60);
- const days = Math.floor(hours / 24);
- if (days > 0)
- return `${days}\uC77C \uC804`;
- if (hours > 0)
- return `${hours}\uC2DC\uAC04 \uC804`;
- if (minutes > 0)
- return `${minutes}\uBD84 \uC804`;
- return `${seconds}\uCD08 \uC804`;
- }
- };
- AdvancedSettingsService.MAX_BACKUPS = 10;
- AdvancedSettingsService.MAX_HISTORY = 100;
- AdvancedSettingsService.backups = [];
- AdvancedSettingsService.profiles = [];
- AdvancedSettingsService.changeHistory = [];
- }
-});
-
-// src/utils/tokenWarningModal.ts
-var tokenWarningModal_exports = {};
-__export(tokenWarningModal_exports, {
- TokenWarningModal: () => TokenWarningModal
-});
-var import_obsidian18, TokenWarningModal;
-var init_tokenWarningModal = __esm({
- "src/utils/tokenWarningModal.ts"() {
- init_logger();
- import_obsidian18 = require("obsidian");
- TokenWarningModal = class {
- /**
- * 토큰 사용량 경고를 확인하고 사용자 확인을 받습니다.
- */
- static async checkTokenUsageWarning(request, aiService, settings, onSettingsUpdate) {
- Logger.log("\u{1F50D} TokenWarningModal.checkTokenUsageWarning \uC2DC\uC791");
- const aiSettings = aiService == null ? void 0 : aiService.getProviderInfo();
- Logger.log("\u{1F50D} AI \uC11C\uBE44\uC2A4 \uC815\uBCF4:", aiSettings);
- if (!aiService || !(aiSettings == null ? void 0 : aiSettings.available)) {
- Logger.warn("AI \uC11C\uBE44\uC2A4\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return false;
- }
- const showWarning = settings.showTokenWarning;
- const threshold = settings.tokenWarningThreshold;
- const maxTokens = settings.maxTokens;
- Logger.log("\u{1F50D} \uD1A0\uD070 \uACBD\uACE0 \uC124\uC815 \uD655\uC778:", { showWarning, threshold, maxTokens });
- if (!showWarning) {
- Logger.log("\u{1F50D} \uD1A0\uD070 \uACBD\uACE0\uAC00 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC5B4\uC11C \uBC14\uB85C \uC9C4\uD589");
- return true;
- }
- Logger.log("\u{1F50D} \uD1A0\uD070 \uC0AC\uC6A9\uB7C9 \uCD94\uC815 \uC2DC\uC791");
- const tokenUsage = await this.estimateTokenUsageWithMorphemes(request, aiService);
- const isOverMaxTokens = tokenUsage.totalEstimated > maxTokens;
- Logger.log("\u{1F50D} \uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC \uD1A0\uD070 \uC0AC\uC6A9\uB7C9:", {
- total: tokenUsage.totalEstimated,
- input: tokenUsage.inputTokens,
- output: tokenUsage.estimatedOutputTokens,
- cost: tokenUsage.estimatedCost,
- morphemeOptimized: tokenUsage.morphemeOptimized,
- threshold,
- maxTokens,
- isOverThreshold: tokenUsage.totalEstimated >= threshold,
- isOverMaxTokens
- });
- if (tokenUsage.totalEstimated < threshold && !isOverMaxTokens) {
- Logger.log("\u{1F50D} \uC784\uACC4\uAC12 \uBBF8\uB9CC\uC774\uACE0 \uCD5C\uB300 \uD1A0\uD070 \uC774\uB0B4\uB77C\uC11C \uBC14\uB85C \uC9C4\uD589");
- return true;
- }
- Logger.log("\u{1F50D} \uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC \uD45C\uC2DC \uC870\uAC74 \uB9CC\uC871 - \uBAA8\uB2EC \uD45C\uC2DC \uC2DC\uC791");
- return new Promise((resolve) => {
- var _a, _b, _c;
- const modal = document.createElement("div");
- modal.className = "modal-overlay token-warning-overlay korean-grammar-token-modal";
- const modalContent = this.createTokenWarningModal(tokenUsage, isOverMaxTokens, maxTokens);
- modal.appendChild(modalContent);
- if (import_obsidian18.Platform.isMobile) {
- document.body.classList.add("spell-popup-open");
- this.hideKeyboardAndBlurEditor();
- Logger.debug("\u{1F4F1} \uD1A0\uD070 \uBAA8\uB2EC: spell-popup-open \uD074\uB798\uC2A4 \uCD94\uAC00 \uBC0F \uD0A4\uBCF4\uB4DC \uC228\uAE40 \uCC98\uB9AC");
- }
- document.body.appendChild(modal);
- setTimeout(() => {
- modal.focus();
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uD3EC\uCEE4\uC2A4 \uC124\uC815 \uC644\uB8CC");
- }, 10);
- let handleResponse = (action) => {
- if (import_obsidian18.Platform.isMobile) {
- document.body.classList.remove("spell-popup-open");
- Logger.debug("\u{1F4F1} \uD1A0\uD070 \uBAA8\uB2EC: spell-popup-open \uD074\uB798\uC2A4 \uC81C\uAC70\uB85C \uBC30\uACBD \uC785\uB825 \uBCF5\uC6D0");
- }
- modal.remove();
- if (action === "proceed") {
- Logger.log("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uC0AC\uC6A9\uC790\uAC00 \uACC4\uC18D \uC9C4\uD589\uC744 \uC120\uD0DD\uD588\uC2B5\uB2C8\uB2E4.");
- resolve(true);
- } else if (action === "updateSettings") {
- Logger.log("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uC0AC\uC6A9\uC790\uAC00 \uC124\uC815 \uC5C5\uB370\uC774\uD2B8\uB97C \uC120\uD0DD\uD588\uC2B5\uB2C8\uB2E4.");
- if (onSettingsUpdate) {
- const newMaxTokens = tokenUsage.totalEstimated + 1e3;
- onSettingsUpdate(newMaxTokens);
- }
- resolve(true);
- } else {
- Logger.log("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uC0AC\uC6A9\uC790\uAC00 \uCDE8\uC18C\uB97C \uC120\uD0DD\uD588\uC2B5\uB2C8\uB2E4.");
- resolve(false);
- }
- };
- const handleKeyboard = (e) => {
- Logger.debug(`\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uD0A4 \uC774\uBCA4\uD2B8 \uAC10\uC9C0 - ${e.key} (\uCF54\uB4DC: ${e.code})`);
- e.preventDefault();
- e.stopPropagation();
- e.stopImmediatePropagation();
- if (e.key === "Enter") {
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: Enter\uD0A4 \uAC10\uC9C0 - \uC9C4\uD589");
- handleResponse("proceed");
- } else if (e.key === "Escape") {
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: Escape\uD0A4 \uAC10\uC9C0 - \uCDE8\uC18C");
- handleResponse("cancel");
- }
- };
- modal.addEventListener("keydown", handleKeyboard, { capture: true });
- const globalKeyHandler = (e) => {
- if (document.body.contains(modal)) {
- Logger.debug(`\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uAE00\uB85C\uBC8C \uD0A4 \uC774\uBCA4\uD2B8 \uCC28\uB2E8 - ${e.key}`);
- e.preventDefault();
- e.stopPropagation();
- e.stopImmediatePropagation();
- if (e.key === "Enter") {
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uAE00\uB85C\uBC8C Enter\uD0A4 \uAC10\uC9C0 - \uC9C4\uD589");
- handleResponse("proceed");
- } else if (e.key === "Escape") {
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uAE00\uB85C\uBC8C Escape\uD0A4 \uAC10\uC9C0 - \uCDE8\uC18C");
- handleResponse("cancel");
- }
- }
- };
- window.addEventListener("keydown", globalKeyHandler, { capture: true });
- const originalHandleResponse = handleResponse;
- handleResponse = (action) => {
- modal.removeEventListener("keydown", handleKeyboard, { capture: true });
- window.removeEventListener("keydown", globalKeyHandler, { capture: true });
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uBAA8\uB4E0 \uC774\uBCA4\uD2B8 \uB9AC\uC2A4\uB108 \uC81C\uAC70 \uC644\uB8CC");
- originalHandleResponse(action);
- };
- (_a = modal.querySelector("#token-warning-cancel")) == null ? void 0 : _a.addEventListener("click", () => handleResponse("cancel"));
- (_b = modal.querySelector("#token-warning-proceed")) == null ? void 0 : _b.addEventListener("click", () => handleResponse("proceed"));
- (_c = modal.querySelector("#token-warning-update-settings")) == null ? void 0 : _c.addEventListener("click", () => handleResponse("updateSettings"));
- });
- }
- /**
- * 토큰 경고 모달의 DOM 구조를 생성합니다.
- */
- static createTokenWarningModal(tokenUsage, isOverMaxTokens, maxTokens) {
- const content = document.createElement("div");
- content.className = "token-warning-content";
- const headerInfo = content.appendChild(document.createElement("div"));
- headerInfo.className = "token-warning-header";
- const title = headerInfo.appendChild(document.createElement("h3"));
- title.className = "token-warning-title";
- title.textContent = isOverMaxTokens ? "\uD1A0\uD070 \uC0AC\uC6A9\uB7C9 \uD655\uC778" : "\uD1A0\uD070 \uC0AC\uC6A9\uB7C9 \uC548\uB0B4";
- const description = headerInfo.appendChild(document.createElement("p"));
- description.className = "token-warning-description";
- description.textContent = isOverMaxTokens ? "\uC124\uC815\uB41C \uD55C\uACC4\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4" : "\uC608\uC0C1 \uC0AC\uC6A9\uB7C9\uC774 \uB192\uC2B5\uB2C8\uB2E4";
- const details = content.appendChild(document.createElement("div"));
- details.className = "token-warning-details";
- const stats = details.appendChild(document.createElement("div"));
- stats.className = "token-warning-stats";
- const totalTokenItem = stats.appendChild(document.createElement("div"));
- totalTokenItem.className = "token-stat-item";
- const totalTokenNumber = totalTokenItem.appendChild(document.createElement("div"));
- totalTokenNumber.className = "token-stat-number";
- totalTokenNumber.textContent = tokenUsage.totalEstimated.toLocaleString();
- const totalTokenLabel = totalTokenItem.appendChild(document.createElement("div"));
- totalTokenLabel.className = "token-stat-label";
- totalTokenLabel.textContent = "\uCD1D \uD1A0\uD070";
- const costItem = stats.appendChild(document.createElement("div"));
- costItem.className = "token-stat-item";
- const costNumber = costItem.appendChild(document.createElement("div"));
- costNumber.className = "token-stat-number";
- costNumber.textContent = tokenUsage.estimatedCost;
- const costLabel = costItem.appendChild(document.createElement("div"));
- costLabel.className = "token-stat-label";
- costLabel.textContent = "\uC608\uC0C1 \uBE44\uC6A9";
- const rec = details.appendChild(document.createElement("div"));
- rec.className = "token-warning-recommendation";
- const recText = rec.appendChild(document.createElement("div"));
- recText.className = "token-warning-recommendation-text";
- recText.textContent = `\uC785\uB825: ${tokenUsage.inputTokens.toLocaleString()} \u2022 \uCD9C\uB825: ${tokenUsage.estimatedOutputTokens.toLocaleString()}`;
- if (isOverMaxTokens) {
- const overLimit = content.appendChild(document.createElement("div"));
- overLimit.className = "token-warning-over-limit";
- const overLimitIcon = overLimit.appendChild(document.createElement("div"));
- overLimitIcon.className = "token-warning-over-limit-icon";
- overLimitIcon.textContent = "\u26A0\uFE0F";
- const overLimitText = overLimit.appendChild(document.createElement("div"));
- overLimitText.className = "token-warning-over-limit-text";
- const overLimitTitle = overLimitText.appendChild(document.createElement("div"));
- overLimitTitle.className = "token-warning-over-limit-title";
- overLimitTitle.textContent = "\uC124\uC815\uB41C \uCD5C\uB300 \uD1A0\uD070\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4";
- const overLimitDesc = overLimitText.appendChild(document.createElement("div"));
- overLimitDesc.className = "token-warning-over-limit-description";
- overLimitDesc.textContent = `\uD604\uC7AC \uC124\uC815: ${maxTokens.toLocaleString()} \uD1A0\uD070 \u2192 \uCD08\uACFC\uB7C9: ${(tokenUsage.totalEstimated - maxTokens).toLocaleString()} \uD1A0\uD070`;
- }
- const actions = content.appendChild(document.createElement("div"));
- actions.className = "token-warning-actions";
- const cancelBtn = document.createElement("button");
- cancelBtn.id = "token-warning-cancel";
- cancelBtn.className = "token-warning-btn token-warning-btn-cancel";
- cancelBtn.textContent = "\uCDE8\uC18C";
- actions.appendChild(cancelBtn);
- if (isOverMaxTokens) {
- const updateBtn = document.createElement("button");
- updateBtn.id = "token-warning-update-settings";
- updateBtn.className = "token-warning-btn token-warning-btn-settings";
- updateBtn.textContent = "\uC124\uC815 \uC5C5\uB370\uC774\uD2B8 \uD6C4 \uC9C4\uD589";
- actions.appendChild(updateBtn);
- }
- const proceedBtn = document.createElement("button");
- proceedBtn.id = "token-warning-proceed";
- proceedBtn.className = "token-warning-btn token-warning-btn-proceed";
- proceedBtn.textContent = isOverMaxTokens ? "\uAC15\uC81C \uC9C4\uD589" : "\uACC4\uC18D \uC9C4\uD589";
- actions.appendChild(proceedBtn);
- return content;
- }
- /**
- * 형태소 최적화를 고려한 토큰 사용량을 추정합니다.
- */
- static async estimateTokenUsageWithMorphemes(request, aiService) {
- var _a;
- try {
- Logger.log("\u{1F50D} \uD1A0\uD070 \uCD94\uC815 \uC2DC\uC791 - \uC694\uCCAD \uC815\uBCF4:", {
- correctionsCount: request.corrections.length,
- contextWindow: request.contextWindow,
- originalTextLength: ((_a = request.originalText) == null ? void 0 : _a.length) || 0
- });
- const hasMultipleCorrections = request.corrections.length > 1;
- const morphemeOptimized = hasMultipleCorrections;
- Logger.log("\u{1F50D} \uD1A0\uD070 \uACBD\uACE0\uC6A9 \uD615\uD0DC\uC18C \uCD5C\uC801\uD654 \uCD94\uC815:", {
- correctionsCount: request.corrections.length,
- estimatedOptimization: morphemeOptimized,
- reason: morphemeOptimized ? "\uBCF5\uC218 \uAD50\uC815\uC73C\uB85C \uCEE8\uD14D\uC2A4\uD2B8 \uCD95\uC18C \uC608\uC0C1" : "\uB2E8\uC77C \uAD50\uC815\uC73C\uB85C \uCD5C\uC801\uD654 \uBD88\uD544\uC694"
- });
- const adjustedRequest = {
- ...request,
- contextWindow: morphemeOptimized ? 30 : 100
- // 형태소 최적화 시 컨텍스트 축소
- };
- Logger.log("\u{1F50D} AI \uC11C\uBE44\uC2A4 \uD1A0\uD070 \uCD94\uC815 \uD638\uCD9C \uC804");
- const baseEstimation = (aiService == null ? void 0 : aiService.estimateTokenUsage(adjustedRequest)) || {
- inputTokens: 0,
- estimatedOutputTokens: 0,
- totalEstimated: 0,
- estimatedCost: "$0.00"
- };
- Logger.log("\u{1F50D} AI \uC11C\uBE44\uC2A4 \uD1A0\uD070 \uCD94\uC815 \uACB0\uACFC:", baseEstimation);
- const morphemeTokens = morphemeOptimized ? 50 : 0;
- const finalEstimation = {
- inputTokens: baseEstimation.inputTokens + morphemeTokens,
- estimatedOutputTokens: baseEstimation.estimatedOutputTokens,
- totalEstimated: baseEstimation.totalEstimated + morphemeTokens,
- estimatedCost: baseEstimation.estimatedCost,
- morphemeOptimized
- };
- Logger.debug("\uD615\uD0DC\uC18C \uCD5C\uC801\uD654 \uBC18\uC601 \uD1A0\uD070 \uCD94\uC815:", {
- before: baseEstimation.totalEstimated,
- after: finalEstimation.totalEstimated,
- contextReduction: morphemeOptimized ? 100 - 30 : 0,
- // 70토큰 절약
- morphemeTokens,
- netChange: morphemeOptimized ? morphemeTokens - 70 : 0,
- // 순 변화량
- optimized: morphemeOptimized
- });
- return finalEstimation;
- } catch (error) {
- Logger.error("\uD1A0\uD070 \uCD94\uC815 \uC2E4\uD328, \uAE30\uBCF8\uAC12 \uC0AC\uC6A9:", error);
- const fallbackEstimation = (aiService == null ? void 0 : aiService.estimateTokenUsage(request)) || {
- inputTokens: 0,
- estimatedOutputTokens: 0,
- totalEstimated: 0,
- estimatedCost: "$0.00"
- };
- Logger.warn("\uD3F4\uBC31 \uD1A0\uD070 \uCD94\uC815 \uC0AC\uC6A9:", fallbackEstimation);
- return {
- ...fallbackEstimation,
- morphemeOptimized: false
- };
- }
- }
- /**
- * 🔧 모바일에서 키보드 숨기기 및 에디터 포커스 해제 (인라인 툴팁과 동일한 로직)
- */
- static hideKeyboardAndBlurEditor() {
- var _a, _b, _c, _d;
- try {
- const obsidianApp = window.app;
- if (obsidianApp) {
- const activeView = obsidianApp.workspace.getActiveViewOfType(import_obsidian18.MarkdownView);
- if (activeView == null ? void 0 : activeView.editor) {
- if ((_b = (_a = activeView.editor).hasFocus) == null ? void 0 : _b.call(_a)) {
- Logger.log("\u{1F4F1} \uD1A0\uD070 \uBAA8\uB2EC: \uC5D0\uB514\uD130 \uD3EC\uCEE4\uC2A4 \uD574\uC81C \uC2DC\uC791");
- (_d = (_c = activeView.editor).blur) == null ? void 0 : _d.call(_c);
- const cmEditor = activeView.editor.cm;
- if (cmEditor && cmEditor.dom) {
- cmEditor.dom.blur();
- }
- }
- }
- }
- const focusedElement = document.activeElement;
- if (focusedElement && focusedElement.blur) {
- focusedElement.blur();
- Logger.log("\u{1F4F1} \uD1A0\uD070 \uBAA8\uB2EC: DOM \uD3EC\uCEE4\uC2A4 \uD574\uC81C \uC644\uB8CC");
- }
- const cmEditors = document.querySelectorAll(".cm-editor .cm-content");
- cmEditors.forEach((editor) => {
- if (editor instanceof HTMLElement) {
- editor.blur();
- }
- });
- const hiddenInput = document.createElement("input");
- hiddenInput.classList.add("kga-stealth-input");
- document.body.appendChild(hiddenInput);
- setTimeout(() => {
- hiddenInput.focus();
- setTimeout(() => {
- hiddenInput.blur();
- document.body.removeChild(hiddenInput);
- Logger.log("\u{1F4F1} \uD1A0\uD070 \uBAA8\uB2EC: \uD0A4\uBCF4\uB4DC \uC228\uAE40 \uCC98\uB9AC \uC644\uB8CC");
- }, 50);
- }, 100);
- } catch (error) {
- Logger.warn("\u{1F4F1} \uD1A0\uD070 \uBAA8\uB2EC \uD0A4\uBCF4\uB4DC \uC228\uAE40 \uC911 \uC624\uB958:", error);
- }
- }
- };
- }
-});
-
-// main.ts
-var main_exports = {};
-__export(main_exports, {
- default: () => KoreanGrammarPlugin
-});
-module.exports = __toCommonJS(main_exports);
-var import_obsidian19 = require("obsidian");
-init_settings();
-
-// src/orchestrator.ts
-var import_obsidian10 = require("obsidian");
-
-// src/services/api.ts
-var import_obsidian2 = require("obsidian");
-init_logger();
-init_errorHandler();
-var SpellCheckApiService = class {
- constructor() {
- this.morphemeCache = /* @__PURE__ */ new Map();
- this.maxCacheSize = 100;
- }
- // 최대 100개 캐시 유지
- /**
- * 텍스트의 형태소를 분석합니다 (캐싱 및 최적화 적용).
- * @param text 분석할 텍스트
- * @param settings 플러그인 설정
- * @returns 형태소 분석 결과
- */
- async analyzeMorphemes(text, settings) {
- const cacheKey = `morpheme_${this.hashText(text)}`;
- const cachedResult = this.morphemeCache.get(cacheKey);
- if (cachedResult) {
- return cachedResult;
- }
- try {
- const result = await this.executeMorphemeRequest(text, settings);
- this.morphemeCache.set(cacheKey, result);
- this.manageCacheSize();
- return result;
- } catch (error) {
- Logger.error("\uD615\uD0DC\uC18C \uBD84\uC11D \uC2E4\uD328:", error);
- throw error;
- }
- }
- /**
- * 실제 형태소 분석 API 요청을 수행합니다.
- */
- async executeMorphemeRequest(text, settings) {
- const protocol = settings.apiPort === 443 ? "https" : "http";
- const port = settings.apiPort === 443 || settings.apiPort === 80 ? "" : `:${settings.apiPort}`;
- const apiUrl = `${protocol}://${settings.apiHost}${port}/bareun/api/v1/analyze`;
- const requestBody = {
- document: {
- content: text,
- language: "ko-KR"
- },
- encoding_type: "UTF8"
- };
- Logger.debug("\uD615\uD0DC\uC18C \uBD84\uC11D API \uC694\uCCAD:", {
- url: apiUrl,
- textLength: text.length,
- cached: false
- });
- return await ErrorHandlerService.withRetry(
- async () => {
- var _a, _b, _c;
- const response = await this.requestWithTimeout(
- (0, import_obsidian2.requestUrl)({
- url: apiUrl,
- method: "POST",
- body: JSON.stringify(requestBody),
- contentType: "application/json",
- headers: {
- "api-key": settings.apiKey
- },
- throw: false
- }),
- 1e4,
- "\uD615\uD0DC\uC18C \uBD84\uC11D \uC694\uCCAD \uD0C0\uC784\uC544\uC6C3 (10\uCD08)"
- );
- if (response.status < 200 || response.status >= 300) {
- Logger.error("\uD615\uD0DC\uC18C \uBD84\uC11D API \uC624\uB958:", {
- status: response.status,
- errorBody: response.text
- });
- throw new Error(`\uD615\uD0DC\uC18C \uBD84\uC11D API \uC694\uCCAD \uC2E4\uD328: ${response.status}`);
- }
- const data = (_a = response.json) != null ? _a : JSON.parse(response.text || "{}");
- Logger.debug("\uD615\uD0DC\uC18C \uBD84\uC11D API \uC751\uB2F5 \uC131\uACF5:", {
- textLength: text.length,
- tokensCount: ((_b = data.sentences) == null ? void 0 : _b.reduce((count, sentence) => count + sentence.tokens.length, 0)) || 0,
- sentencesCount: ((_c = data.sentences) == null ? void 0 : _c.length) || 0
- });
- return data;
- },
- `morpheme-analysis-${text.substring(0, 50)}`,
- {
- maxRetries: 2,
- baseDelay: 1e3,
- maxDelay: 3e3,
- backoffFactor: 1.5
- }
- );
- }
- /**
- * 텍스트를 해시합니다 (캐시 키 생성용).
- */
- hashText(text) {
- let hash = 0;
- for (let i = 0; i < text.length; i++) {
- const char = text.charCodeAt(i);
- hash = (hash << 5) - hash + char;
- hash = hash & hash;
- }
- return hash.toString(36);
- }
- /**
- * 캐시 크기를 관리합니다 (LRU 방식).
- */
- manageCacheSize() {
- if (this.morphemeCache.size > this.maxCacheSize) {
- const firstKey = this.morphemeCache.keys().next().value;
- this.morphemeCache.delete(firstKey);
- }
- }
- /**
- * 캐시를 수동으로 정리합니다.
- */
- clearMorphemeCache() {
- this.morphemeCache.clear();
- }
- /**
- * 캐시 통계를 반환합니다.
- */
- getMorphemeCacheStats() {
- return {
- size: this.morphemeCache.size,
- maxSize: this.maxCacheSize
- };
- }
- /**
- * 텍스트의 맞춤법을 검사합니다.
- * @param text 검사할 텍스트
- * @param settings 플러그인 설정
- * @returns 검사 결과
- */
- async checkSpelling(text, settings) {
- var _a;
- if (!settings.apiKey || settings.apiKey.trim() === "") {
- throw new Error("API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC5D0\uC11C Bareun.ai API \uD0A4\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
- }
- const protocol = settings.apiPort === 443 ? "https" : "http";
- const port = settings.apiPort === 443 || settings.apiPort === 80 ? "" : `:${settings.apiPort}`;
- const apiUrl = `${protocol}://${settings.apiHost}${port}/bareun/api/v1/correct-error`;
- const requestBody = {
- document: {
- content: text,
- type: "PLAIN_TEXT"
- },
- encoding_type: "UTF8",
- auto_split: false
- // 🔧 자동 분할 비활성화하여 불필요한 교정 방지
- };
- const response = await this.requestWithTimeout(
- (0, import_obsidian2.requestUrl)({
- url: apiUrl,
- method: "POST",
- body: JSON.stringify(requestBody),
- contentType: "application/json",
- headers: {
- "api-key": settings.apiKey
- },
- throw: false
- }),
- 15e3,
- "\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC694\uCCAD \uD0C0\uC784\uC544\uC6C3 (15\uCD08)"
- );
- if (response.status < 200 || response.status >= 300) {
- Logger.error("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC API \uC624\uB958:", {
- status: response.status,
- errorBody: response.text
- });
- throw new Error(`API \uC694\uCCAD \uC2E4\uD328: ${response.status}`);
- }
- const data = (_a = response.json) != null ? _a : JSON.parse(response.text || "{}");
- return this.parseBareunResults(data, text, settings);
- }
- /**
- * requestUrl 호출에 대한 타임아웃 래퍼
- */
- async requestWithTimeout(requestPromise, timeoutMs, timeoutMessage) {
- let timeoutId;
- const timeoutPromise = new Promise((_, reject) => {
- timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
- });
- try {
- return await Promise.race([requestPromise, timeoutPromise]);
- } finally {
- if (timeoutId) {
- clearTimeout(timeoutId);
- }
- }
- }
- /**
- * Bareun.ai API 응답을 파싱하여 교정 정보를 추출합니다.
- * @param data API 응답 데이터
- * @param originalText 원본 텍스트
- * @param settings 플러그인 설정
- * @returns 파싱된 결과
- */
- parseBareunResults(data, originalText, settings) {
- var _a;
- const corrections = [];
- let resultOutput = data.revised || originalText;
- const correctionMap = /* @__PURE__ */ new Map();
- Logger.debug("=== Bareun API \uC751\uB2F5 \uBD84\uC11D ===");
- Logger.debug("\uC6D0\uBCF8 \uD14D\uC2A4\uD2B8:", originalText);
- Logger.debug("\uAD50\uC815\uB41C \uD14D\uC2A4\uD2B8:", data.revised);
- Logger.debug("revisedSentences \uC218:", ((_a = data.revisedSentences) == null ? void 0 : _a.length) || 0);
- const cleanOriginal = originalText.trim().replace(/\s+/g, " ");
- const cleanRevised = (data.revised || "").trim().replace(/\s+/g, " ");
- Logger.log("\u{1F50D} \uC804\uCCB4 \uD14D\uC2A4\uD2B8 \uBE44\uAD50:");
- Logger.log(" \uC815\uB9AC\uB41C \uC6D0\uBCF8:", `"${cleanOriginal}"`);
- Logger.log(" \uC815\uB9AC\uB41C \uAD50\uC815:", `"${cleanRevised}"`);
- Logger.log(" \uD14D\uC2A4\uD2B8 \uB3D9\uC77C \uC5EC\uBD80:", cleanOriginal === cleanRevised);
- if (cleanOriginal === cleanRevised) {
- Logger.log("\u2705 \uC804\uCCB4 \uD14D\uC2A4\uD2B8 \uAC80\uC99D: \uC6D0\uBCF8\uACFC \uAD50\uC815\uBCF8\uC774 \uB3D9\uC77C\uD558\uC5EC \uC624\uB958 \uC5C6\uC74C\uC73C\uB85C \uCC98\uB9AC");
- return {
- resultOutput: originalText,
- corrections: []
- };
- }
- if (data.revisedSentences && Array.isArray(data.revisedSentences)) {
- data.revisedSentences.forEach((sentence, sentenceIndex) => {
- var _a2;
- Logger.debug(`
---- \uBB38\uC7A5 ${sentenceIndex + 1} ---`);
- Logger.debug("\uC6D0\uBCF8 \uBB38\uC7A5:", sentence.origin);
- Logger.debug("\uAD50\uC815\uB41C \uBB38\uC7A5:", sentence.revised);
- Logger.debug("revisedBlocks \uC218:", ((_a2 = sentence.revisedBlocks) == null ? void 0 : _a2.length) || 0);
- if (sentence.revisedBlocks && Array.isArray(sentence.revisedBlocks)) {
- sentence.revisedBlocks.forEach((block, blockIndex) => {
- var _a3, _b, _c, _d, _e, _f;
- Logger.log(`
- \uBE14\uB85D ${blockIndex + 1}:`);
- Logger.debug(` \uC804\uCCB4 \uBE14\uB85D \uC815\uBCF4:`, JSON.stringify(block, null, 2));
- Logger.debug(" \uC6D0\uBCF8 \uB0B4\uC6A9:", (_a3 = block.origin) == null ? void 0 : _a3.content);
- Logger.debug(" \uC6D0\uBCF8 \uC704\uCE58:", `${(_b = block.origin) == null ? void 0 : _b.beginOffset}-${((_c = block.origin) == null ? void 0 : _c.beginOffset) + ((_d = block.origin) == null ? void 0 : _d.length)}`);
- Logger.debug(" \uAD50\uC815:", block.revised);
- Logger.debug(" \uC81C\uC548 \uC218:", ((_e = block.revisions) == null ? void 0 : _e.length) || 0);
- if (block.origin && block.revised && block.revisions) {
- const blockOriginalText = block.origin.content;
- const blockStart = block.origin.beginOffset;
- const blockLength = block.origin.length;
- const blockEnd = blockStart + blockLength;
- const actualTextAtPosition = originalText.slice(blockStart, blockEnd);
- const positionMatches = actualTextAtPosition === blockOriginalText;
- Logger.log(`\u{1F4DD} \uBE14\uB85D \uC0C1\uC138 \uBD84\uC11D:`);
- Logger.log(` API \uC6D0\uBCF8: "${blockOriginalText}"`);
- Logger.log(` API \uAD50\uC815: "${block.revised}"`);
- Logger.log(` API \uC704\uCE58: ${blockStart}-${blockEnd} (\uAE38\uC774: ${blockLength})`);
- Logger.log(` \uC2E4\uC81C \uC704\uCE58 \uD14D\uC2A4\uD2B8: "${actualTextAtPosition}"`);
- Logger.log(` \uC704\uCE58 \uB9E4\uCE6D: ${positionMatches ? "\u2705" : "\u274C"}`);
- Logger.log(` \uC6D0\uBCF8 = \uAD50\uC815: ${blockOriginalText === block.revised}`);
- if (correctionMap.has(blockOriginalText)) {
- Logger.warn(` \u26A0\uFE0F \uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uAD50\uC815: "${blockOriginalText}"`);
- Logger.warn(` \uAE30\uC874 \uC81C\uC548\uB4E4:`, correctionMap.get(blockOriginalText).corrected);
- Logger.warn(` \uC0C8\uB85C\uC6B4 \uC81C\uC548\uB4E4:`, block.revisions.map((rev) => rev.revised));
- }
- if (blockOriginalText === block.revised) {
- Logger.debug(" -> \uC6D0\uBCF8\uACFC \uAD50\uC815\uBCF8\uC774 \uB3D9\uC77C\uD558\uC5EC \uAC74\uB108\uB700");
- return;
- }
- if (!blockOriginalText || blockOriginalText.trim().length === 0) {
- Logger.debug(" -> \uBE48 \uD14D\uC2A4\uD2B8\uB85C \uAC74\uB108\uB700");
- return;
- }
- if (!positionMatches) {
- Logger.debug(`\u{1F4CD} API \uC704\uCE58 vs \uC2E4\uC81C \uC704\uCE58 \uCC28\uC774: ${blockStart}-${blockEnd} vs "${actualTextAtPosition}"`);
- const foundIndex = originalText.indexOf(blockOriginalText);
- if (foundIndex === -1) {
- Logger.debug(" -> \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uC5D0\uC11C \uC644\uC804\uD788 \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uAC74\uB108\uB700");
- return;
- } else {
- Logger.debug(` -> "${blockOriginalText}" \uC2E4\uC81C \uC704\uCE58: ${foundIndex} (API\uC640 ${Math.abs(foundIndex - blockStart)}\uC790 \uCC28\uC774)`);
- }
- } else {
- if (originalText.indexOf(blockOriginalText) === -1) {
- Logger.debug(" -> \uC6D0\uBCF8 \uD14D\uC2A4\uD2B8\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uAC74\uB108\uB700");
- return;
- }
- }
- const suggestions = block.revisions.map((rev) => rev.revised);
- Logger.debug(` \u{1F50D} API\uC5D0\uC11C \uBC1B\uC740 \uC81C\uC548 \uC218: ${suggestions.length}\uAC1C`);
- Logger.debug(" \uC81C\uC548\uB4E4:", suggestions);
- const uniqueSuggestions = [...new Set(suggestions)].filter((s) => {
- const isValid = s !== blockOriginalText && s.trim() !== blockOriginalText.trim() && s.length > 0 && !s.includes("\uFFFD");
- Logger.debug(` "${s}" \u2192 "${blockOriginalText}" \uC720\uD6A8\uC131: ${isValid}`);
- return isValid;
- });
- Logger.debug(` \u2705 \uC911\uBCF5\uC81C\uAC70 \uD6C4 \uC720\uD6A8\uD55C \uC81C\uC548 \uC218: ${uniqueSuggestions.length}\uAC1C`);
- Logger.debug(" \uC720\uD6A8\uD55C \uC81C\uC548\uB4E4:", uniqueSuggestions);
- const filteredSuggestions = this.applySingleCharFilter(
- blockOriginalText,
- uniqueSuggestions,
- settings.filterSingleCharErrors
- );
- Logger.debug(` \u{1F680} \uCD5C\uC885 \uD544\uD130\uB9C1\uB41C \uC81C\uC548 \uC218: ${filteredSuggestions.length}\uAC1C`);
- Logger.debug(" \uD544\uD130\uB9C1\uB41C \uC81C\uC548\uB4E4:", filteredSuggestions);
- if (filteredSuggestions.length > 0) {
- if (correctionMap.has(blockOriginalText)) {
- Logger.debug(" -> \uAE30\uC874 \uAD50\uC815\uC5D0 \uC81C\uC548 \uCD94\uAC00");
- const existing = correctionMap.get(blockOriginalText);
- Logger.debug(" \u{1F50D} \uC911\uBCF5 \uC81C\uAC70 \uC0C1\uC138 \uBD84\uC11D:");
- Logger.debug(" \uAE30\uC874 \uC81C\uC548\uB4E4:", existing.corrected);
- Logger.debug(" \uC0C8\uB85C\uC6B4 \uC81C\uC548\uB4E4:", filteredSuggestions);
- const newUniqueSuggestions = filteredSuggestions.filter((newSuggestion) => {
- const isDuplicate = existing.corrected.includes(newSuggestion);
- Logger.debug(` "${newSuggestion}" \uC911\uBCF5 \uAC80\uC0AC: ${isDuplicate ? "\uC911\uBCF5\uB428" : "\uACE0\uC720\uD568"}`);
- return !isDuplicate;
- });
- Logger.debug(" -> \uD544\uD130\uB9C1\uB41C \uACE0\uC720 \uC81C\uC548\uB4E4:", newUniqueSuggestions);
- if (newUniqueSuggestions.length > 0) {
- const combinedSuggestions = [...existing.corrected, ...newUniqueSuggestions];
- correctionMap.set(blockOriginalText, {
- ...existing,
- corrected: combinedSuggestions
- });
- Logger.debug(" -> \uC0C8\uB85C\uC6B4 \uACE0\uC720 \uC81C\uC548\uB4E4 \uCD94\uAC00:", newUniqueSuggestions);
- Logger.debug(" -> \uCD5C\uC885 \uD1B5\uD569\uB41C \uC81C\uC548\uB4E4:", combinedSuggestions);
- } else {
- Logger.warn(" \u26A0\uFE0F \uBAA8\uB4E0 \uC81C\uC548\uC774 \uC911\uBCF5\uB418\uC5B4 \uAE30\uC874 \uC720\uC9C0:", existing.corrected);
- Logger.warn(" \u26A0\uFE0F \uC774\uAC83\uC774 \uD234\uD301\uC5D0\uC11C \uC911\uBCF5 \uD45C\uC2DC\uB418\uB294 \uC6D0\uC778\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4!");
- }
- } else {
- Logger.debug(" -> \uC0C8 \uAD50\uC815 \uC0DD\uC131");
- correctionMap.set(blockOriginalText, {
- original: blockOriginalText,
- corrected: filteredSuggestions,
- help: ((_f = block.revisions[0]) == null ? void 0 : _f.comment) || "\uB9DE\uCDA4\uBC95 \uAD50\uC815"
- });
- Logger.debug(" -> \uC0C8 \uAD50\uC815 \uC81C\uC548\uB4E4:", filteredSuggestions);
- }
- } else {
- Logger.debug(" -> \uC720\uD6A8\uD55C \uC81C\uC548\uC774 \uC5C6\uC5B4 \uAC74\uB108\uB700");
- }
- }
- });
- }
- });
- }
- corrections.push(...Array.from(correctionMap.values()));
- Logger.debug("\n=== \uCD5C\uC885 \uAD50\uC815 \uACB0\uACFC ===");
- Logger.debug("\uAD50\uC815 \uB9F5 \uD06C\uAE30:", correctionMap.size);
- Logger.log("\uCD5C\uC885 \uAD50\uC815 \uBC30\uC5F4:", corrections);
- Logger.log("\u{1F4CA} \uCD5C\uC885 \uAD50\uC815 \uC0C1\uC138:");
- Logger.log(` \uCD1D \uAD50\uC815 \uC218: ${corrections.length}\uAC1C`);
- corrections.forEach((correction, index) => {
- const originalCount = correction.corrected.length;
- correction.corrected = [...new Set(correction.corrected)];
- const deduplicatedCount = correction.corrected.length;
- Logger.log(` ${index + 1}. "${correction.original}" \u2192 [${correction.corrected.join(", ")}]`);
- Logger.log(` \uC124\uBA85: ${correction.help}`);
- if (originalCount !== deduplicatedCount) {
- Logger.warn(` \u26A0\uFE0F \uCD5C\uC885 \uC911\uBCF5 \uC81C\uAC70 \uC644\uB8CC: ${originalCount}\uAC1C \u2192 ${deduplicatedCount}\uAC1C`);
- }
- });
- const normalizedSource = originalText.replace(/\s+/g, " ").trim();
- const normalizedResult = resultOutput.replace(/\s+/g, " ").trim();
- const hasSignificantChange = normalizedResult !== normalizedSource;
- const lengthDiff = Math.abs(normalizedSource.length - normalizedResult.length);
- const isMinorChange = lengthDiff <= 2;
- if (corrections.length === 0 && hasSignificantChange && !isMinorChange) {
- Logger.log("\u{1F50D} diff \uB85C\uC9C1 \uC2E4\uD589 \uC870\uAC74:");
- Logger.log(` \uD14D\uC2A4\uD2B8 \uBCC0\uACBD: ${hasSignificantChange}`);
- Logger.log(` \uAE38\uC774 \uCC28\uC774: ${lengthDiff}\uC790`);
- Logger.log(` \uC0AC\uC18C\uD55C \uBCC0\uACBD: ${isMinorChange}`);
- Logger.log("\u2192 \uC138\uBD80 \uC815\uBCF4\uAC00 \uC5C6\uC5B4 diff \uB85C\uC9C1 \uC0AC\uC6A9");
- Logger.debug("\uC6D0\uBCF8 (\uC815\uADDC\uD654):", normalizedSource);
- Logger.debug("\uACB0\uACFC (\uC815\uADDC\uD654):", normalizedResult);
- const words = originalText.split(/(\s+)/);
- const revisedWords = resultOutput.split(/(\s+)/);
- for (let i = 0; i < Math.min(words.length, revisedWords.length); i++) {
- if (words[i] !== revisedWords[i] && words[i].trim() && revisedWords[i].trim()) {
- const wordLengthDiff = Math.abs(words[i].length - revisedWords[i].length);
- if (wordLengthDiff > 0 || words[i].toLowerCase() !== revisedWords[i].toLowerCase()) {
- corrections.push({
- original: words[i],
- corrected: [revisedWords[i]],
- help: "\uC790\uB3D9 \uAD50\uC815\uB428"
- });
- }
- }
- }
- } else if (corrections.length === 0) {
- Logger.log("\u2705 \uB9DE\uCDA4\uBC95 \uC624\uB958 \uC5C6\uC74C - diff \uB85C\uC9C1 \uC2E4\uD589 \uC548\uD568");
- } else if (hasSignificantChange && isMinorChange) {
- Logger.log("\u26A0\uFE0F \uC0AC\uC18C\uD55C \uBCC0\uACBD\uC774\uBBC0\uB85C diff \uB85C\uC9C1 \uAC74\uB108\uB700");
- }
- return { resultOutput, corrections };
- }
- /**
- * 형태소 분석을 활용하여 겹치는 오류를 해결합니다.
- * @param text 원본 텍스트
- * @param corrections 교정 배열
- * @param settings 플러그인 설정
- * @returns 개선된 교정 배열
- */
- async improveCorrectionsWithMorphemes(text, corrections, settings) {
- try {
- Logger.log("\n=== \uD615\uD0DC\uC18C \uBD84\uC11D \uAE30\uBC18 \uAD50\uC815 \uAC1C\uC120 ===");
- const morphemeData = await this.analyzeMorphemes(text, settings);
- Logger.debug("\uD615\uD0DC\uC18C \uBD84\uC11D \uC644\uB8CC:", morphemeData);
- const improvedCorrections = this.groupCorrectionsByMorphemes(corrections, morphemeData, text);
- Logger.debug(`\uAD50\uC815 \uAC1C\uC120 \uACB0\uACFC: ${corrections.length}\uAC1C \u2192 ${improvedCorrections.length}\uAC1C`);
- return improvedCorrections;
- } catch (error) {
- Logger.debug("\uD615\uD0DC\uC18C \uBD84\uC11D \uC2E4\uD328, \uC6D0\uBCF8 \uAD50\uC815 \uC0AC\uC6A9:", error);
- return corrections;
- }
- }
- /**
- * 이미 분석된 형태소 데이터를 사용하여 교정을 개선합니다 (중복 API 호출 방지).
- * @param text 원본 텍스트
- * @param corrections 교정 배열
- * @param settings 플러그인 설정
- * @param morphemeData 이미 분석된 형태소 데이터
- * @returns 개선된 교정 배열
- */
- async improveCorrectionsWithMorphemeData(text, corrections, settings, morphemeData) {
- try {
- Logger.debug("=== \uD615\uD0DC\uC18C \uB370\uC774\uD130 \uAE30\uBC18 \uAD50\uC815 \uAC1C\uC120 (\uC7AC\uC0AC\uC6A9) ===");
- Logger.debug("\uAE30\uC874 \uD615\uD0DC\uC18C \uB370\uC774\uD130 \uC7AC\uC0AC\uC6A9:", morphemeData);
- const improvedCorrections = this.groupCorrectionsByMorphemes(corrections, morphemeData, text);
- Logger.debug(`\uAD50\uC815 \uAC1C\uC120 \uACB0\uACFC (\uC7AC\uC0AC\uC6A9): ${corrections.length}\uAC1C \u2192 ${improvedCorrections.length}\uAC1C`);
- return improvedCorrections;
- } catch (error) {
- Logger.debug("\uD615\uD0DC\uC18C \uB370\uC774\uD130 \uAE30\uBC18 \uAD50\uC815 \uAC1C\uC120 \uC2E4\uD328, \uC6D0\uBCF8 \uAD50\uC815 \uC0AC\uC6A9:", error);
- return corrections;
- }
- }
- /**
- * 형태소 정보를 기반으로 교정을 그룹화합니다.
- * @param corrections 원본 교정 배열
- * @param morphemeData 형태소 분석 결과
- * @param originalText 원본 텍스트
- * @returns 그룹화된 교정 배열
- */
- groupCorrectionsByMorphemes(corrections, morphemeData, originalText) {
- const tokenMap = /* @__PURE__ */ new Map();
- const tokensByPosition = /* @__PURE__ */ new Map();
- morphemeData.sentences.forEach((sentence) => {
- sentence.tokens.forEach((token) => {
- const tokenText = token.text.content;
- const tokenPosition = token.text.beginOffset;
- tokenMap.set(tokenText, token);
- if (!tokensByPosition.has(tokenPosition)) {
- tokensByPosition.set(tokenPosition, []);
- }
- tokensByPosition.get(tokenPosition).push(token);
- });
- });
- Logger.debug("\uD1A0\uD070 \uB9F5:", Array.from(tokenMap.keys()));
- Logger.debug("\uC704\uCE58\uBCC4 \uD1A0\uD070 \uB9F5:", Array.from(tokensByPosition.entries()).map(([pos, tokens]) => `${pos}: [${tokens.map((t) => t.text.content).join(", ")}]`));
- tokenMap.tokensByPosition = tokensByPosition;
- const groupedCorrections = [];
- const processedRanges = /* @__PURE__ */ new Set();
- const processedCorrections = /* @__PURE__ */ new Set();
- corrections.forEach((correction) => {
- if (processedCorrections.has(correction.original)) {
- Logger.debug(`\uC774\uBBF8 \uCC98\uB9AC\uB41C \uAD50\uC815 \uAC74\uB108\uB700: "${correction.original}"`);
- return;
- }
- const correctionPositions = this.findAllPositions(originalText, correction.original);
- Logger.debug(`"${correction.original}" \uC704\uCE58\uB4E4:`, correctionPositions);
- const firstPosition = correctionPositions[0];
- if (firstPosition !== void 0) {
- const rangeKey = `${firstPosition}_${firstPosition + correction.original.length}`;
- if (!processedRanges.has(rangeKey)) {
- const overlappingCorrections = this.findOverlappingCorrections(
- corrections,
- originalText,
- firstPosition,
- correction.original.length
- );
- if (overlappingCorrections.length > 1) {
- Logger.debug(`\uC704\uCE58 ${firstPosition}\uC5D0\uC11C \uACB9\uCE58\uB294 \uAD50\uC815\uB4E4:`, overlappingCorrections.map((c) => c.original));
- const bestCorrection = this.selectBestCorrectionWithTokens(
- overlappingCorrections,
- tokenMap,
- originalText,
- firstPosition
- );
- if (bestCorrection) {
- groupedCorrections.push(bestCorrection);
- Logger.debug(`\uC120\uD0DD\uB41C \uAD50\uC815: "${bestCorrection.original}"`);
- overlappingCorrections.forEach((corr) => {
- processedCorrections.add(corr.original);
- const corrPositions = this.findAllPositions(originalText, corr.original);
- corrPositions.forEach((pos) => {
- const key = `${pos}_${pos + corr.original.length}`;
- processedRanges.add(key);
- });
- });
- }
- } else {
- groupedCorrections.push(correction);
- Logger.debug(`\uB3C5\uB9BD\uC801\uC778 \uAD50\uC815 \uCD94\uAC00: "${correction.original}"`);
- correctionPositions.forEach((pos) => {
- const key = `${pos}_${pos + correction.original.length}`;
- processedRanges.add(key);
- });
- processedCorrections.add(correction.original);
- }
- }
- }
- });
- return groupedCorrections;
- }
- /**
- * 특정 위치에서 겹치는 교정들을 찾습니다.
- * 수정: 실제로 범위가 겹치는 경우만 정확히 감지하도록 개선
- */
- findOverlappingCorrections(corrections, text, position, length) {
- const overlapping = [];
- const endPosition = position + length;
- corrections.forEach((correction) => {
- const positions = this.findAllPositions(text, correction.original);
- positions.forEach((pos) => {
- const corrEnd = pos + correction.original.length;
- const hasOverlap = Math.max(0, Math.min(endPosition, corrEnd) - Math.max(position, pos)) > 0;
- if (hasOverlap) {
- if (!overlapping.some((existing) => existing.original === correction.original)) {
- Logger.debug(`\uACB9\uCE68 \uAC10\uC9C0: "${correction.original}" (\uC704\uCE58 ${pos}-${corrEnd}) \u2194 \uAE30\uC900 (\uC704\uCE58 ${position}-${endPosition})`);
- overlapping.push(correction);
- }
- } else {
- Logger.debug(`\uACB9\uCE68 \uC5C6\uC74C: "${correction.original}" (\uC704\uCE58 ${pos}-${corrEnd}) \u2194 \uAE30\uC900 (\uC704\uCE58 ${position}-${endPosition})`);
- }
- });
- });
- Logger.debug(`\uACB9\uCE58\uB294 \uAD50\uC815 ${overlapping.length}\uAC1C \uBC1C\uACAC:`, overlapping.map((c) => c.original));
- return overlapping;
- }
- /**
- * 토큰 정보를 기반으로 최적의 교정을 선택합니다.
- * 수정: 위치 정보를 고려한 정확한 토큰 매칭
- */
- selectBestCorrectionWithTokens(corrections, tokenMap, text, position) {
- Logger.debug(`\uD1A0\uD070 \uAE30\uBC18 \uAD50\uC815 \uC120\uD0DD \uC2DC\uC791: \uC704\uCE58 ${position}, \uD6C4\uBCF4 ${corrections.length}\uAC1C`);
- for (const correction of corrections) {
- const isTokenBoundary = this.isTokenBoundaryMatch(correction, tokenMap, text, position);
- if (isTokenBoundary) {
- Logger.debug(`\uD1A0\uD070 \uACBD\uACC4 \uC77C\uCE58 \uAD50\uC815 \uC120\uD0DD: "${correction.original}" (\uC704\uCE58 ${position}\uC5D0\uC11C \uD1A0\uD070 \uB2E8\uC704)`);
- return correction;
- }
- }
- const longestCorrections = corrections.filter(
- (c) => c.original.length === Math.max(...corrections.map((corr) => corr.original.length))
- );
- if (longestCorrections.length === 1) {
- Logger.debug(`\uAC00\uC7A5 \uAE34 \uAD50\uC815 \uC120\uD0DD: "${longestCorrections[0].original}" (${longestCorrections[0].original.length}\uAE00\uC790)`);
- return longestCorrections[0];
- }
- Logger.debug(`\uAE30\uBCF8 \uC120\uD0DD (\uB3D9\uC77C \uAE38\uC774 \uC911 \uCCAB \uBC88\uC9F8): "${longestCorrections[0].original}"`);
- return longestCorrections[0];
- }
- /**
- * 교정이 특정 위치에서 토큰 경계와 일치하는지 확인합니다.
- */
- isTokenBoundaryMatch(correction, tokenMap, text, position) {
- const tokensByPosition = tokenMap.tokensByPosition;
- if (tokensByPosition) {
- for (const [tokenPos, tokens] of tokensByPosition.entries()) {
- if (Math.abs(tokenPos - position) <= 2) {
- const matchingToken = tokens.find((token) => token.text.content === correction.original);
- if (matchingToken) {
- Logger.debug(`\uD1A0\uD070 \uACBD\uACC4 \uB9E4\uCE6D \uC131\uACF5 (\uC704\uCE58\uAE30\uBC18): "${correction.original}" \uD1A0\uD070\uC704\uCE58=${tokenPos} \uAD50\uC815\uC704\uCE58=${position}`);
- return true;
- }
- }
- }
- } else {
- for (const [tokenText, token] of tokenMap.entries()) {
- if (tokenText === correction.original) {
- const tokenPosition = token.text.beginOffset;
- if (Math.abs(tokenPosition - position) <= 2) {
- Logger.debug(`\uD1A0\uD070 \uACBD\uACC4 \uB9E4\uCE6D \uC131\uACF5 (\uAE30\uC874\uBC29\uC2DD): "${correction.original}" \uD1A0\uD070\uC704\uCE58=${tokenPosition} \uAD50\uC815\uC704\uCE58=${position}`);
- return true;
- }
- }
- }
- }
- Logger.debug(`\uD1A0\uD070 \uACBD\uACC4 \uB9E4\uCE6D \uC2E4\uD328: "${correction.original}" \uC704\uCE58=${position}`);
- return false;
- }
- /**
- * 텍스트에서 특정 패턴의 모든 위치를 찾습니다.
- */
- findAllPositions(text, pattern) {
- const positions = [];
- let index = text.indexOf(pattern);
- while (index !== -1) {
- positions.push(index);
- index = text.indexOf(pattern, index + 1);
- }
- return positions;
- }
- /**
- * 한 글자 오류 필터링을 적용합니다.
- * @param original 원본 텍스트
- * @param suggestions 수정 제안들
- * @param filterEnabled 필터링 활성화 여부
- * @returns 필터링된 제안들
- */
- applySingleCharFilter(original, suggestions, filterEnabled) {
- if (!filterEnabled) {
- Logger.debug(" \uD55C \uAE00\uC790 \uD544\uD130\uB9C1 \uBE44\uD65C\uC131\uD654\uB428");
- return suggestions;
- }
- if (original.length !== 1) {
- Logger.debug(` \uC6D0\uBCF8\uC774 \uD55C \uAE00\uC790\uAC00 \uC544\uB2D8 (${original.length}\uAE00\uC790): "${original}"`);
- return suggestions;
- }
- Logger.debug(` \uD55C \uAE00\uC790 \uC6D0\uBCF8 \uAC10\uC9C0: "${original}"`);
- const meaningfulSuggestions = suggestions.filter((suggestion) => {
- const exceptions = this.checkSingleCharExceptions(original, suggestion);
- if (exceptions.isException) {
- Logger.debug(` "${suggestion}": \uC608\uC678 \uCC98\uB9AC\uB428 (${exceptions.reason})`);
- return true;
- }
- Logger.debug(` "${suggestion}": \uD55C \uAE00\uC790 \uAD50\uC815\uC73C\uB85C \uD544\uD130\uB9C1\uB428`);
- return false;
- });
- Logger.debug(` \uD544\uD130\uB9C1 \uACB0\uACFC: ${suggestions.length} \u2192 ${meaningfulSuggestions.length}`);
- return meaningfulSuggestions;
- }
- /**
- * 한 글자 교정의 예외 케이스를 확인합니다.
- * @param original 원본 글자
- * @param suggestion 제안 글자
- * @returns 예외 처리 결과
- */
- checkSingleCharExceptions(original, suggestion) {
- var _a;
- if (/[0-9a-zA-Z]/.test(original) && /[가-힣]/.test(suggestion)) {
- return { isException: true, reason: "\uC22B\uC790/\uC601\uBB38 \u2192 \uD55C\uAE00 \uBCC0\uD658" };
- }
- if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(original) && /[가-힣]/.test(suggestion)) {
- return { isException: true, reason: "\uD2B9\uC218\uBB38\uC790 \u2192 \uD55C\uAE00 \uBCC0\uD658" };
- }
- const commonSingleCharCorrections = {
- "\uB418": ["\uB41C", "\uB428", "\uB3FC"],
- // 되/돼 혼용
- "\uB3FC": ["\uB41C", "\uB418"],
- "\uC548": ["\uC54A"],
- // 안/않 혼용
- "\uC54A": ["\uC548"],
- "\uC758": ["\uC5D0", "\uC744", "\uB97C"],
- // 조사 혼용
- "\uC5D0": ["\uC758", "\uC744"],
- "\uC744": ["\uB97C", "\uC758"],
- "\uB97C": ["\uC744", "\uC758"],
- "\uC774": ["\uAC00", "\uD788"],
- // 이/가, 이/히 혼용
- "\uAC00": ["\uC774", "\uAC00"],
- "\uD788": ["\uC774", "\uAC8C"],
- "\uAC8C": ["\uD788", "\uC5D0"]
- };
- if ((_a = commonSingleCharCorrections[original]) == null ? void 0 : _a.includes(suggestion)) {
- return { isException: true, reason: "\uC790\uC8FC \uD2C0\uB9AC\uB294 \uD55C \uAE00\uC790 \uAD50\uC815" };
- }
- if (suggestion.length > 1) {
- return { isException: true, reason: "\uD55C \uAE00\uC790 \u2192 \uC5EC\uB7EC \uAE00\uC790 \uD655\uC7A5" };
- }
- return { isException: false, reason: "\uC77C\uBC18\uC801\uC778 \uD55C \uAE00\uC790 \uAD50\uC815" };
- }
-};
-
-// src/services/cacheService.ts
-init_logger();
-var SpellCheckCacheService = class {
- constructor(maxSize = 1e3, ttlMinutes = 30, cleanupIntervalMinutes = 5) {
- this.cache = /* @__PURE__ */ new Map();
- // 통계 정보
- this.stats = {
- totalRequests: 0,
- cacheHits: 0,
- cacheMisses: 0
- };
- this.maxSize = maxSize;
- this.ttl = ttlMinutes * 60 * 1e3;
- this.cleanupInterval = cleanupIntervalMinutes * 60 * 1e3;
- Logger.debug("SpellCheckCacheService \uCD08\uAE30\uD654:", {
- maxSize,
- ttlMinutes,
- cleanupIntervalMinutes
- });
- this.startCleanupTimer();
- }
- /**
- * 캐시에서 결과를 가져옵니다
- * @param text 검사할 텍스트 (키로 사용)
- * @returns 캐시된 결과 또는 null
- */
- get(text) {
- this.stats.totalRequests++;
- const key = this.generateKey(text);
- const item = this.cache.get(key);
- if (!item) {
- this.stats.cacheMisses++;
- Logger.debug("\uCE90\uC2DC \uBBF8\uC2A4:", { key: key.substring(0, 50) + "..." });
- return null;
- }
- const now = Date.now();
- if (now - item.timestamp > this.ttl) {
- this.cache.delete(key);
- this.stats.cacheMisses++;
- Logger.debug("\uCE90\uC2DC \uB9CC\uB8CC:", { key: key.substring(0, 50) + "..." });
- return null;
- }
- item.accessCount++;
- item.lastAccessed = now;
- this.cache.delete(key);
- this.cache.set(key, item);
- this.stats.cacheHits++;
- Logger.debug("\uCE90\uC2DC \uD788\uD2B8:", {
- key: key.substring(0, 50) + "...",
- accessCount: item.accessCount
- });
- return item.result;
- }
- /**
- * 결과를 캐시에 저장합니다
- * @param text 검사한 텍스트
- * @param result 검사 결과
- */
- set(text, result) {
- const key = this.generateKey(text);
- const now = Date.now();
- if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
- this.evictLeastRecentlyUsed();
- }
- const item = {
- result,
- timestamp: now,
- accessCount: 1,
- lastAccessed: now
- };
- this.cache.set(key, item);
- Logger.debug("\uCE90\uC2DC \uC800\uC7A5:", {
- key: key.substring(0, 50) + "...",
- cacheSize: this.cache.size,
- corrections: result.corrections.length
- });
- }
- /**
- * 캐시 통계를 반환합니다
- */
- getStats() {
- const hitRatio = this.stats.totalRequests > 0 ? this.stats.cacheHits / this.stats.totalRequests * 100 : 0;
- return {
- totalRequests: this.stats.totalRequests,
- cacheHits: this.stats.cacheHits,
- cacheMisses: this.stats.cacheMisses,
- hitRatio: Math.round(hitRatio * 100) / 100,
- cacheSize: this.cache.size,
- memoryUsage: this.estimateMemoryUsage()
- };
- }
- /**
- * 캐시를 완전히 비웁니다
- */
- clear() {
- this.cache.clear();
- this.stats = {
- totalRequests: 0,
- cacheHits: 0,
- cacheMisses: 0
- };
- Logger.debug("\uCE90\uC2DC \uC804\uCCB4 \uC0AD\uC81C \uC644\uB8CC");
- }
- /**
- * 만료된 항목들을 정리합니다
- */
- cleanup() {
- const now = Date.now();
- let removedCount = 0;
- for (const [key, item] of this.cache.entries()) {
- if (now - item.timestamp > this.ttl) {
- this.cache.delete(key);
- removedCount++;
- }
- }
- if (removedCount > 0) {
- Logger.debug("\uCE90\uC2DC \uC815\uB9AC \uC644\uB8CC:", {
- removedCount,
- remainingSize: this.cache.size
- });
- }
- }
- /**
- * 캐시 서비스를 종료하고 정리합니다
- */
- destroy() {
- if (this.cleanupTimer) {
- clearInterval(this.cleanupTimer);
- this.cleanupTimer = void 0;
- }
- this.clear();
- Logger.debug("SpellCheckCacheService \uC885\uB8CC");
- }
- /**
- * 텍스트에서 캐시 키를 생성합니다
- * @param text 원본 텍스트
- * @returns 해시된 키
- */
- generateKey(text) {
- const normalized = text.trim().replace(/\s+/g, " ");
- let hash = 0;
- for (let i = 0; i < normalized.length; i++) {
- const char = normalized.charCodeAt(i);
- hash = (hash << 5) - hash + char;
- hash = hash & hash;
- }
- return `spell_${Math.abs(hash)}_${normalized.length}`;
- }
- /**
- * LRU 정책에 따라 가장 오래된 항목을 제거합니다
- */
- evictLeastRecentlyUsed() {
- let oldestKey = null;
- let oldestTime = Date.now();
- for (const [key, item] of this.cache.entries()) {
- if (item.lastAccessed < oldestTime) {
- oldestTime = item.lastAccessed;
- oldestKey = key;
- }
- }
- if (oldestKey) {
- this.cache.delete(oldestKey);
- Logger.debug("LRU \uC81C\uAC70:", {
- key: oldestKey.substring(0, 50) + "...",
- age: Date.now() - oldestTime
- });
- }
- }
- /**
- * 대략적인 메모리 사용량을 추정합니다 (바이트 단위)
- */
- estimateMemoryUsage() {
- let totalSize = 0;
- for (const [key, item] of this.cache.entries()) {
- totalSize += key.length * 2;
- totalSize += JSON.stringify(item.result).length * 2;
- totalSize += 32;
- }
- return totalSize;
- }
- /**
- * 정리 타이머를 시작합니다
- */
- startCleanupTimer() {
- this.cleanupTimer = setInterval(() => {
- this.cleanup();
- }, this.cleanupInterval);
- }
-};
-
-// src/services/optimizedApiService.ts
-init_errorHandler();
-init_logger();
-var OptimizedSpellCheckService = class {
- constructor(maxBatchSize = 5, batchTimeoutMs = 2e3, requestTimeoutMs = 15e3, maxConcurrentBatches = 3, cacheOptions) {
- this.requestQueue = [];
- this.processing = false;
- this.activeBatches = 0;
- // 성능 메트릭
- this.metrics = {
- totalRequests: 0,
- successfulRequests: 0,
- failedRequests: 0,
- responseTimes: []
- };
- this.apiService = new SpellCheckApiService();
- this.cacheService = new SpellCheckCacheService(
- cacheOptions == null ? void 0 : cacheOptions.maxSize,
- cacheOptions == null ? void 0 : cacheOptions.ttlMinutes,
- cacheOptions == null ? void 0 : cacheOptions.cleanupIntervalMinutes
- );
- this.maxBatchSize = maxBatchSize;
- this.batchTimeout = batchTimeoutMs;
- this.requestTimeout = requestTimeoutMs;
- this.maxConcurrentBatches = maxConcurrentBatches;
- Logger.debug("OptimizedSpellCheckService \uCD08\uAE30\uD654:", {
- maxBatchSize,
- batchTimeoutMs,
- requestTimeoutMs,
- maxConcurrentBatches
- });
- }
- /**
- * 맞춤법 검사를 수행합니다 (최적화된 버전)
- * @param text 검사할 텍스트
- * @param settings 플러그인 설정
- * @param priority 요청 우선순위
- * @returns 검사 결과 Promise
- */
- async checkSpelling(text, settings, priority = "medium") {
- this.metrics.totalRequests++;
- const cachedResult = this.cacheService.get(text);
- if (cachedResult) {
- Logger.debug("\uCE90\uC2DC\uC5D0\uC11C \uACB0\uACFC \uBC18\uD658:", { textLength: text.length });
- return cachedResult;
- }
- if (text.length < 50 || priority === "high") {
- Logger.debug("\uC989\uC2DC \uCC98\uB9AC:", { textLength: text.length, priority });
- return this.processSingleRequest(text, settings);
- }
- return new Promise((resolve, reject) => {
- const item = {
- text,
- resolve,
- reject,
- timestamp: Date.now(),
- priority
- };
- this.insertByPriority(item);
- Logger.debug("\uBC30\uCE58 \uD050\uC5D0 \uCD94\uAC00:", {
- queueLength: this.requestQueue.length,
- priority,
- textLength: text.length
- });
- this.scheduleBatchProcessing(settings);
- });
- }
- /**
- * 서비스 성능 메트릭을 반환합니다
- */
- getMetrics() {
- const avgResponseTime = this.metrics.responseTimes.length > 0 ? this.metrics.responseTimes.reduce((a, b) => a + b, 0) / this.metrics.responseTimes.length : 0;
- return {
- totalRequests: this.metrics.totalRequests,
- successfulRequests: this.metrics.successfulRequests,
- failedRequests: this.metrics.failedRequests,
- averageResponseTime: Math.round(avgResponseTime),
- queueLength: this.requestQueue.length,
- activeBatches: this.activeBatches,
- cache: this.cacheService.getStats(),
- morphemeCache: this.apiService.getMorphemeCacheStats()
- // ⭐ NEW: 형태소 캐시 통계
- };
- }
- /**
- * 캐시를 수동으로 정리합니다
- */
- clearCache() {
- this.cacheService.clear();
- this.apiService.clearMorphemeCache();
- }
- /**
- * 대기 중인 모든 요청을 취소합니다
- */
- cancelPendingRequests() {
- const error = new Error("\uC694\uCCAD\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
- this.requestQueue.forEach((item) => {
- item.reject(error);
- });
- this.requestQueue = [];
- if (this.batchTimer) {
- clearTimeout(this.batchTimer);
- this.batchTimer = void 0;
- }
- Logger.debug("\uB300\uAE30 \uC911\uC778 \uBAA8\uB4E0 \uC694\uCCAD \uCDE8\uC18C\uB428");
- }
- /**
- * 서비스를 종료하고 리소스를 정리합니다
- */
- destroy() {
- this.cancelPendingRequests();
- this.cacheService.destroy();
- Logger.debug("OptimizedSpellCheckService \uC885\uB8CC");
- }
- /**
- * 단일 요청을 즉시 처리합니다 (에러 핸들링 및 재시도 포함)
- */
- async processSingleRequest(text, settings) {
- const startTime = Date.now();
- try {
- const result = await ErrorHandlerService.withRetry(
- async () => {
- return await this.executeWithTimeout(
- () => this.apiService.checkSpelling(text, settings),
- this.requestTimeout
- );
- },
- `spell-check-${text.substring(0, 50)}`,
- {
- maxRetries: 2,
- baseDelay: 1e3,
- maxDelay: 5e3,
- backoffFactor: 2
- }
- );
- this.cacheService.set(text, result);
- const responseTime = Date.now() - startTime;
- this.updateMetrics(true, responseTime);
- return result;
- } catch (error) {
- this.updateMetrics(false, Date.now() - startTime);
- const errorInfo = ErrorHandlerService.handleError(error, "spell-check-api");
- throw error;
- }
- }
- /**
- * 배치 처리를 스케줄링합니다
- */
- scheduleBatchProcessing(settings) {
- if (this.processing || this.requestQueue.length === 0) {
- return;
- }
- if (this.activeBatches >= this.maxConcurrentBatches) {
- Logger.debug("\uCD5C\uB300 \uB3D9\uC2DC \uBC30\uCE58 \uC218 \uB3C4\uB2EC, \uB300\uAE30 \uC911");
- return;
- }
- if (this.requestQueue.length >= this.maxBatchSize) {
- this.processBatch(settings);
- } else if (!this.batchTimer) {
- this.batchTimer = setTimeout(() => {
- this.processBatch(settings);
- }, this.batchTimeout);
- }
- }
- /**
- * 배치를 처리합니다
- */
- async processBatch(settings) {
- if (this.processing || this.requestQueue.length === 0) {
- return;
- }
- this.processing = true;
- this.activeBatches++;
- if (this.batchTimer) {
- clearTimeout(this.batchTimer);
- this.batchTimer = void 0;
- }
- const batch = this.requestQueue.splice(0, this.maxBatchSize);
- Logger.debug("\uBC30\uCE58 \uCC98\uB9AC \uC2DC\uC791:", {
- batchSize: batch.length,
- remainingQueue: this.requestQueue.length,
- activeBatches: this.activeBatches
- });
- try {
- const promises = batch.map(async (item) => {
- try {
- const result = await this.processSingleRequest(item.text, settings);
- item.resolve(result);
- } catch (error) {
- item.reject(error instanceof Error ? error : new Error(String(error)));
- }
- });
- await Promise.allSettled(promises);
- } catch (error) {
- Logger.error("\uBC30\uCE58 \uCC98\uB9AC \uC911 \uC624\uB958:", error);
- batch.forEach((item) => {
- item.reject(error instanceof Error ? error : new Error(String(error)));
- });
- } finally {
- this.processing = false;
- this.activeBatches--;
- Logger.debug("\uBC30\uCE58 \uCC98\uB9AC \uC644\uB8CC:", {
- remainingQueue: this.requestQueue.length,
- activeBatches: this.activeBatches
- });
- if (this.requestQueue.length > 0) {
- setTimeout(() => this.scheduleBatchProcessing(settings), 100);
- }
- }
- }
- /**
- * 우선순위에 따라 큐에 아이템을 삽입합니다
- */
- insertByPriority(item) {
- const priorityOrder = { high: 0, medium: 1, low: 2 };
- const itemPriority = priorityOrder[item.priority];
- let insertIndex = this.requestQueue.length;
- for (let i = 0; i < this.requestQueue.length; i++) {
- const existingPriority = priorityOrder[this.requestQueue[i].priority];
- if (itemPriority < existingPriority) {
- insertIndex = i;
- break;
- }
- }
- this.requestQueue.splice(insertIndex, 0, item);
- }
- /**
- * 타임아웃을 적용한 함수 실행
- */
- async executeWithTimeout(fn, timeoutMs) {
- return new Promise((resolve, reject) => {
- const timer = setTimeout(() => {
- reject(new Error(`\uC694\uCCAD \uD0C0\uC784\uC544\uC6C3 (${timeoutMs}ms)`));
- }, timeoutMs);
- fn().then((result) => {
- clearTimeout(timer);
- resolve(result);
- }).catch((error) => {
- clearTimeout(timer);
- reject(error);
- });
- });
- }
- /**
- * 성능 메트릭을 업데이트합니다
- */
- updateMetrics(success, responseTime) {
- if (success) {
- this.metrics.successfulRequests++;
- } else {
- this.metrics.failedRequests++;
- }
- this.metrics.responseTimes.push(responseTime);
- if (this.metrics.responseTimes.length > 100) {
- this.metrics.responseTimes = this.metrics.responseTimes.slice(-100);
- }
- }
- /**
- * 형태소 분석을 활용하여 겹치는 오류를 해결합니다.
- * @param text 원본 텍스트
- * @param corrections 교정 배열
- * @param settings 플러그인 설정
- * @returns 개선된 교정 배열
- */
- async improveCorrectionsWithMorphemes(text, corrections, settings) {
- return await this.apiService.improveCorrectionsWithMorphemes(text, corrections, settings);
- }
- /**
- * 이미 분석된 형태소 데이터를 사용하여 교정을 개선합니다 (중복 API 호출 방지).
- * @param text 원본 텍스트
- * @param corrections 교정 배열
- * @param settings 플러그인 설정
- * @param morphemeData 이미 분석된 형태소 데이터
- * @returns 개선된 교정 배열
- */
- async improveCorrectionsWithMorphemeData(text, corrections, settings, morphemeData) {
- return await this.apiService.improveCorrectionsWithMorphemeData(text, corrections, settings, morphemeData);
- }
- /**
- * 형태소 분석을 수행합니다.
- */
- async analyzeMorphemes(text, settings) {
- return await this.apiService.analyzeMorphemes(text, settings);
- }
-};
-
-// src/orchestrator.ts
-init_settings();
-
-// src/services/ignoredWords.ts
-var IgnoredWordsService = class {
- /**
- * 단어가 예외 처리되었는지 확인합니다.
- * @param word 확인할 단어
- * @param settings 플러그인 설정
- * @returns 예외 처리 여부
- */
- static isWordIgnored(word, settings) {
- return settings.ignoredWords.includes(word.trim());
- }
- /**
- * 단어를 예외 처리 목록에 추가합니다.
- * @param word 추가할 단어
- * @param settings 플러그인 설정
- * @returns 업데이트된 설정
- */
- static addIgnoredWord(word, settings) {
- const trimmedWord = word.trim();
- if (!trimmedWord || this.isWordIgnored(trimmedWord, settings)) {
- return settings;
- }
- return {
- ...settings,
- ignoredWords: [...settings.ignoredWords, trimmedWord].sort()
- };
- }
- /**
- * 단어를 예외 처리 목록에서 제거합니다.
- * @param word 제거할 단어
- * @param settings 플러그인 설정
- * @returns 업데이트된 설정
- */
- static removeIgnoredWord(word, settings) {
- const trimmedWord = word.trim();
- return {
- ...settings,
- ignoredWords: settings.ignoredWords.filter((w) => w !== trimmedWord)
- };
- }
- /**
- * 예외 처리된 단어 목록을 가져옵니다.
- * @param settings 플러그인 설정
- * @returns 예외 처리된 단어 배열
- */
- static getIgnoredWords(settings) {
- return [...settings.ignoredWords].sort();
- }
- /**
- * 예외 처리된 단어 수를 가져옵니다.
- * @param settings 플러그인 설정
- * @returns 예외 처리된 단어 수
- */
- static getIgnoredWordsCount(settings) {
- return settings.ignoredWords.length;
- }
- /**
- * 모든 예외 처리된 단어를 제거합니다.
- * @param settings 플러그인 설정
- * @returns 업데이트된 설정
- */
- static clearAllIgnoredWords(settings) {
- return {
- ...settings,
- ignoredWords: []
- };
- }
- /**
- * 단어 목록을 일괄 추가합니다.
- * @param words 추가할 단어 배열
- * @param settings 플러그인 설정
- * @returns 업데이트된 설정
- */
- static addMultipleIgnoredWords(words, settings) {
- const newWords = words.map((word) => word.trim()).filter((word) => word && !this.isWordIgnored(word, settings));
- if (newWords.length === 0) {
- return settings;
- }
- return {
- ...settings,
- ignoredWords: [...settings.ignoredWords, ...newWords].sort()
- };
- }
- /**
- * 예외 처리된 단어를 검색합니다.
- * @param query 검색어
- * @param settings 플러그인 설정
- * @returns 검색 결과
- */
- static searchIgnoredWords(query, settings) {
- const trimmedQuery = query.trim().toLowerCase();
- if (!trimmedQuery) {
- return this.getIgnoredWords(settings);
- }
- return settings.ignoredWords.filter((word) => word.toLowerCase().includes(trimmedQuery)).sort();
- }
- /**
- * 예외 처리된 단어를 문자열로 내보냅니다.
- * @param settings 플러그인 설정
- * @param separator 구분자 (기본값: ', ')
- * @returns 예외 처리된 단어들을 구분자로 연결한 문자열
- */
- static exportIgnoredWords(settings, separator = ", ") {
- return this.getIgnoredWords(settings).join(separator);
- }
- /**
- * 문자열에서 예외 처리된 단어를 가져와 일괄 추가합니다.
- * @param wordsString 단어들이 포함된 문자열
- * @param separator 구분자 (기본값: ',')
- * @param settings 플러그인 설정
- * @returns 업데이트된 설정
- */
- static importIgnoredWords(wordsString, separator = ",", settings) {
- const words = wordsString.split(separator).map((word) => word.trim()).filter((word) => word);
- return this.addMultipleIgnoredWords(words, settings);
- }
-};
-
-// src/ui/correctionPopup.ts
-var import_obsidian4 = require("obsidian");
-
-// src/utils/htmlUtils.ts
-var import_obsidian3 = require("obsidian");
-function escapeHtml(text) {
- return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'");
-}
-function safeRemoveElement(element) {
- if (element && element.parentNode) {
- element.parentNode.removeChild(element);
- }
-}
-function addEventListenerWithCleanup(element, event, handler) {
- element.addEventListener(event, handler);
- return () => element.removeEventListener(event, handler);
-}
-
-// src/utils/domUtils.ts
-function parseHTMLSafely(htmlString) {
- var _a;
- const sanitized = (_a = window.sanitizeHTMLToDom) == null ? void 0 : _a.call(window, htmlString);
- const fragment = document.createDocumentFragment();
- if (sanitized) {
- fragment.appendChild(sanitized);
- }
- return fragment;
-}
-function clearElement(element) {
- try {
- while (element.firstChild) {
- const child = element.firstChild;
- if (child.parentNode === element) {
- element.removeChild(child);
- } else {
- break;
- }
- }
- } catch (error) {
- element.textContent = "";
- }
-}
-function createMetricsDisplay(parent, metrics) {
- clearElement(parent);
- const apiSection = parent.createEl("div");
- apiSection.createEl("strong", { text: "API \uC131\uB2A5 \uD1B5\uACC4:" });
- apiSection.createEl("br");
- const apiMetrics = [
- `\uCD1D \uC694\uCCAD: ${metrics.totalRequests}`,
- `\uC131\uACF5 \uC694\uCCAD: ${metrics.successfulRequests}`,
- `\uC2E4\uD328 \uC694\uCCAD: ${metrics.failedRequests}`,
- `\uD3C9\uADE0 \uC751\uB2F5\uC2DC\uAC04: ${metrics.averageResponseTime}ms`,
- `\uB300\uAE30\uC5F4 \uAE38\uC774: ${metrics.queueLength}`,
- `\uD65C\uC131 \uBC30\uCE58: ${metrics.activeBatches}`
- ];
- apiMetrics.forEach((metric) => {
- apiSection.createEl("div", { text: `\u2022 ${metric}` });
- });
- parent.createEl("br");
- if (metrics.cache) {
- const cacheSection = parent.createEl("div");
- cacheSection.createEl("strong", { text: "\uCE90\uC2DC \uD1B5\uACC4:" });
- cacheSection.createEl("br");
- const cacheMetrics = [
- `\uCD1D \uC694\uCCAD: ${metrics.cache.totalRequests}`,
- `\uCE90\uC2DC \uD788\uD2B8: ${metrics.cache.cacheHits}`,
- `\uCE90\uC2DC \uBBF8\uC2A4: ${metrics.cache.cacheMisses}`,
- `\uD788\uD2B8\uC728: ${metrics.cache.hitRatio}%`,
- `\uCE90\uC2DC \uD06C\uAE30: ${metrics.cache.cacheSize}\uAC1C`,
- `\uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9: ${Math.round(metrics.cache.memoryUsage / 1024)}KB`
- ];
- cacheMetrics.forEach((metric) => {
- cacheSection.createEl("div", { text: `\u2022 ${metric}` });
- });
- }
-}
-
-// src/ui/baseComponent.ts
-var BaseComponent = class {
- constructor(tagName = "div", className) {
- this.cleanupFunctions = [];
- this.isDestroyed = false;
- this.element = document.createElement(tagName);
- if (className) {
- this.element.className = className;
- }
- }
- /**
- * 이벤트 리스너를 추가하고 정리 함수를 등록합니다.
- * @param element 대상 요소
- * @param event 이벤트 타입
- * @param handler 이벤트 핸들러
- */
- addEventListener(element, event, handler) {
- const cleanup = addEventListenerWithCleanup(element, event, handler);
- this.cleanupFunctions.push(cleanup);
- }
- /**
- * 컴포넌트의 루트 요소를 반환합니다.
- */
- getElement() {
- return this.element;
- }
- /**
- * 컴포넌트를 DOM에 추가합니다.
- * @param parent 부모 요소
- */
- appendTo(parent) {
- if (!this.isDestroyed) {
- parent.appendChild(this.element);
- }
- }
- /**
- * 컴포넌트를 DOM에서 제거합니다.
- */
- remove() {
- safeRemoveElement(this.element);
- }
- /**
- * 컴포넌트를 완전히 파괴하고 리소스를 정리합니다.
- */
- destroy() {
- if (this.isDestroyed)
- return;
- this.cleanupFunctions.forEach((cleanup) => cleanup());
- this.cleanupFunctions = [];
- this.remove();
- this.isDestroyed = true;
- }
- /**
- * 컴포넌트가 파괴되었는지 확인합니다.
- */
- isComponentDestroyed() {
- return this.isDestroyed;
- }
- /**
- * CSS 클래스를 추가합니다.
- * @param className 클래스명
- */
- addClass(className) {
- this.element.classList.add(className);
- }
- /**
- * CSS 클래스를 제거합니다.
- * @param className 클래스명
- */
- removeClass(className) {
- this.element.classList.remove(className);
- }
- /**
- * CSS 클래스를 토글합니다.
- * @param className 클래스명
- * @param force 강제 설정값
- */
- toggleClass(className, force) {
- this.element.classList.toggle(className, force);
- }
- /**
- * 요소의 내용을 설정합니다.
- * @param content HTML 또는 텍스트 내용
- * @param isHTML HTML 여부 (기본값: false)
- */
- setContent(content, isHTML = false) {
- if (isHTML) {
- clearElement(this.element);
- const fragment = parseHTMLSafely(content);
- this.element.appendChild(fragment);
- } else {
- this.element.textContent = content;
- }
- }
- /**
- * 요소에 속성을 설정합니다.
- * @param name 속성명
- * @param value 속성값
- */
- setAttribute(name, value) {
- this.element.setAttribute(name, value);
- }
- /**
- * 요소의 스타일을 설정합니다.
- * @param property CSS 속성
- * @param value CSS 값
- */
- setStyle(property, value) {
- this.element.style[property] = value;
- }
-};
-
-// src/state/correctionState.ts
-init_logger();
-var CorrectionStateManager = class {
- constructor(corrections, ignoredWords = []) {
- this.states = /* @__PURE__ */ new Map();
- this.corrections = [];
- this.ignoredWords = [];
- this.userEditedValues = /* @__PURE__ */ new Map();
- this.corrections = corrections;
- this.ignoredWords = ignoredWords;
- this.initializeStates(ignoredWords);
- }
- /**
- * 상태를 초기화합니다.
- */
- initializeStates(ignoredWords) {
- this.states.clear();
- this.corrections.forEach((correction, index) => {
- const isOriginalKept = ignoredWords.includes(correction.original);
- this.setState(index, correction.original, false, isOriginalKept);
- Logger.debug(`Initializing: ${correction.original} at index ${index} as ${isOriginalKept ? "ORIGINAL_KEPT" : "ERROR"}.`);
- });
- }
- /**
- * 특정 교정의 상태를 설정합니다.
- * @param correctionIndex 교정 인덱스
- * @param value 설정할 값
- * @param isExceptionState 예외 처리 상태 여부
- * @param isOriginalKept 원본유지 상태 여부
- * @param isUserEdited 사용자 편집 상태 여부
- */
- setState(correctionIndex, value, isExceptionState = false, isOriginalKept = false, isUserEdited = false) {
- var _a;
- const stack = new Error().stack;
- const caller = ((_a = stack == null ? void 0 : stack.split("\n")[2]) == null ? void 0 : _a.trim()) || "unknown";
- Logger.debug(`\u{1F527} setState \uD638\uCD9C\uB428: index=${correctionIndex}, value="${value}", isUserEdited=${isUserEdited}, caller=${caller}`);
- this.states.set(correctionIndex, value);
- const exceptionKey = `${correctionIndex}_exception`;
- const originalKeptKey = `${correctionIndex}_originalKept`;
- const userEditedKey = `${correctionIndex}_userEdited`;
- if (isExceptionState) {
- this.states.set(exceptionKey, true);
- } else {
- this.states.delete(exceptionKey);
- }
- if (isOriginalKept) {
- this.states.set(originalKeptKey, true);
- } else {
- this.states.delete(originalKeptKey);
- }
- if (isUserEdited) {
- this.states.set(userEditedKey, true);
- this.userEditedValues.set(correctionIndex, value);
- Logger.debug(`\u{1F527} setState: \uC0AC\uC6A9\uC790 \uD3B8\uC9D1 \uC0C1\uD0DC \uC124\uC815 - userEditedKey="${userEditedKey}", value="${value}"`);
- } else {
- const wasUserEdited = this.states.has(userEditedKey);
- const existingUserValue = this.userEditedValues.get(correctionIndex);
- this.states.delete(userEditedKey);
- if (wasUserEdited) {
- Logger.debug(`\u{1F527} setState: \uC0AC\uC6A9\uC790 \uD3B8\uC9D1 \uC0C1\uD0DC \uD574\uC81C (\uD3B8\uC9D1\uAC12 \uBCF4\uC874) - userEditedKey="${userEditedKey}", \uBCF4\uC874\uAC12="${existingUserValue}", caller=${caller}`);
- }
- }
- }
- /**
- * 특정 교정의 현재 값을 가져옵니다.
- * @param correctionIndex 교정 인덱스
- * @returns 현재 값
- */
- getValue(correctionIndex) {
- const isUserEdited = this.isUserEditedState(correctionIndex);
- const userEditedValue = this.userEditedValues.get(correctionIndex);
- const statesValue = this.states.get(correctionIndex) || "";
- const finalValue = isUserEdited && userEditedValue !== void 0 ? userEditedValue : statesValue;
- if (isUserEdited && !userEditedValue) {
- Logger.warn(`\u26A0\uFE0F \uC0AC\uC6A9\uC790 \uD3B8\uC9D1 \uC0C1\uD0DC\uC778\uB370 \uD3B8\uC9D1\uAC12\uC774 \uC5C6\uC74C: index=${correctionIndex}`);
- }
- Logger.debug(`getValue(${correctionIndex}): states="${statesValue}", userEdited=${isUserEdited}, userEditedValue="${userEditedValue}", finalValue="${finalValue}"`);
- return finalValue;
- }
- /**
- * 특정 교정이 예외 처리 상태인지 확인합니다.
- * @param correctionIndex 교정 인덱스
- * @returns 예외 처리 상태 여부
- */
- isExceptionState(correctionIndex) {
- const exceptionKey = `${correctionIndex}_exception`;
- return !!this.states.get(exceptionKey);
- }
- /**
- * 특정 교정이 원본유지 상태인지 확인합니다.
- * @param correctionIndex 교정 인덱스
- * @returns 원본유지 상태 여부
- */
- isOriginalKeptState(correctionIndex) {
- const originalKeptKey = `${correctionIndex}_originalKept`;
- return !!this.states.get(originalKeptKey);
- }
- /**
- * 특정 교정이 사용자 편집 상태인지 확인합니다.
- * @param correctionIndex 교정 인덱스
- * @returns 사용자 편집 상태 여부
- */
- isUserEditedState(correctionIndex) {
- const userEditedKey = `${correctionIndex}_userEdited`;
- return !!this.states.get(userEditedKey);
- }
- /**
- * 사용자 편집된 값을 설정합니다.
- * @param correctionIndex 교정 인덱스
- * @param userValue 사용자가 입력한 값
- */
- setUserEdited(correctionIndex, userValue) {
- Logger.debug(`\u{1F527} setUserEdited \uD638\uCD9C: index=${correctionIndex}, value="${userValue}"`);
- const beforeStates = this.states.get(correctionIndex);
- const beforeUserEdited = this.isUserEditedState(correctionIndex);
- const beforeUserValue = this.userEditedValues.get(correctionIndex);
- Logger.debug(`\u{1F527} Before setState: states="${beforeStates}", userEdited=${beforeUserEdited}, userValue="${beforeUserValue}"`);
- this.setState(correctionIndex, userValue, false, false, true);
- Logger.debug(`\u{1F527} \uC0AC\uC6A9\uC790 \uD3B8\uC9D1\uC740 \uB3D9\uAE30\uD654\uD558\uC9C0 \uC54A\uC74C - \uAC1C\uBCC4 \uD56D\uBAA9\uB9CC \uC801\uC6A9`);
- const afterStates = this.states.get(correctionIndex);
- const afterUserEdited = this.isUserEditedState(correctionIndex);
- const afterUserValue = this.userEditedValues.get(correctionIndex);
- Logger.debug(`\u{1F527} After setState: states="${afterStates}", userEdited=${afterUserEdited}, userValue="${afterUserValue}"`);
- }
- /**
- * 특정 단어가 초기에 무시된 단어인지 확인합니다.
- * @param word 확인할 단어
- * @returns 초기에 무시된 단어 여부
- */
- isInitiallyIgnoredWord(word) {
- return this.ignoredWords.includes(word);
- }
- /**
- * 5단계 토글을 수행합니다.
- * - 오류(빨간색) → 수정1, 수정2...(초록색) → 예외처리(파란색) → 원본유지(주황색) → 사용자편집(보라색) → 오류(빨간색)
- * @param correctionIndex 교정 인덱스
- * @returns 새로운 상태 정보
- */
- toggleState(correctionIndex) {
- Logger.debug(`\u{1F504} toggleState \uD638\uCD9C\uB428! correctionIndex: ${correctionIndex}`);
- if (correctionIndex < 0 || correctionIndex >= this.corrections.length) {
- throw new Error(`Invalid correction index: ${correctionIndex}`);
- }
- const correction = this.corrections[correctionIndex];
- const suggestions = [correction.original, ...correction.corrected];
- const currentValue = this.getValue(correctionIndex);
- const isCurrentlyException = this.isExceptionState(correctionIndex);
- const isCurrentlyOriginalKept = this.isOriginalKeptState(correctionIndex);
- const isCurrentlyUserEdited = this.isUserEditedState(correctionIndex);
- Logger.debug("toggleState Initial state:", {
- correctionIndex,
- currentValue,
- isCurrentlyException,
- isCurrentlyOriginalKept,
- isCurrentlyUserEdited,
- originalText: correction.original,
- suggestions
- });
- let newValue;
- let newIsException;
- let newIsOriginalKept;
- let newIsUserEdited;
- Logger.debug(`\u{1F50D} toggleState \uBD84\uAE30 \uC9C4\uB2E8: isCurrentlyUserEdited=${isCurrentlyUserEdited}, isCurrentlyOriginalKept=${isCurrentlyOriginalKept}, isCurrentlyException=${isCurrentlyException}`);
- if (isCurrentlyUserEdited) {
- Logger.debug("\u{1F504} toggleState \uBD84\uAE30 1 \uC9C4\uC785: UserEdited -> Error");
- newValue = correction.original;
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = false;
- Logger.debug("toggleState UserEdited -> Error");
- } else if (isCurrentlyOriginalKept) {
- Logger.debug("\u{1F504} toggleState \uBD84\uAE30 2 \uC9C4\uC785: OriginalKept -> ?");
- const userEditedValue = this.userEditedValues.get(correctionIndex);
- if (userEditedValue) {
- newValue = userEditedValue;
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = true;
- Logger.debug(`toggleState OriginalKept -> UserEdited: userEditedValue="${userEditedValue}"`);
- } else {
- newValue = correction.original;
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = false;
- Logger.debug("toggleState OriginalKept -> Error (\uD3B8\uC9D1\uAC12 \uC5C6\uC74C, \uC0AC\uC6A9\uC790\uD3B8\uC9D1 \uAC74\uB108\uB700)");
- }
- } else if (isCurrentlyException) {
- Logger.debug("\u{1F504} toggleState \uBD84\uAE30 3 \uC9C4\uC785: Exception -> OriginalKept");
- newValue = correction.original;
- newIsException = false;
- newIsOriginalKept = true;
- newIsUserEdited = false;
- Logger.debug("toggleState Exception -> OriginalKept");
- } else {
- Logger.debug("\u{1F504} toggleState \uBD84\uAE30 4 \uC9C4\uC785: \uC81C\uC548 \uC21C\uD658 \uB85C\uC9C1");
- let nextIndex = suggestions.indexOf(currentValue) + 1;
- if (nextIndex >= suggestions.length) {
- newValue = correction.original;
- newIsException = true;
- newIsOriginalKept = false;
- newIsUserEdited = false;
- Logger.debug("toggleState Last Suggestion -> Exception");
- } else {
- newValue = suggestions[nextIndex];
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = false;
- Logger.debug("toggleState Next Suggestion:", newValue);
- }
- }
- if (isCurrentlyUserEdited || newIsUserEdited) {
- this.setState(correctionIndex, newValue, newIsException, newIsOriginalKept, newIsUserEdited);
- Logger.debug(`\uC0AC\uC6A9\uC790 \uD3B8\uC9D1 \uAD00\uB828 \uC0C1\uD0DC \uBCC0\uD654\uB294 \uAC1C\uBCC4 \uC801\uC6A9\uB9CC \uC218\uD589: index ${correctionIndex}, from=${isCurrentlyUserEdited} to=${newIsUserEdited}`);
- } else {
- this.syncSameWordStates(correction.original, newValue, newIsException, newIsOriginalKept, newIsUserEdited, correctionIndex);
- }
- return { value: newValue, isExceptionState: newIsException };
- }
- /**
- * 5단계 역방향 토글을 수행합니다.
- * - 오류(빨간색) → 사용자편집(보라색) → 원본유지(주황색) → 예외처리(파란색) → 수정N, 수정1(초록색) → 오류(빨간색)
- * @param correctionIndex 교정 인덱스
- * @returns 새로운 상태 정보
- */
- toggleStatePrev(correctionIndex) {
- Logger.debug(`\u{1F504} toggleStatePrev \uD638\uCD9C\uB428! correctionIndex: ${correctionIndex}`);
- if (correctionIndex < 0 || correctionIndex >= this.corrections.length) {
- throw new Error(`Invalid correction index: ${correctionIndex}`);
- }
- const correction = this.corrections[correctionIndex];
- const suggestions = [correction.original, ...correction.corrected];
- const currentValue = this.getValue(correctionIndex);
- const isCurrentlyException = this.isExceptionState(correctionIndex);
- const isCurrentlyOriginalKept = this.isOriginalKeptState(correctionIndex);
- const isCurrentlyUserEdited = this.isUserEditedState(correctionIndex);
- Logger.debug("toggleStatePrev Initial state:", {
- correctionIndex,
- currentValue,
- isCurrentlyException,
- isCurrentlyOriginalKept,
- isCurrentlyUserEdited,
- originalText: correction.original,
- suggestions
- });
- let newValue;
- let newIsException;
- let newIsOriginalKept;
- let newIsUserEdited;
- if (currentValue === correction.original && !isCurrentlyException && !isCurrentlyOriginalKept && !isCurrentlyUserEdited) {
- const userEditedValue = this.userEditedValues.get(correctionIndex);
- if (userEditedValue) {
- newValue = userEditedValue;
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = true;
- Logger.debug(`toggleStatePrev Error -> UserEdited: userEditedValue="${userEditedValue}"`);
- } else {
- newValue = correction.original;
- newIsException = false;
- newIsOriginalKept = true;
- newIsUserEdited = false;
- Logger.debug("toggleStatePrev Error -> OriginalKept (\uD3B8\uC9D1\uAC12 \uC5C6\uC74C, \uC0AC\uC6A9\uC790\uD3B8\uC9D1 \uAC74\uB108\uB700)");
- }
- } else if (isCurrentlyUserEdited) {
- newValue = correction.original;
- newIsException = false;
- newIsOriginalKept = true;
- newIsUserEdited = false;
- Logger.debug("toggleStatePrev UserEdited -> OriginalKept");
- } else if (isCurrentlyOriginalKept) {
- newValue = correction.original;
- newIsException = true;
- newIsOriginalKept = false;
- newIsUserEdited = false;
- Logger.debug("toggleStatePrev OriginalKept -> Exception");
- } else if (isCurrentlyException) {
- if (correction.corrected.length > 0) {
- newValue = correction.corrected[correction.corrected.length - 1];
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = false;
- Logger.debug("toggleStatePrev Exception -> Last Suggestion");
- } else {
- const userEditedValue = this.userEditedValues.get(correctionIndex);
- if (userEditedValue) {
- newValue = userEditedValue;
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = true;
- Logger.debug(`toggleStatePrev Exception -> UserEdited (no suggestions): userEditedValue="${userEditedValue}"`);
- } else {
- newValue = correction.original;
- newIsException = false;
- newIsOriginalKept = true;
- newIsUserEdited = false;
- Logger.debug("toggleStatePrev Exception -> OriginalKept (no suggestions, \uD3B8\uC9D1\uAC12 \uC5C6\uC74C)");
- }
- }
- } else {
- let currentIndex = suggestions.indexOf(currentValue);
- let prevIndex = currentIndex - 1;
- if (prevIndex < 0) {
- newValue = correction.original;
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = false;
- Logger.debug("toggleStatePrev First Suggestion -> Error");
- } else {
- newValue = suggestions[prevIndex];
- newIsException = false;
- newIsOriginalKept = false;
- newIsUserEdited = false;
- Logger.debug("toggleStatePrev Previous Suggestion:", newValue);
- }
- }
- if (isCurrentlyUserEdited || newIsUserEdited) {
- this.setState(correctionIndex, newValue, newIsException, newIsOriginalKept, newIsUserEdited);
- Logger.debug(`\uC0AC\uC6A9\uC790 \uD3B8\uC9D1 \uAD00\uB828 \uC0C1\uD0DC \uBCC0\uD654\uB294 \uAC1C\uBCC4 \uC801\uC6A9\uB9CC \uC218\uD589: index ${correctionIndex}, from=${isCurrentlyUserEdited} to=${newIsUserEdited}`);
- } else {
- this.syncSameWordStates(correction.original, newValue, newIsException, newIsOriginalKept, newIsUserEdited, correctionIndex);
- }
- return { value: newValue, isExceptionState: newIsException };
- }
- /**
- * 같은 원본 텍스트를 가진 모든 교정 항목의 상태를 동기화합니다.
- * @param originalText 원본 텍스트
- * @param newValue 새로운 값
- * @param isException 예외 처리 상태
- * @param isOriginalKept 원본 유지 상태
- * @param isUserEdited 사용자 편집 상태
- */
- syncSameWordStates(originalText, newValue, isException, isOriginalKept, isUserEdited = false, currentCorrectionIndex) {
- let syncedCount = 0;
- const coreWord = this.extractCoreWord(originalText);
- Logger.debug(`=== \uB3D9\uAE30\uD654 \uC2DC\uC791 ===`);
- Logger.debug(`\uC6D0\uBCF8: "${originalText}", \uD575\uC2EC: "${coreWord}"`);
- Logger.debug(`\uC804\uCCB4 \uAD50\uC815 \uAC1C\uC218: ${this.corrections.length}`);
- for (let i = 0; i < this.corrections.length; i++) {
- const targetOriginal = this.corrections[i].original;
- const targetCoreWord = this.extractCoreWord(targetOriginal);
- Logger.debug(`\uAD50\uC815 ${i}: "${targetOriginal}" \u2192 \uD575\uC2EC: "${targetCoreWord}"`);
- if (targetCoreWord === coreWord) {
- const existingUserEdited = this.isUserEditedState(i);
- if (existingUserEdited && i !== currentCorrectionIndex) {
- Logger.debug(` \u2192 \uB9E4\uCE58\uD558\uC9C0\uB9CC \uAE30\uC874 \uC0AC\uC6A9\uC790 \uD3B8\uC9D1 \uC0C1\uD0DC \uC720\uC9C0 (index ${i})`);
- } else if (isUserEdited && i !== currentCorrectionIndex) {
- Logger.debug(` \u2192 \uB9E4\uCE58\uD558\uC9C0\uB9CC \uC0AC\uC6A9\uC790 \uD3B8\uC9D1\uC740 \uAC1C\uBCC4 \uD56D\uBAA9\uB9CC \uC801\uC6A9 (index ${i})`);
- } else {
- const shouldPreserveUserEdited = existingUserEdited && !isUserEdited;
- const finalIsUserEdited = shouldPreserveUserEdited ? true : isUserEdited;
- const finalValue = shouldPreserveUserEdited ? this.userEditedValues.get(i) || newValue : newValue;
- Logger.debug(` \u2192 \uB9E4\uCE58! \uB3D9\uAE30\uD654 \uC2E4\uD589 (index ${i}), preserveUserEdited=${shouldPreserveUserEdited}, finalIsUserEdited=${finalIsUserEdited}, finalValue="${finalValue}"`);
- this.setState(i, finalValue, isException, isOriginalKept, finalIsUserEdited);
- syncedCount++;
- }
- } else {
- Logger.debug(` \u2192 \uB9E4\uCE58 \uC548\uB428 ("${targetCoreWord}" \u2260 "${coreWord}")`);
- }
- }
- Logger.debug(`\uAC19\uC740 \uB2E8\uC5B4 \uC77C\uAD04 \uC2DC\uAC01\uC801 \uC5C5\uB370\uC774\uD2B8: "${originalText}" (\uD575\uC2EC: "${coreWord}") \u2192 "${newValue}" (${syncedCount}\uAC1C \uD56D\uBAA9)`);
- }
- /**
- * 텍스트에서 핵심 단어를 추출합니다.
- * @param text 원본 텍스트
- * @returns 핵심 단어
- */
- extractCoreWord(text) {
- let coreWord = text.replace(/\([^)]*\)/g, "");
- const particles = ["\uC740", "\uB294", "\uC774", "\uAC00", "\uC744", "\uB97C", "\uC5D0", "\uC5D0\uC11C", "\uB85C", "\uC73C\uB85C", "\uC640", "\uACFC", "\uB3C4", "\uB9CC", "\uAE4C\uC9C0", "\uBD80\uD130", "\uCC98\uB7FC", "\uAC19\uC774", "\uBCF4\uB2E4", "\uB9C8\uB2E4", "\uC870\uCC28", "\uB9C8\uC800", "\uB77C\uB3C4", "\uB098\uB9C8", "\uC774\uB098", "\uAC70\uB098"];
- for (const particle of particles) {
- if (coreWord.endsWith(particle)) {
- coreWord = coreWord.slice(0, -particle.length);
- break;
- }
- }
- coreWord = coreWord.trim();
- Logger.debug(`\uD575\uC2EC \uB2E8\uC5B4 \uCD94\uCD9C: "${text}" \u2192 "${coreWord}"`);
- return coreWord;
- }
- /**
- * 특정 교정의 표시 클래스를 가져옵니다.
- * @param correctionIndex 교정 인덱스
- * @returns CSS 클래스명
- */
- getDisplayClass(correctionIndex) {
- const correction = this.corrections[correctionIndex];
- if (!correction)
- return "";
- if (this.isUserEditedState(correctionIndex)) {
- Logger.debug(`DisplayClass for ${correction.original} (index ${correctionIndex}): spell-user-edited`);
- return "spell-user-edited";
- }
- if (this.isOriginalKeptState(correctionIndex)) {
- Logger.debug(`DisplayClass for ${correction.original} (index ${correctionIndex}): spell-original-kept`);
- return "spell-original-kept";
- }
- const currentValue = this.getValue(correctionIndex);
- const isException = this.isExceptionState(correctionIndex);
- if (currentValue === correction.original) {
- const className = isException ? "spell-exception-processed" : "spell-error";
- Logger.debug(`DisplayClass for ${correction.original} (index ${correctionIndex}): ${className}`);
- return className;
- } else {
- Logger.debug(`DisplayClass for ${correction.original} (index ${correctionIndex}): spell-corrected`);
- return "spell-corrected";
- }
- }
- /**
- * 특정 값이 선택되었는지 확인합니다 (오류 카드용).
- * @param correctionIndex 교정 인덱스
- * @param value 확인할 값
- * @returns 선택 여부
- */
- isSelected(correctionIndex, value) {
- const correction = this.corrections[correctionIndex];
- if (!correction)
- return false;
- const currentValue = this.getValue(correctionIndex);
- const isException = this.isExceptionState(correctionIndex);
- if (value === correction.original) {
- return currentValue === correction.original && isException;
- } else {
- return currentValue === value && !isException;
- }
- }
- /**
- * 모든 상태를 가져옵니다.
- * @returns 상태 맵
- */
- getAllStates() {
- const allStates = {};
- for (let i = 0; i < this.corrections.length; i++) {
- const correction = this.corrections[i];
- const value = this.getValue(i);
- let state;
- if (this.isUserEditedState(i)) {
- state = "user-edited";
- } else if (this.isOriginalKeptState(i)) {
- state = "original-kept";
- } else if (this.isExceptionState(i)) {
- state = "exception-processed";
- } else if (value !== correction.original) {
- state = "corrected";
- } else {
- state = "error";
- }
- allStates[i] = { state, value };
- }
- return allStates;
- }
- /**
- * 최종 적용할 텍스트를 생성합니다.
- * @param originalText 원본 텍스트
- * @returns 교정이 적용된 텍스트와 예외 처리된 단어들
- */
- applyCorrections(originalText) {
- let finalText = originalText;
- const exceptionWords = [];
- Logger.log("\u{1F527} applyCorrections \uC2DC\uC791:", {
- originalTextLength: originalText.length,
- correctionsCount: this.corrections.length,
- originalPreview: originalText.substring(0, 100) + (originalText.length > 100 ? "..." : "")
- });
- for (let i = this.corrections.length - 1; i >= 0; i--) {
- const correction = this.corrections[i];
- const selectedValue = this.getValue(i);
- const isException = this.isExceptionState(i);
- const isUserEdited = this.isUserEditedState(i);
- Logger.debug(`\u{1F527} \uAD50\uC815 \uCC98\uB9AC [${i}]: "${correction.original}" \u2192 "${selectedValue}" (userEdited=${isUserEdited}, exception=${isException})`);
- if (isException) {
- if (!exceptionWords.includes(correction.original)) {
- exceptionWords.push(correction.original);
- }
- Logger.debug(`\u{1F527} \uC608\uC678\uCC98\uB9AC\uB85C \uCD94\uAC00: "${correction.original}"`);
- } else if (selectedValue !== correction.original) {
- Logger.log(`\u{1F527} \uD14D\uC2A4\uD2B8 \uAD50\uCCB4 \uC2E4\uD589: "${correction.original}" \u2192 "${selectedValue}" (userEdited=${isUserEdited})`);
- const beforeReplace = finalText;
- finalText = this.replaceAllOccurrences(finalText, correction.original, selectedValue);
- const changed = beforeReplace !== finalText;
- Logger.debug(`\u{1F527} \uAD50\uCCB4 \uACB0\uACFC: \uBCC0\uACBD\uB428=${changed}, \uD14D\uC2A4\uD2B8\uAE38\uC774 ${beforeReplace.length} \u2192 ${finalText.length}`);
- } else {
- Logger.debug(`\u{1F527} \uAD50\uC815 \uAC74\uB108\uB700: \uC6D0\uBCF8\uACFC \uB3D9\uC77C\uD558\uAC70\uB098 \uC608\uC678\uCC98\uB9AC\uB428`);
- }
- }
- Logger.log("\u{1F527} applyCorrections \uC644\uB8CC:", {
- finalTextLength: finalText.length,
- exceptionWordsCount: exceptionWords.length,
- changed: originalText !== finalText,
- finalPreview: finalText.substring(0, 100) + (finalText.length > 100 ? "..." : "")
- });
- return { finalText, exceptionWords };
- }
- /**
- * 텍스트에서 모든 발생 위치를 안전하게 교체합니다.
- * @param text 대상 텍스트
- * @param original 원본 문자열
- * @param replacement 교체할 문자열
- * @returns 교체된 텍스트
- */
- replaceAllOccurrences(text, original, replacement) {
- const escapedOriginal = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- const regex = new RegExp(escapedOriginal, "g");
- const result = text.replace(regex, replacement);
- const occurrences = (text.match(regex) || []).length;
- if (occurrences > 0) {
- Logger.debug(`\uC77C\uAD04 \uC218\uC815: "${original}" \u2192 "${replacement}" (${occurrences}\uAC1C \uC704\uCE58)`);
- }
- return result;
- }
- /**
- * 예외 처리된 단어 목록을 가져옵니다.
- * @returns 예외 처리된 단어 배열
- */
- getExceptionWords() {
- const exceptionWords = [];
- for (let i = 0; i < this.corrections.length; i++) {
- if (this.isExceptionState(i)) {
- const correction = this.corrections[i];
- if (!exceptionWords.includes(correction.original)) {
- exceptionWords.push(correction.original);
- }
- }
- }
- return exceptionWords;
- }
- /**
- * 최종 교정된 텍스트를 가져옵니다.
- * @param originalText 원본 텍스트
- * @returns 교정된 텍스트
- */
- getFinalText(originalText) {
- const { finalText } = this.applyCorrections(originalText);
- return finalText;
- }
- /**
- * 사용자가 편집한 값들을 가져옵니다.
- * @returns 사용자 편집 값들의 맵
- */
- getUserEditedValues() {
- return new Map(this.userEditedValues);
- }
- /**
- * 디버그 정보를 가져옵니다.
- * @returns 디버그 정보 객체
- */
- getDebugInfo() {
- let exceptionStatesCount = 0;
- let originalKeptStatesCount = 0;
- for (let i = 0; i < this.corrections.length; i++) {
- if (this.isExceptionState(i)) {
- exceptionStatesCount++;
- }
- if (this.isOriginalKeptState(i)) {
- originalKeptStatesCount++;
- }
- }
- return {
- correctionCount: this.corrections.length,
- statesCount: this.states.size,
- exceptionStatesCount,
- originalKeptStatesCount,
- userEditedValuesCount: this.userEditedValues.size,
- ignoredWordsCount: this.ignoredWords.length
- };
- }
-};
-
-// src/utils/textUtils.ts
-init_logger();
-function findOptimalBreakPoint(text, targetLength, tolerance = 200) {
- if (text.length <= targetLength)
- return text.length;
- const minLength = Math.max(0, targetLength - tolerance);
- const maxLength = Math.min(text.length, targetLength + tolerance);
- const patterns = [
- { regex: /[.!?]\s+/g, score: 100 },
- // 문장 끝 + 공백
- { regex: /[.!?]$/g, score: 95 },
- // 문장 끝 (텍스트 마지막)
- { regex: /[.!?]/g, score: 90 },
- // 문장 끝
- { regex: /[,;]\s+/g, score: 80 },
- // 쉼표, 세미콜론 + 공백
- { regex: /\n\s*/g, score: 85 },
- // 줄바꿈
- { regex: /\s{2,}/g, score: 70 },
- // 여러 공백
- { regex: /\s+/g, score: 60 }
- // 단일 공백
- ];
- let bestBreakPoint = targetLength;
- let bestScore = -1;
- for (const pattern of patterns) {
- const matches = Array.from(text.matchAll(pattern.regex));
- for (const match of matches) {
- const endIndex = match.index + match[0].length;
- if (endIndex >= minLength && endIndex <= maxLength) {
- const distanceFromTarget = Math.abs(endIndex - targetLength);
- const distanceScore = Math.max(0, 50 - distanceFromTarget);
- const totalScore = pattern.score + distanceScore;
- if (totalScore > bestScore) {
- bestScore = totalScore;
- bestBreakPoint = endIndex;
- }
- }
- }
- }
- return bestBreakPoint;
-}
-function splitTextIntoPages(text, charsPerPage) {
- if (text.length <= charsPerPage) {
- return [text.length];
- }
- const pageBreaks = [];
- let currentPosition = 0;
- while (currentPosition < text.length) {
- const remainingText = text.slice(currentPosition);
- if (remainingText.length <= charsPerPage) {
- pageBreaks.push(text.length);
- break;
- }
- const breakPoint = findOptimalBreakPoint(remainingText, charsPerPage);
- const absoluteBreakPoint = currentPosition + breakPoint;
- pageBreaks.push(absoluteBreakPoint);
- currentPosition = absoluteBreakPoint;
- }
- return pageBreaks;
-}
-function calculateDynamicCharsPerPage(previewElement, isErrorExpanded = false) {
- if (!previewElement) {
- Logger.debug("No previewElement, returning default 800.");
- return 800;
- }
- const previewRect = previewElement.getBoundingClientRect();
- const availableHeight = previewRect.height;
- const avgCharsPerLine = 75;
- const lineHeight = 15 * 1.7;
- const linesPerPage = Math.floor(availableHeight / lineHeight);
- let calculatedChars;
- if (isErrorExpanded) {
- calculatedChars = Math.max(500, Math.min(1e3, linesPerPage * avgCharsPerLine));
- } else {
- calculatedChars = Math.max(800, Math.min(1800, linesPerPage * avgCharsPerLine));
- }
- Logger.debug(`Available height: ${availableHeight}, Lines per page: ${linesPerPage}, Calculated chars: ${calculatedChars}, Error expanded: ${isErrorExpanded}`);
- return calculatedChars;
-}
-function getCurrentParagraph(editor) {
- const cursor = editor.getCursor();
- const currentLine = cursor.line;
- const totalLines = editor.lineCount();
- Logger.debug(`\uBB38\uB2E8 \uAC10\uC9C0 \uC2DC\uC791: \uD604\uC7AC \uB77C\uC778 ${currentLine}, \uCD1D \uB77C\uC778 ${totalLines}`);
- const currentWord = editor.wordAt(cursor);
- if (currentWord) {
- Logger.debug(`\uD604\uC7AC \uB2E8\uC5B4 \uBC94\uC704: ${currentWord.from.line}:${currentWord.from.ch} - ${currentWord.to.line}:${currentWord.to.ch}`);
- }
- let startLine = currentLine;
- let endLine = currentLine;
- while (startLine > 0) {
- const prevLine = editor.getLine(startLine - 1);
- if (prevLine.trim() === "") {
- break;
- }
- if (prevLine.startsWith("#")) {
- break;
- }
- if (prevLine.match(/^[\s]*[-*+]\s/) || prevLine.match(/^[\s]*\d+\.\s/)) {
- break;
- }
- if (prevLine.startsWith("```")) {
- break;
- }
- startLine--;
- }
- while (endLine < totalLines - 1) {
- const nextLine = editor.getLine(endLine + 1);
- if (nextLine.trim() === "") {
- break;
- }
- if (nextLine.startsWith("#")) {
- break;
- }
- if (nextLine.match(/^[\s]*[-*+]\s/) || nextLine.match(/^[\s]*\d+\.\s/)) {
- break;
- }
- if (nextLine.startsWith("```")) {
- break;
- }
- endLine++;
- }
- const from = { line: startLine, ch: 0 };
- const to = { line: endLine, ch: editor.getLine(endLine).length };
- const text = editor.getRange(from, to);
- Logger.debug(`\uBB38\uB2E8 \uAC10\uC9C0 \uC644\uB8CC: ${startLine}\uD589-${endLine}\uD589 (${text.length}\uC790)`);
- Logger.debug(`\uBB38\uB2E8 \uB0B4\uC6A9 \uBBF8\uB9AC\uBCF4\uAE30: "${text.substring(0, 50)}${text.length > 50 ? "..." : ""}"`);
- return { text, from, to };
-}
-function getCurrentWord(editor) {
- const cursor = editor.getCursor();
- const wordRange = editor.wordAt(cursor);
- if (!wordRange) {
- return null;
- }
- const text = editor.getRange(wordRange.from, wordRange.to);
- Logger.debug(`\uD604\uC7AC \uB2E8\uC5B4 \uAC10\uC9C0: "${text}" (${wordRange.from.line}:${wordRange.from.ch} - ${wordRange.to.line}:${wordRange.to.ch})`);
- return {
- text,
- from: wordRange.from,
- to: wordRange.to
- };
-}
-function getCurrentSentence(editor) {
- const cursor = editor.getCursor();
- const currentLine = cursor.line;
- const currentChar = cursor.ch;
- const currentLineText = editor.getLine(currentLine);
- Logger.debug(`\uBB38\uC7A5 \uAC10\uC9C0 \uC2DC\uC791: ${currentLine}\uD589 ${currentChar}\uC5F4`);
- const sentenceEndPattern = /[.!?。!?]/;
- const sentenceEndPatternGlobal = /[.!?。!?]/g;
- let sentenceStart = 0;
- let sentenceStartLine = currentLine;
- let sentenceStartChar = 0;
- for (let i = currentChar - 1; i >= 0; i--) {
- if (sentenceEndPattern.test(currentLineText[i])) {
- sentenceStartChar = i + 1;
- break;
- }
- }
- if (sentenceStartChar === 0) {
- let searchLine = currentLine - 1;
- while (searchLine >= 0) {
- const lineText = editor.getLine(searchLine);
- if (lineText.trim() === "") {
- sentenceStartLine = searchLine + 1;
- sentenceStartChar = 0;
- break;
- }
- if (lineText.startsWith("#") || lineText.match(/^[\s]*[-*+]\s/) || lineText.match(/^[\s]*\d+\.\s/)) {
- sentenceStartLine = searchLine + 1;
- sentenceStartChar = 0;
- break;
- }
- const matches = Array.from(lineText.matchAll(sentenceEndPatternGlobal));
- if (matches.length > 0) {
- const lastMatch = matches[matches.length - 1];
- if (lastMatch && typeof lastMatch.index === "number") {
- sentenceStartLine = searchLine;
- sentenceStartChar = lastMatch.index + 1;
- break;
- }
- }
- searchLine--;
- }
- }
- let sentenceEndLine = currentLine;
- let sentenceEndChar = currentLineText.length;
- for (let i = currentChar; i < currentLineText.length; i++) {
- if (sentenceEndPattern.test(currentLineText[i])) {
- sentenceEndChar = i + 1;
- break;
- }
- }
- if (sentenceEndChar === currentLineText.length) {
- const totalLines = editor.lineCount();
- let searchLine = currentLine + 1;
- while (searchLine < totalLines) {
- const lineText = editor.getLine(searchLine);
- if (lineText.trim() === "") {
- sentenceEndLine = searchLine - 1;
- sentenceEndChar = editor.getLine(sentenceEndLine).length;
- break;
- }
- if (lineText.startsWith("#") || lineText.match(/^[\s]*[-*+]\s/) || lineText.match(/^[\s]*\d+\.\s/)) {
- sentenceEndLine = searchLine - 1;
- sentenceEndChar = editor.getLine(sentenceEndLine).length;
- break;
- }
- const match = lineText.match(sentenceEndPattern);
- if (match && match.index !== void 0) {
- sentenceEndLine = searchLine;
- sentenceEndChar = match.index + 1;
- break;
- }
- searchLine++;
- }
- }
- const from = { line: sentenceStartLine, ch: sentenceStartChar };
- const to = { line: sentenceEndLine, ch: sentenceEndChar };
- const text = editor.getRange(from, to).trim();
- Logger.debug(`\uBB38\uC7A5 \uAC10\uC9C0 \uC644\uB8CC: ${sentenceStartLine}:${sentenceStartChar} - ${sentenceEndLine}:${sentenceEndChar}`);
- Logger.debug(`\uBB38\uC7A5 \uB0B4\uC6A9: "${text.substring(0, 50)}${text.length > 50 ? "..." : ""}"`);
- return { text, from, to };
-}
-
-// src/ui/correctionPopup.ts
-init_logger();
-var HIDDEN_CLASS = "kga-hidden";
-var FORCE_HIDDEN_CLASS = "kga-force-hidden";
-var ERROR_SUMMARY_EXPANDED_CLASS = "kga-error-summary-expanded";
-var NO_OUTLINE_CLASS = "kga-no-outline";
-var FADE_OUT_CLASS = "kga-fade-out";
-var CorrectionPopup = class extends BaseComponent {
- constructor(app, config, aiService, onSettingsUpdate) {
- super("div", "correction-popup-container");
- // Pagination state
- this.isLongText = false;
- this.currentPreviewPage = 0;
- this.totalPreviewPages = 1;
- this.pageBreaks = [];
- this.charsPerPage = 800;
- // AI 분석 결과
- this.aiAnalysisResults = [];
- this.isAiAnalyzing = false;
- this.currentFocusIndex = 0;
- this.currentCorrections = [];
- // 전체 오류 위치 캐시
- this.allErrorPositions = [];
- this.app = app;
- this.config = config;
- this.stateManager = new CorrectionStateManager(config.corrections, this.config.ignoredWords);
- this.aiService = aiService;
- this.onSettingsUpdate = onSettingsUpdate;
- this.keyboardScope = new import_obsidian4.Scope();
- this.setupKeyboardNavigation();
- this.initializePagination();
- this.calculateAllErrorPositions();
- }
- /**
- * 키보드 네비게이션을 설정합니다.
- */
- setupKeyboardNavigation() {
- this.keyboardScope.register([], "Tab", (evt) => {
- evt.preventDefault();
- this.focusNextError();
- return false;
- });
- this.keyboardScope.register(["Shift"], "Tab", (evt) => {
- evt.preventDefault();
- this.focusPrevError();
- return false;
- });
- this.keyboardScope.register([], "Enter", (evt) => {
- var _a;
- const target = evt.target;
- if (target && (((_a = target.dataset) == null ? void 0 : _a.editMode) === "true" || target.classList.contains("error-original-input"))) {
- Logger.debug("Enter key in edit mode - allowing default behavior");
- return true;
- }
- evt.preventDefault();
- this.applyCurrentSelection();
- return false;
- });
- this.keyboardScope.register([], "Escape", (evt) => {
- var _a;
- const target = evt.target;
- if (target && (((_a = target.dataset) == null ? void 0 : _a.editMode) === "true" || target.classList.contains("error-original-input"))) {
- Logger.debug("Escape key in edit mode - allowing default behavior");
- return true;
- }
- evt.preventDefault();
- this.close();
- return false;
- });
- this.keyboardScope.register([], "ArrowRight", (evt) => {
- if (this.isInEditMode()) {
- Logger.debug("\u{1F6AB} \uD3B8\uC9D1 \uBAA8\uB4DC \uC911 - ArrowRight \uBE44\uD65C\uC131\uD654");
- return;
- }
- evt.preventDefault();
- evt.stopPropagation();
- evt.stopImmediatePropagation();
- this.cycleCurrentCorrectionNext();
- setTimeout(() => {
- this.updateFocusHighlight();
- }, 50);
- return false;
- });
- this.keyboardScope.register([], "ArrowLeft", (evt) => {
- if (this.isInEditMode()) {
- Logger.debug("\u{1F6AB} \uD3B8\uC9D1 \uBAA8\uB4DC \uC911 - ArrowLeft \uBE44\uD65C\uC131\uD654");
- return;
- }
- evt.preventDefault();
- evt.stopPropagation();
- evt.stopImmediatePropagation();
- this.cycleCurrentCorrectionPrev();
- setTimeout(() => {
- this.updateFocusHighlight();
- }, 50);
- return false;
- });
- this.keyboardScope.register(["Shift", "Mod"], "KeyA", (evt) => {
- evt.preventDefault();
- evt.stopPropagation();
- evt.stopImmediatePropagation();
- this.triggerAIAnalysis();
- return false;
- });
- this.keyboardScope.register([], "ArrowUp", (evt) => {
- if (this.isLongText && this.currentPreviewPage > 0) {
- evt.preventDefault();
- this.goToPrevPage();
- return false;
- }
- return true;
- });
- this.keyboardScope.register([], "ArrowDown", (evt) => {
- if (this.isInEditMode()) {
- return;
- }
- if (this.isLongText && this.currentPreviewPage < this.totalPreviewPages - 1) {
- evt.preventDefault();
- this.goToNextPage();
- return false;
- }
- return true;
- });
- this.keyboardScope.register(["Mod", "Shift"], "ArrowRight", (evt) => {
- if (this.isInEditMode()) {
- Logger.debug("\u{1F6AB} \uD3B8\uC9D1 \uBAA8\uB4DC \uC911 - \uC77C\uAD04 \uBCC0\uACBD \uBE44\uD65C\uC131\uD654");
- return;
- }
- evt.preventDefault();
- this.batchCycleCorrections("next");
- return false;
- });
- this.keyboardScope.register(["Mod", "Shift"], "ArrowLeft", (evt) => {
- if (this.isInEditMode()) {
- Logger.debug("\u{1F6AB} \uD3B8\uC9D1 \uBAA8\uB4DC \uC911 - \uC77C\uAD04 \uBCC0\uACBD \uBE44\uD65C\uC131\uD654");
- return;
- }
- evt.preventDefault();
- this.batchCycleCorrections("prev");
- return false;
- });
- this.keyboardScope.register(["Mod"], "Enter", (evt) => {
- evt.preventDefault();
- evt.stopPropagation();
- evt.stopImmediatePropagation();
- this.applyCorrections();
- return false;
- });
- Logger.log("\uD0A4\uBCF4\uB4DC \uB124\uBE44\uAC8C\uC774\uC158 \uC124\uC815 \uC644\uB8CC");
- }
- /**
- * 다음 오류 항목으로 포커스를 이동합니다.
- */
- focusNextError() {
- var _a, _b;
- Logger.debug("========= focusNextError \uC2DC\uC791 =========");
- Logger.debug(`\uD604\uC7AC \uD3EC\uCEE4\uC2A4 \uC778\uB371\uC2A4: ${this.currentFocusIndex}`);
- const rawCorrections = this.getCurrentCorrections();
- Logger.debug(`RAW \uC218\uC815\uC0AC\uD56D \uAC1C\uC218: ${rawCorrections.length}`);
- Logger.debug("RAW \uC218\uC815\uC0AC\uD56D \uBAA9\uB85D:", rawCorrections.map((pc) => ({
- original: pc.correction.original,
- originalIndex: pc.originalIndex,
- uniqueId: pc.uniqueId,
- absolutePosition: pc.absolutePosition
- })));
- this.currentCorrections = this.removeDuplicateCorrections(rawCorrections);
- Logger.debug(`\uC911\uBCF5 \uC81C\uAC70 \uC804\uD6C4: ${rawCorrections.length} \u2192 ${this.currentCorrections.length}`);
- Logger.debug("\uC911\uBCF5 \uC81C\uAC70 \uD6C4 \uBAA9\uB85D:", this.currentCorrections.map((pc) => ({
- original: pc.correction.original,
- originalIndex: pc.originalIndex,
- uniqueId: pc.uniqueId,
- absolutePosition: pc.absolutePosition
- })));
- if (this.currentCorrections.length === 0) {
- Logger.debug("\uC218\uC815\uC0AC\uD56D\uC774 \uC5C6\uC5B4 \uD568\uC218 \uC885\uB8CC");
- return;
- }
- const oldFocusIndex = this.currentFocusIndex;
- if (this.currentFocusIndex === -1) {
- this.currentFocusIndex = 0;
- } else {
- this.currentFocusIndex = (this.currentFocusIndex + 1) % this.currentCorrections.length;
- }
- Logger.debug(`\uD3EC\uCEE4\uC2A4 \uC778\uB371\uC2A4 \uBCC0\uACBD: ${oldFocusIndex} \u2192 ${this.currentFocusIndex}`);
- Logger.debug(`\uD3EC\uCEE4\uC2A4 \uB300\uC0C1: ${(_a = this.currentCorrections[this.currentFocusIndex]) == null ? void 0 : _a.correction.original} (\uACE0\uC720ID: ${(_b = this.currentCorrections[this.currentFocusIndex]) == null ? void 0 : _b.uniqueId})`);
- this.updateFocusHighlight();
- const errorSummary = document.getElementById("errorSummary");
- const isExpanded = errorSummary && !errorSummary.classList.contains("collapsed");
- if (isExpanded) {
- this.scrollToFocusedError(false);
- }
- Logger.debug(`\uD3EC\uCEE4\uC2A4 \uC774\uB3D9 \uC644\uB8CC: ${this.currentFocusIndex}/${this.currentCorrections.length}, \uC0C1\uC138\uBCF4\uAE30 \uD3BC\uCCD0\uC9D0: ${isExpanded}`);
- Logger.debug("========= focusNextError \uC885\uB8CC =========");
- }
- /**
- * 이전 오류 항목으로 포커스를 이동합니다.
- */
- focusPrevError() {
- const rawCorrections = this.getCurrentCorrections();
- this.currentCorrections = this.removeDuplicateCorrections(rawCorrections);
- if (this.currentCorrections.length === 0)
- return;
- if (this.currentFocusIndex === -1) {
- this.currentFocusIndex = this.currentCorrections.length - 1;
- } else {
- this.currentFocusIndex = this.currentFocusIndex === 0 ? this.currentCorrections.length - 1 : this.currentFocusIndex - 1;
- }
- this.updateFocusHighlight();
- const errorSummary = document.getElementById("errorSummary");
- const isExpanded = errorSummary && !errorSummary.classList.contains("collapsed");
- if (isExpanded) {
- this.scrollToFocusedError(false);
- }
- Logger.debug(`\uD3EC\uCEE4\uC2A4 \uC774\uB3D9: ${this.currentFocusIndex}/${this.currentCorrections.length}, \uC0C1\uC138\uBCF4\uAE30 \uD3BC\uCCD0\uC9D0: ${isExpanded}`);
- }
- /**
- * 현재 포커스된 항목의 수정사항을 적용합니다.
- */
- applyCurrentSelection() {
- if (this.currentCorrections.length === 0) {
- this.close();
- return;
- }
- const pageCorrection = this.currentCorrections[this.currentFocusIndex || 0];
- if (!pageCorrection)
- return;
- const actualIndex = pageCorrection.originalIndex;
- if (actualIndex === void 0) {
- Logger.warn("pageCorrection.originalIndex is undefined");
- return;
- }
- const currentState = this.stateManager.getValue(actualIndex);
- if (currentState === void 0) {
- Logger.warn(`stateManager.getValue(${actualIndex}) returned undefined`);
- return;
- }
- Logger.debug(`\uD0A4\uBCF4\uB4DC\uB85C \uC218\uC815\uC0AC\uD56D \uC801\uC6A9: ${currentState}`);
- }
- /**
- * 현재 포커스된 오류의 다음 수정 제안으로 순환합니다.
- */
- cycleCurrentCorrectionNext() {
- if (this.currentCorrections.length === 0)
- return;
- const pageCorrection = this.currentCorrections[this.currentFocusIndex || 0];
- if (!pageCorrection)
- return;
- const actualIndex = pageCorrection.originalIndex;
- if (actualIndex === void 0) {
- Logger.warn("pageCorrection.originalIndex is undefined");
- return;
- }
- this.cycleCorrectionState(actualIndex, "next");
- }
- /**
- * 현재 포커스된 오류의 이전 수정 제안으로 순환합니다.
- */
- cycleCurrentCorrectionPrev() {
- if (this.currentCorrections.length === 0)
- return;
- const pageCorrection = this.currentCorrections[this.currentFocusIndex || 0];
- if (!pageCorrection)
- return;
- const actualIndex = pageCorrection.originalIndex;
- if (actualIndex === void 0) {
- Logger.warn("pageCorrection.originalIndex is undefined");
- return;
- }
- this.cycleCorrectionState(actualIndex, "prev");
- }
- /**
- * 수정 제안 상태를 순환시킵니다.
- */
- cycleCorrectionState(correctionIndex, direction) {
- const correction = this.config.corrections[correctionIndex];
- if (!correction)
- return;
- const result = direction === "next" ? this.stateManager.toggleState(correctionIndex) : this.stateManager.toggleStatePrev(correctionIndex);
- Logger.log(`\uC218\uC815 \uC81C\uC548 \uC21C\uD658: ${direction}, index: ${correctionIndex}, \uC0C8\uB85C\uC6B4 \uAC12: ${result.value}`);
- this.updateDisplay();
- }
- /**
- * AI 분석을 트리거합니다.
- */
- triggerAIAnalysis() {
- Logger.log("AI \uBD84\uC11D \uD2B8\uB9AC\uAC70\uB428 (\uD0A4\uBCF4\uB4DC \uB2E8\uCD95\uD0A4)");
- const aiBtn = this.element.querySelector("#aiAnalyzeBtn");
- if (aiBtn && !aiBtn.disabled) {
- Logger.log("AI \uBD84\uC11D \uBC84\uD2BC \uD074\uB9AD \uC2E4\uD589");
- aiBtn.click();
- } else {
- Logger.warn("AI \uBD84\uC11D \uBC84\uD2BC\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uAC70\uB098 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- if (this.aiService && !this.isAiAnalyzing) {
- this.performAIAnalysis();
- }
- }
- }
- /**
- * 이전 페이지로 이동합니다.
- */
- goToPrevPage() {
- if (this.currentPreviewPage > 0) {
- this.currentPreviewPage--;
- this.updateDisplay();
- this.resetFocusToFirstError();
- }
- }
- /**
- * 다음 페이지로 이동합니다.
- */
- goToNextPage() {
- if (this.currentPreviewPage < this.totalPreviewPages - 1) {
- this.currentPreviewPage++;
- this.updateDisplay();
- this.resetFocusToFirstError();
- }
- }
- /**
- * 포커스를 첫 번째 오류로 리셋합니다.
- */
- resetFocusToFirstError() {
- Logger.debug("========= resetFocusToFirstError \uC2DC\uC791 =========");
- const rawCorrections = this.getCurrentCorrections();
- Logger.debug(`RAW \uC624\uB958 \uAC1C\uC218: ${rawCorrections.length}`);
- this.currentCorrections = this.removeDuplicateCorrections(rawCorrections);
- Logger.debug(`\uC911\uBCF5 \uC81C\uAC70 \uD6C4 \uC624\uB958 \uAC1C\uC218: ${this.currentCorrections.length}`);
- if (this.currentCorrections.length > 0) {
- this.currentFocusIndex = 0;
- Logger.debug(`\uD3EC\uCEE4\uC2A4 \uC778\uB371\uC2A4\uB97C 0\uC73C\uB85C \uC124\uC815`);
- setTimeout(() => {
- Logger.debug("\uC9C0\uC5F0 \uD6C4 \uD3EC\uCEE4\uC2A4 \uD558\uC774\uB77C\uC774\uD2B8 \uC5C5\uB370\uC774\uD2B8 \uC2E4\uD589");
- this.updateFocusHighlight();
- }, 100);
- const firstPageCorrection = this.currentCorrections[0];
- const actualIndex = firstPageCorrection.originalIndex;
- Logger.debug(`\uCD08\uAE30 \uD3EC\uCEE4\uC2A4 \uC124\uC815: ${this.currentFocusIndex}/${this.currentCorrections.length}`);
- Logger.debug(`\uCCAB \uBC88\uC9F8 \uC624\uB958: "${firstPageCorrection.correction.original}" (\uC804\uCCB4 \uBC30\uC5F4 \uC778\uB371\uC2A4: ${actualIndex}, \uACE0\uC720ID: ${firstPageCorrection.uniqueId})`);
- Logger.debug("\uD604\uC7AC \uD398\uC774\uC9C0 \uC624\uB958 \uBAA9\uB85D:", this.currentCorrections.map((pc) => ({
- original: pc.correction.original,
- originalIndex: pc.originalIndex,
- uniqueId: pc.uniqueId
- })));
- } else {
- this.currentFocusIndex = -1;
- Logger.debug("\uC624\uB958\uAC00 \uC5C6\uC5B4 \uD3EC\uCEE4\uC2A4 \uC124\uC815\uD558\uC9C0 \uC54A\uC74C");
- }
- Logger.debug("========= resetFocusToFirstError \uC885\uB8CC =========");
- }
- /**
- * 현재 포커스된 항목을 시각적으로 표시합니다.
- */
- updateFocusHighlight() {
- var _a;
- Logger.debug("========= updateFocusHighlight \uC2DC\uC791 =========");
- Logger.debug(`currentCorrections \uAE38\uC774: ${this.currentCorrections.length}`);
- Logger.debug(`currentFocusIndex: ${this.currentFocusIndex}`);
- const prevFocused = this.element.querySelectorAll(".keyboard-focused");
- Logger.debug(`\uAE30\uC874 \uD3EC\uCEE4\uC2A4 \uC694\uC18C ${prevFocused.length}\uAC1C \uC81C\uAC70`);
- prevFocused.forEach((el) => el.removeClass("keyboard-focused"));
- if (this.currentCorrections.length > 0 && this.currentFocusIndex >= 0 && this.currentFocusIndex < this.currentCorrections.length) {
- const pageCorrection = this.currentCorrections[this.currentFocusIndex];
- const actualIndex = pageCorrection.originalIndex;
- const uniqueId = pageCorrection.uniqueId;
- Logger.debug(`\uD3EC\uCEE4\uC2A4 \uB300\uC0C1 \uC815\uBCF4:`, {
- original: pageCorrection.correction.original,
- actualIndex,
- uniqueId,
- absolutePosition: pageCorrection.absolutePosition
- });
- let errorItem = this.element.querySelector(`[data-unique-id="${uniqueId}"]`);
- Logger.debug(`\uACE0\uC720 ID\uB85C \uAC80\uC0C9: [data-unique-id="${uniqueId}"] \u2192 ${errorItem ? "\uBC1C\uACAC" : "\uBBF8\uBC1C\uACAC"}`);
- if (!errorItem) {
- errorItem = this.element.querySelector(`[data-correction-index="${actualIndex}"]`);
- Logger.debug(`\uC778\uB371\uC2A4\uB85C \uAC80\uC0C9: [data-correction-index="${actualIndex}"] \u2192 ${errorItem ? "\uBC1C\uACAC" : "\uBBF8\uBC1C\uACAC"}`);
- }
- if (errorItem) {
- errorItem.addClass("keyboard-focused");
- errorItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
- Logger.debug(`\uD3EC\uCEE4\uC2A4 \uD558\uC774\uB77C\uC774\uD2B8 \uC801\uC6A9 \uC131\uACF5: \uACE0\uC720 ID ${uniqueId}, \uC6D0\uBCF8 \uC778\uB371\uC2A4 ${actualIndex}, \uC808\uB300 \uC704\uCE58 ${pageCorrection.absolutePosition}`);
- Logger.debug(`\uD3EC\uCEE4\uC2A4\uB41C \uC694\uC18C \uC815\uBCF4:`, {
- tagName: errorItem.tagName,
- className: errorItem.className,
- textContent: ((_a = errorItem.textContent) == null ? void 0 : _a.substring(0, 50)) + "...",
- dataset: errorItem.dataset
- });
- } else {
- Logger.warn(`\uD3EC\uCEE4\uC2A4 \uB300\uC0C1 \uC694\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C: \uACE0\uC720 ID ${uniqueId}, \uC778\uB371\uC2A4 ${actualIndex}`);
- const allUniqueIdElements = this.element.querySelectorAll("[data-unique-id]");
- const allCorrectionIndexElements = this.element.querySelectorAll("[data-correction-index]");
- Logger.debug(
- `DOM \uB0B4 data-unique-id \uC18D\uC131 \uC694\uC18C ${allUniqueIdElements.length}\uAC1C:`,
- Array.from(allUniqueIdElements).map((el) => el.dataset.uniqueId)
- );
- Logger.debug(
- `DOM \uB0B4 data-correction-index \uC18D\uC131 \uC694\uC18C ${allCorrectionIndexElements.length}\uAC1C:`,
- Array.from(allCorrectionIndexElements).map((el) => el.dataset.correctionIndex)
- );
- }
- } else {
- Logger.debug("\uD3EC\uCEE4\uC2A4\uD560 \uC624\uB958\uAC00 \uC5C6\uAC70\uB098 \uC778\uB371\uC2A4\uAC00 \uBC94\uC704\uB97C \uBC97\uC5B4\uB0A8");
- Logger.debug(`\uC138\uBD80 \uC815\uBCF4: length=${this.currentCorrections.length}, index=${this.currentFocusIndex}, \uC778\uB371\uC2A4 \uBC94\uC704 \uB0B4? ${this.currentFocusIndex >= 0 && this.currentFocusIndex < this.currentCorrections.length}`);
- }
- Logger.debug("========= updateFocusHighlight \uC885\uB8CC =========");
- }
- /**
- * 페이지네이션을 초기화합니다.
- */
- initializePagination() {
- const trimmedText = this.config.selectedText.trim();
- const textLength = trimmedText.length;
- this.isLongText = textLength > 1e3;
- this.charsPerPage = 800;
- this.pageBreaks = [textLength];
- this.totalPreviewPages = 1;
- this.currentPreviewPage = 0;
- Logger.log(`Initial pagination setup: Long text: ${this.isLongText}, Trimmed length: ${textLength}`);
- }
- /**
- * 팝업을 렌더링합니다.
- */
- render() {
- this.element.id = "correctionPopup";
- this.element.setAttribute("tabindex", "-1");
- this.createPopupStructure();
- this.bindEvents();
- this.app.keymap.pushScope(this.keyboardScope);
- setTimeout(() => {
- this.element.focus();
- }, 50);
- this.resetFocusToFirstError();
- this.showKeyboardHint();
- document.body.classList.add("spell-popup-open");
- return this.element;
- }
- /**
- * 팝업 DOM 구조를 생성합니다.
- */
- createPopupStructure() {
- this.element.empty();
- const overlay = this.element.createDiv("popup-overlay");
- const content = this.element.createDiv("popup-content");
- const header = content.createDiv("header");
- new import_obsidian4.Setting(header).setName("\uD55C\uAD6D\uC5B4 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC").setHeading();
- const headerTop = header.createDiv("preview-header-top");
- const aiBtn = headerTop.createEl("button", {
- cls: "ai-analyze-btn",
- attr: { id: "aiAnalyzeBtn" }
- });
- this.updateAiButtonState(aiBtn);
- headerTop.createEl("button", { cls: "close-btn-header", text: "\xD7" });
- const mainContent = content.createDiv("content");
- const previewSection = mainContent.createDiv("preview-section");
- const previewHeader = previewSection.createDiv("preview-header");
- const previewLabel = previewHeader.createDiv("preview-label");
- previewLabel.createSpan({ text: "\uBBF8\uB9AC\uBCF4\uAE30" });
- previewLabel.createSpan({ cls: "preview-hint", text: "\uD074\uB9AD\uD558\uC5EC \uC218\uC815\uC0AC\uD56D \uC801\uC6A9" });
- const colorLegend = previewHeader.createDiv("color-legend");
- const legendItems = [
- { cls: "error", text: "\uC624\uB958" },
- { cls: "corrected", text: "\uC218\uC815" },
- { cls: "exception-processed", text: "\uC608\uC678\uCC98\uB9AC" },
- { cls: "original-kept", text: "\uC6D0\uBCF8\uC720\uC9C0" },
- { cls: "user-edited", text: "\uD3B8\uC9D1\uB428" }
- ];
- legendItems.forEach((item) => {
- const legendItem = colorLegend.createDiv("color-legend-item");
- legendItem.createDiv(`color-legend-dot ${item.cls}`);
- legendItem.createSpan({ text: item.text });
- });
- const paginationDiv = previewHeader.createDiv();
- this.createPaginationElement(paginationDiv);
- const previewContent = previewSection.createDiv("preview-text");
- previewContent.id = "resultPreview";
- previewContent.createEl("span", { text: this.config.selectedText.trim() });
- const errorSummary = mainContent.createDiv("error-summary collapsed");
- errorSummary.id = "errorSummary";
- const errorToggle = errorSummary.createDiv("error-summary-toggle");
- const leftSection = errorToggle.createDiv("left-section");
- leftSection.createSpan({ cls: "error-summary-label", text: "\uC624\uB958 \uC0C1\uC138" });
- const badge = leftSection.createSpan({
- cls: "error-count-badge",
- text: this.getErrorStateCount().toString(),
- attr: { id: "errorCountBadge" }
- });
- errorToggle.createSpan({ cls: "toggle-icon", text: "\u25BC" });
- const errorContent = errorSummary.createDiv("error-summary-content");
- errorContent.id = "errorSummaryContent";
- this.createErrorSummaryElement(errorContent);
- const buttonArea = content.createDiv("button-area");
- buttonArea.createEl("button", { cls: "cancel-btn", text: "\uCDE8\uC18C" });
- buttonArea.createEl("button", {
- cls: "apply-btn",
- text: "\uC801\uC6A9",
- attr: { id: "applyCorrectionsButton" }
- });
- }
- /**
- * 페이지네이션 요소를 생성합니다.
- */
- createPaginationElement(container) {
- container.empty();
- if (!this.isLongText || this.totalPreviewPages <= 1) {
- const hiddenContainer = container.createDiv("pagination-container-hidden");
- hiddenContainer.id = "paginationContainer";
- return;
- }
- const paginationControls = container.createDiv("pagination-controls");
- paginationControls.id = "paginationContainer";
- const prevBtn = paginationControls.createEl("button", {
- cls: "pagination-btn",
- text: "\uC774\uC804"
- });
- prevBtn.id = "prevPreviewPage";
- if (this.currentPreviewPage === 0) {
- prevBtn.disabled = true;
- }
- const pageInfo = paginationControls.createSpan("page-info");
- pageInfo.id = "previewPageInfo";
- pageInfo.textContent = `${this.currentPreviewPage + 1} / ${this.totalPreviewPages}`;
- const nextBtn = paginationControls.createEl("button", {
- cls: "pagination-btn",
- text: "\uB2E4\uC74C"
- });
- nextBtn.id = "nextPreviewPage";
- if (this.currentPreviewPage === this.totalPreviewPages - 1) {
- nextBtn.disabled = true;
- }
- const pageCharsInfo = paginationControls.createSpan("page-chars-info");
- pageCharsInfo.id = "pageCharsInfo";
- pageCharsInfo.textContent = `${this.charsPerPage}\uC790`;
- }
- /**
- * 페이지네이션 HTML을 생성합니다. (하위 호환성을 위해 유지)
- * @deprecated createPaginationElement 사용 권장
- */
- createPaginationHTML() {
- if (!this.isLongText || this.totalPreviewPages <= 1) {
- return '
';
- }
- return `
-
- `;
- }
- /**
- * 현재 페이지의 교정 목록을 가져옵니다. (절대 위치 기반 정확한 순서)
- */
- getCurrentCorrections() {
- Logger.debug("========= getCurrentCorrections \uC2DC\uC791 =========");
- Logger.debug(`isLongText: ${this.isLongText}`);
- Logger.debug(`currentPreviewPage: ${this.currentPreviewPage}`);
- Logger.debug(`allErrorPositions \uAC1C\uC218: ${this.allErrorPositions.length}`);
- if (!this.isLongText) {
- const result = this.allErrorPositions.map((errorPos, index) => ({
- correction: errorPos.correction,
- originalIndex: errorPos.originalIndex || index,
- positionInPage: errorPos.absolutePosition || 0,
- absolutePosition: errorPos.absolutePosition || 0,
- uniqueId: errorPos.uniqueId || `fallback_${index}`,
- // Phase 3에서 추가된 필수 필드들
- pageIndex: 0,
- absoluteIndex: errorPos.originalIndex || index,
- relativeIndex: index,
- isVisible: true
- }));
- Logger.debug(`\uC9E7\uC740 \uD14D\uC2A4\uD2B8 \uBAA8\uB4DC: \uC804\uCCB4 ${result.length}\uAC1C \uC624\uB958 \uBC18\uD658`);
- Logger.debug("\uBC18\uD658 \uC624\uB958 \uBAA9\uB85D:", result.map((pc) => ({
- original: pc.correction.original,
- originalIndex: pc.originalIndex,
- uniqueId: pc.uniqueId,
- absolutePosition: pc.absolutePosition
- })));
- Logger.debug("========= getCurrentCorrections \uC885\uB8CC (\uC9E7\uC740 \uD14D\uC2A4\uD2B8) =========");
- return result;
- }
- const previewStartIndex = this.currentPreviewPage === 0 ? 0 : this.pageBreaks[this.currentPreviewPage - 1];
- const previewEndIndex = this.pageBreaks[this.currentPreviewPage];
- Logger.debug(`\uD398\uC774\uC9C0 \uBC94\uC704: ${previewStartIndex} ~ ${previewEndIndex}`);
- const pageCorrections = [];
- this.allErrorPositions.forEach((errorPos, index) => {
- Logger.debug(`[${index}] \uC624\uB958 \uC704\uCE58 \uAC80\uC0AC: "${errorPos.correction.original}" at ${errorPos.absolutePosition} (\uACE0\uC720ID: ${errorPos.uniqueId})`);
- if ((errorPos.absolutePosition || 0) >= previewStartIndex && (errorPos.absolutePosition || 0) < previewEndIndex) {
- const pageCorrection = {
- correction: errorPos.correction,
- originalIndex: errorPos.originalIndex || index,
- positionInPage: (errorPos.absolutePosition || 0) - previewStartIndex,
- absolutePosition: errorPos.absolutePosition || 0,
- uniqueId: errorPos.uniqueId || `fallback_${index}`,
- // Phase 3에서 추가된 필수 필드들
- pageIndex: this.currentPreviewPage || 0,
- absoluteIndex: errorPos.originalIndex || index,
- relativeIndex: pageCorrections.length,
- isVisible: true
- };
- pageCorrections.push(pageCorrection);
- Logger.debug(`[${index}] \uD398\uC774\uC9C0 \uBC94\uC704 \uB0B4 \uC624\uB958 \uCD94\uAC00: positionInPage=${pageCorrection.positionInPage}`);
- } else {
- Logger.debug(`[${index}] \uD398\uC774\uC9C0 \uBC94\uC704 \uBC16 \uC624\uB958 \uC81C\uC678`);
- }
- });
- pageCorrections.sort((a, b) => (a.absolutePosition || 0) - (b.absolutePosition || 0));
- Logger.debug(`getCurrentCorrections: \uD398\uC774\uC9C0 ${this.currentPreviewPage + 1}, \uC624\uB958 ${pageCorrections.length}\uAC1C`);
- Logger.debug("\uCD5C\uC885 \uC624\uB958 \uC704\uCE58 \uC21C\uC11C:", pageCorrections.map((pc) => ({
- original: pc.correction.original,
- originalIndex: pc.originalIndex,
- positionInPage: pc.positionInPage,
- absolutePosition: pc.absolutePosition,
- uniqueId: pc.uniqueId
- })));
- Logger.debug("========= getCurrentCorrections \uC885\uB8CC (\uAE34 \uD14D\uC2A4\uD2B8) =========");
- return pageCorrections;
- }
- /**
- * 중복된 교정 항목을 제거합니다.
- * 같은 original 텍스트를 가진 corrections를 그룹화하여 대표 항목만 선택합니다.
- */
- removeDuplicateCorrections(corrections) {
- Logger.debug("========= removeDuplicateCorrections \uC2DC\uC791 =========");
- Logger.debug(`\uC785\uB825 corrections \uAC1C\uC218: ${corrections.length}`);
- Logger.debug("\uC785\uB825 corrections:", corrections.map((pc) => ({
- original: pc.correction.original,
- originalIndex: pc.originalIndex,
- uniqueId: pc.uniqueId,
- absolutePosition: pc.absolutePosition
- })));
- const uniqueMap = /* @__PURE__ */ new Map();
- const duplicateGroups = /* @__PURE__ */ new Map();
- corrections.forEach((correction, index) => {
- const originalText = correction.correction.original;
- const position = correction.absolutePosition;
- Logger.debug(`[${index}] \uADF8\uB8F9\uD654 \uC911: "${originalText}" (\uC704\uCE58: ${position}, \uACE0\uC720ID: ${correction.uniqueId})`);
- let groupKey = originalText;
- let foundOverlap = false;
- for (const [existingKey, existingGroup] of duplicateGroups) {
- if (existingGroup.length > 0) {
- const existingCorrection = existingGroup[0];
- const existingPos = existingCorrection.absolutePosition;
- const existingText = existingCorrection.correction.original;
- if (position === existingPos && (originalText.includes(existingText) || existingText.includes(originalText))) {
- groupKey = existingKey;
- foundOverlap = true;
- Logger.debug(`[${index}] \uC704\uCE58 \uAE30\uBC18 \uC911\uBCF5 \uBC1C\uACAC: "${originalText}" \u2194 "${existingText}" (\uC704\uCE58: ${position})`);
- break;
- }
- }
- }
- if (!duplicateGroups.has(groupKey)) {
- duplicateGroups.set(groupKey, []);
- Logger.debug(`[${index}] \uC0C8\uB85C\uC6B4 \uADF8\uB8F9 \uC0DD\uC131: "${groupKey}"`);
- }
- duplicateGroups.get(groupKey).push(correction);
- Logger.debug(`[${index}] \uADF8\uB8F9 \uCD94\uAC00 \uC644\uB8CC. \uD604\uC7AC "${groupKey}" \uADF8\uB8F9 \uD06C\uAE30: ${duplicateGroups.get(groupKey).length}`);
- });
- Logger.debug(`\uADF8\uB8F9\uD654 \uC644\uB8CC. \uCD1D ${duplicateGroups.size}\uAC1C \uADF8\uB8F9 \uC0DD\uC131`);
- duplicateGroups.forEach((group, originalText) => {
- Logger.debug(`\uCC98\uB9AC \uC911\uC778 \uADF8\uB8F9: "${originalText}", \uADF8\uB8F9 \uD06C\uAE30: ${group.length}`);
- Logger.debug(`\uADF8\uB8F9 \uB0B4 \uD56D\uBAA9\uB4E4:`, group.map((pc) => ({
- originalIndex: pc.originalIndex,
- uniqueId: pc.uniqueId,
- absolutePosition: pc.absolutePosition
- })));
- if (group.length === 1) {
- uniqueMap.set(originalText, group[0]);
- Logger.debug(`[\uB2E8\uC77C \uD56D\uBAA9] "${originalText}" \u2192 \uB300\uD45C \uD56D\uBAA9: ${group[0].uniqueId}`);
- } else {
- const representative = this.selectRepresentativeCorrection(group);
- uniqueMap.set(originalText, representative);
- Logger.debug(`[\uC911\uBCF5 \uD56D\uBAA9] "${originalText}", ${group.length}\uAC1C \uD56D\uBAA9 \u2192 \uB300\uD45C \uD56D\uBAA9 \uC120\uD0DD (uniqueId: ${representative.uniqueId}, originalIndex: ${representative.originalIndex})`);
- Logger.debug(`\uC81C\uC678\uB41C \uD56D\uBAA9\uB4E4:`, group.filter((pc) => pc.uniqueId !== (representative == null ? void 0 : representative.uniqueId)).map((pc) => ({
- uniqueId: pc.uniqueId || "undefined",
- originalIndex: pc.originalIndex || -1,
- absolutePosition: pc.absolutePosition || -1
- })));
- }
- });
- Logger.debug(`\uB300\uD45C \uD56D\uBAA9 \uC120\uD0DD \uC644\uB8CC. uniqueMap \uD06C\uAE30: ${uniqueMap.size}`);
- const result = Array.from(uniqueMap.values()).sort((a, b) => (a.absolutePosition || 0) - (b.absolutePosition || 0));
- Logger.debug(`\uC911\uBCF5 \uC81C\uAC70 \uACB0\uACFC: ${corrections.length}\uAC1C \u2192 ${result.length}\uAC1C`);
- Logger.debug("\uCD5C\uC885 \uC911\uBCF5 \uC81C\uAC70 \uD6C4 \uD56D\uBAA9\uB4E4:", result.map((pc) => ({
- original: pc.correction.original,
- originalIndex: pc.originalIndex,
- uniqueId: pc.uniqueId,
- absolutePosition: pc.absolutePosition
- })));
- Logger.debug("========= removeDuplicateCorrections \uC885\uB8CC =========");
- return result;
- }
- /**
- * 중복된 교정 항목들 중에서 대표 항목을 선택합니다.
- * 선택 기준:
- * 1. 가장 앞에 위치한 항목 (absolutePosition이 가장 작은 항목)
- * 2. 동일한 위치인 경우 가장 많은 수정 제안을 가진 항목
- * 3. 수정 제안이 같은 경우 더 좋은 도움말을 가진 항목 (문법 > 맞춤법 > 띄어쓰기)
- * 4. 그 외에는 첫 번째 항목
- */
- selectRepresentativeCorrection(corrections) {
- if (corrections.length === 0) {
- throw new Error("\uBE48 \uAD50\uC815 \uBC30\uC5F4\uC5D0\uC11C\uB294 \uB300\uD45C \uD56D\uBAA9\uC744 \uC120\uD0DD\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
- }
- if (corrections.length === 1) {
- return corrections[0];
- }
- const minPosition = Math.min(...corrections.map((c) => c.absolutePosition || 0));
- const frontmostCorrections = corrections.filter((c) => (c.absolutePosition || 0) === minPosition);
- if (frontmostCorrections.length === 1) {
- Logger.debug(`\uB300\uD45C \uD56D\uBAA9 \uC120\uD0DD: \uAC00\uC7A5 \uC55E \uC704\uCE58 \uAE30\uC900 (\uC704\uCE58: ${minPosition})`);
- return frontmostCorrections[0];
- }
- const maxSuggestions = Math.max(...frontmostCorrections.map((c) => c.correction.corrected.length));
- const bestSuggestionCorrections = frontmostCorrections.filter((c) => c.correction.corrected.length === maxSuggestions);
- if (bestSuggestionCorrections.length === 1) {
- Logger.debug(`\uB300\uD45C \uD56D\uBAA9 \uC120\uD0DD: \uC218\uC815 \uC81C\uC548 \uC218 \uAE30\uC900 (\uC81C\uC548 \uC218: ${maxSuggestions})`);
- return bestSuggestionCorrections[0];
- }
- const helpPriority = (help) => {
- const helpLower = help.toLowerCase();
- if (helpLower.includes("\uBB38\uBC95"))
- return 3;
- if (helpLower.includes("\uB9DE\uCDA4\uBC95"))
- return 2;
- if (helpLower.includes("\uB744\uC5B4\uC4F0\uAE30"))
- return 1;
- return 0;
- };
- const maxHelpPriority = Math.max(...bestSuggestionCorrections.map((c) => helpPriority(c.correction.help)));
- const bestHelpCorrections = bestSuggestionCorrections.filter((c) => helpPriority(c.correction.help) === maxHelpPriority);
- if (bestHelpCorrections.length === 1) {
- Logger.debug(`\uB300\uD45C \uD56D\uBAA9 \uC120\uD0DD: \uB3C4\uC6C0\uB9D0 \uC6B0\uC120\uC21C\uC704 \uAE30\uC900 (\uC6B0\uC120\uC21C\uC704: ${maxHelpPriority})`);
- return bestHelpCorrections[0];
- }
- Logger.debug(`\uB300\uD45C \uD56D\uBAA9 \uC120\uD0DD: \uCCAB \uBC88\uC9F8 \uD56D\uBAA9 \uAE30\uBCF8 \uC120\uD0DD`);
- return bestHelpCorrections[0];
- }
- /**
- * 전체 텍스트에서 모든 오류의 위치를 계산합니다.
- */
- calculateAllErrorPositions() {
- this.allErrorPositions = [];
- this.config.corrections.forEach((correction, originalIndex) => {
- let searchPos = 0;
- let occurrenceCount = 0;
- while (true) {
- const foundPos = this.config.selectedText.indexOf(correction.original, searchPos);
- if (foundPos === -1)
- break;
- const endPos = foundPos + correction.original.length;
- if (this.config.selectedText.slice(foundPos, endPos) === correction.original) {
- this.allErrorPositions.push({
- correction,
- originalIndex,
- absolutePosition: foundPos,
- uniqueId: `${originalIndex}_${occurrenceCount}`
- });
- occurrenceCount++;
- }
- searchPos = foundPos + 1;
- }
- });
- this.allErrorPositions.sort((a, b) => a.absolutePosition - b.absolutePosition);
- Logger.debug("\uC804\uCCB4 \uC624\uB958 \uC704\uCE58 \uACC4\uC0B0 \uC644\uB8CC:", {
- totalErrors: this.allErrorPositions.length,
- positions: this.allErrorPositions.map((ep) => ({
- original: ep.correction.original,
- originalIndex: ep.originalIndex,
- absolutePosition: ep.absolutePosition,
- uniqueId: ep.uniqueId
- }))
- });
- }
- /**
- * 현재 페이지에서 오류 상태(빨간색)인 항목의 개수를 가져옵니다.
- */
- getErrorStateCount() {
- const currentCorrections = this.getCurrentCorrections();
- const uniqueCorrections = this.removeDuplicateCorrections(currentCorrections);
- let errorCount = 0;
- uniqueCorrections.forEach((pageCorrection) => {
- const actualIndex = pageCorrection.originalIndex;
- if (actualIndex === void 0) {
- Logger.warn("pageCorrection.originalIndex is undefined in getErrorStateCount");
- return;
- }
- const correction = pageCorrection.correction;
- const currentValue = this.stateManager.getValue(actualIndex);
- const isException = this.stateManager.isExceptionState(actualIndex);
- const isOriginalKept = this.stateManager.isOriginalKeptState(actualIndex);
- if (currentValue === correction.original && !isException && !isOriginalKept) {
- errorCount++;
- }
- });
- return errorCount;
- }
- /**
- * 미리보기 콘텐츠를 업데이트합니다 (DOM API 사용).
- */
- updatePreviewContent(previewElement) {
- this.createPreviewElement(previewElement);
- }
- /**
- * 미리보기 HTML을 생성합니다.
- */
- generatePreviewHTML() {
- const previewText = this.isLongText ? this.getCurrentPreviewText() : this.config.selectedText.trim();
- const rawCorrections = this.getCurrentCorrections();
- const currentCorrections = this.removeDuplicateCorrections(rawCorrections);
- const originalText = this.config.selectedText;
- const trimmedStartOffset = originalText.length - originalText.trimStart().length;
- Logger.debug("generatePreviewHTML \uB514\uBC84\uAE45:", {
- isLongText: this.isLongText,
- originalLength: originalText.length,
- trimmedLength: previewText.length,
- trimmedStartOffset,
- startsWithSpace: previewText.startsWith(" "),
- endsWithSpace: previewText.endsWith(" "),
- firstChars: previewText.substring(0, 20),
- lastChars: previewText.substring(previewText.length - 20),
- correctionsCount: currentCorrections.length
- });
- const processedPositions = /* @__PURE__ */ new Map();
- const segments = [];
- currentCorrections.forEach((pageCorrection) => {
- const correction = pageCorrection.correction;
- const actualIndex = pageCorrection.originalIndex;
- if (actualIndex === void 0) {
- Logger.warn("pageCorrection.originalIndex is undefined in renderPreview");
- return;
- }
- const uniqueId = pageCorrection.uniqueId || "unknown";
- const positionInPage = pageCorrection.positionInPage || 0;
- const displayClass = this.stateManager.getDisplayClass(actualIndex);
- const currentValue = this.stateManager.getValue(actualIndex);
- if (currentValue === void 0) {
- Logger.warn(`stateManager.getValue(${actualIndex}) returned undefined in renderPreview`);
- return;
- }
- const escapedValue = escapeHtml(currentValue);
- const isUserEdited = this.stateManager.isUserEditedState(actualIndex);
- if (isUserEdited) {
- Logger.debug(`\u{1F3A8} \uBBF8\uB9AC\uBCF4\uAE30 \uC0AC\uC6A9\uC790\uD3B8\uC9D1: index=${actualIndex}, original="${correction.original}", currentValue="${currentValue}", displayClass="${displayClass}"`);
- }
- const replacementHtml = `${escapedValue}`;
- const expectedText = correction.original || "";
- const expectedEnd = positionInPage + ((expectedText == null ? void 0 : expectedText.length) || 0);
- if (positionInPage >= 0 && expectedEnd <= previewText.length) {
- const actualText = previewText.slice(positionInPage, expectedEnd);
- if (actualText === expectedText) {
- const positionKey = `${positionInPage}-${expectedEnd}`;
- if (!processedPositions.has(positionKey)) {
- processedPositions.set(positionKey, true);
- segments.push({
- text: actualText,
- html: replacementHtml,
- start: positionInPage,
- end: expectedEnd
- });
- Logger.debug(`\uBBF8\uB9AC\uBCF4\uAE30 \uC624\uB958 \uCC98\uB9AC: ${actualText} at ${positionInPage}-${expectedEnd}, \uACE0\uC720 ID: ${uniqueId}`);
- }
- } else {
- Logger.warn(`\uD14D\uC2A4\uD2B8 \uBD88\uC77C\uCE58: \uC608\uC0C1 "${expectedText}", \uC2E4\uC81C "${actualText}" at ${positionInPage}-${expectedEnd}`);
- }
- } else {
- Logger.warn(`\uC704\uCE58 \uBC94\uC704 \uCD08\uACFC: ${positionInPage}-${expectedEnd}, \uD14D\uC2A4\uD2B8 \uAE38\uC774: ${previewText.length}`);
- }
- });
- segments.sort((a, b) => a.start - b.start);
- let finalHtml = "";
- let currentPos = 0;
- segments.forEach((segment) => {
- if (segment.start > currentPos) {
- const betweenText = previewText.substring(currentPos, segment.start);
- const cleanedBetweenText = currentPos === 0 ? betweenText.trimStart() : betweenText;
- finalHtml += escapeHtml(cleanedBetweenText);
- }
- if (segment.start >= currentPos) {
- finalHtml += segment.html;
- currentPos = segment.end;
- }
- });
- if (currentPos < previewText.length) {
- const remainingText = previewText.substring(currentPos);
- const cleanedRemainingText = currentPos === 0 ? remainingText.trimStart() : remainingText;
- finalHtml += escapeHtml(cleanedRemainingText);
- }
- return finalHtml;
- }
- /**
- * 현재 페이지의 미리보기 텍스트를 가져옵니다.
- */
- getCurrentPreviewText() {
- if (!this.isLongText)
- return this.config.selectedText.trim();
- const previewStartIndex = this.currentPreviewPage === 0 ? 0 : this.pageBreaks[this.currentPreviewPage - 1];
- const previewEndIndex = this.pageBreaks[this.currentPreviewPage];
- const pageText = this.config.selectedText.slice(previewStartIndex, previewEndIndex);
- const cleanedPageText = pageText.trim();
- Logger.debug("getCurrentPreviewText \uB514\uBC84\uAE45:", {
- currentPage: this.currentPreviewPage,
- startIndex: previewStartIndex,
- endIndex: previewEndIndex,
- originalLength: pageText.length,
- cleanedLength: cleanedPageText.length,
- startsWithSpace: pageText.startsWith(" "),
- endsWithSpace: pageText.endsWith(" "),
- firstChars: pageText.substring(0, 20),
- cleanedFirstChars: cleanedPageText.substring(0, 20)
- });
- return cleanedPageText;
- }
- /**
- * 오류 요약 HTML을 생성합니다.
- */
- generateErrorSummaryHTML() {
- Logger.debug(`\u{1F3D7}\uFE0F generateErrorSummaryHTML \uC2DC\uC791`);
- const rawCorrections = this.getCurrentCorrections();
- const currentCorrections = this.removeDuplicateCorrections(rawCorrections);
- Logger.debug(`\u{1F3D7}\uFE0F rawCorrections: ${rawCorrections.length}, currentCorrections: ${currentCorrections.length}`);
- if (currentCorrections.length === 0) {
- Logger.debug(`\u{1F3D7}\uFE0F \uC624\uB958 \uC5C6\uC74C - \uD50C\uB808\uC774\uC2A4\uD640\uB354 \uBC18\uD658`);
- return `
-
-
\u2713
-
\uC774 \uD398\uC774\uC9C0\uC5D0\uB294 \uBC1C\uACAC\uB41C \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4
-
\uB2E4\uB978 \uD398\uC774\uC9C0\uC5D0\uC11C \uC624\uB958\uB97C \uD655\uC778\uD558\uC138\uC694
-
- `;
- }
- const uniqueCorrections = this.removeDuplicateCorrections(currentCorrections);
- Logger.debug(`\u{1F3D7}\uFE0F uniqueCorrections: ${uniqueCorrections.length}`);
- return uniqueCorrections.map((pageCorrection, index) => {
- const actualIndex = pageCorrection.originalIndex;
- const correction = pageCorrection.correction;
- const isOriginalKept = this.stateManager.isOriginalKeptState(actualIndex);
- const isUserEdited = this.stateManager.isUserEditedState(actualIndex);
- const suggestions = correction.corrected.slice(0, 3);
- Logger.debug(`\u{1F3D7}\uFE0F HTML \uC0DD\uC131: "${correction.original}" \u2192 actualIndex=${actualIndex}, pageIndex=${index}`);
- const aiResult = this.aiAnalysisResults.find((result) => result.correctionIndex === actualIndex);
- const reasoningHTML = aiResult ? `
-
\u{1F916} \uC2E0\uB8B0\uB3C4: ${aiResult.confidence}%
-
${escapeHtml(aiResult.reasoning)}
-
` : isOriginalKept ? `
-
\uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uC120\uD0DD\uD588\uAC70\uB098, \uC608\uC678 \uB2E8\uC5B4\uB85C \uB4F1\uB85D\uB41C \uD56D\uBAA9\uC785\uB2C8\uB2E4.
-
` : "";
- const suggestionsHTML = suggestions.map(
- (suggestion) => `
- ${escapeHtml(suggestion)}
- `
- ).join("");
- const stateClass = isUserEdited ? "user-edited" : isOriginalKept ? "original-kept" : this.stateManager.isExceptionState(actualIndex) ? "exception-processed" : this.stateManager.getValue(actualIndex) !== correction.original ? "corrected" : "";
- const htmlString = `
-
-
-
${escapeHtml(this.stateManager.getValue(actualIndex))}
-
- ${suggestionsHTML}
-
- \uC608\uC678\uCC98\uB9AC
-
-
-
-
${escapeHtml(correction.help)}
- ${reasoningHTML}
-
- `;
- Logger.debug(`\u{1F3D7}\uFE0F HTML \uCCAB \uBD80\uBD84 - actualIndex=${actualIndex}: ${htmlString.substring(0, 200)}...`);
- return htmlString;
- }).join("");
- }
- /**
- * 이벤트를 바인딩합니다.
- */
- bindEvents() {
- const cmdEHandler = this.app.scope.register(["Mod"], "KeyE", (evt) => {
- if (this.isInEditMode()) {
- return true;
- }
- evt.preventDefault();
- evt.stopPropagation();
- this.enterEditModeForFocusedError();
- return false;
- });
- const cmdShiftEHandler = this.app.scope.register(["Mod", "Shift"], "KeyE", (evt) => {
- evt.preventDefault();
- evt.stopPropagation();
- this.toggleErrorSummary();
- return false;
- });
- this.cleanupFunctions.push(() => this.app.scope.unregister(cmdEHandler));
- this.cleanupFunctions.push(() => this.app.scope.unregister(cmdShiftEHandler));
- const documentKeyListener = (evt) => {
- if (evt.code === "KeyE" && (evt.metaKey && !evt.ctrlKey || !evt.metaKey && evt.ctrlKey) && !evt.shiftKey) {
- if (this.isInEditMode()) {
- return;
- }
- evt.preventDefault();
- evt.stopPropagation();
- this.enterEditModeForFocusedError();
- return;
- }
- if (evt.code === "KeyE" && (evt.metaKey && !evt.ctrlKey || !evt.metaKey && evt.ctrlKey) && evt.shiftKey) {
- evt.preventDefault();
- evt.stopPropagation();
- this.toggleErrorSummary();
- return;
- }
- };
- document.addEventListener("keydown", documentKeyListener);
- this.cleanupFunctions.push(() => document.removeEventListener("keydown", documentKeyListener));
- this.addEventListener(this.element, "keydown", (evt) => {
- if (evt.code === "KeyA" && evt.shiftKey && evt.metaKey && !evt.ctrlKey) {
- evt.preventDefault();
- evt.stopPropagation();
- this.triggerAIAnalysis();
- return;
- }
- if (evt.code === "KeyE" && (evt.metaKey && !evt.ctrlKey || !evt.metaKey && evt.ctrlKey) && !evt.shiftKey) {
- if (this.isInEditMode()) {
- return;
- }
- evt.preventDefault();
- evt.stopPropagation();
- this.enterEditModeForFocusedError();
- return;
- }
- if (evt.code === "KeyE" && (evt.metaKey && !evt.ctrlKey || !evt.metaKey && evt.ctrlKey) && evt.shiftKey) {
- evt.preventDefault();
- evt.stopPropagation();
- this.toggleErrorSummary();
- return;
- }
- });
- this.bindCloseEvents();
- const overlay = this.element.querySelector(".popup-overlay");
- if (overlay) {
- this.addEventListener(overlay, "click", () => {
- this.close();
- });
- }
- this.bindPaginationEvents();
- this.bindErrorToggleEvents();
- this.bindCorrectionEvents();
- this.bindApplyEvents();
- this.bindAIAnalysisEvents();
- }
- /**
- * 닫기 이벤트를 바인딩합니다.
- */
- bindCloseEvents() {
- const closeButtons = this.element.querySelectorAll(".close-btn-header, .cancel-btn");
- closeButtons.forEach((button) => {
- this.addEventListener(button, "click", () => {
- this.close();
- });
- });
- const escKeyHandler = (e) => {
- if (e.key === "Escape") {
- this.close();
- }
- };
- document.addEventListener("keydown", escKeyHandler);
- this.cleanupFunctions.push(() => document.removeEventListener("keydown", escKeyHandler));
- }
- /**
- * 페이지네이션 이벤트를 바인딩합니다.
- */
- bindPaginationEvents() {
- const prevButton = this.element.querySelector("#prevPreviewPage");
- const nextButton = this.element.querySelector("#nextPreviewPage");
- if (prevButton) {
- this.addEventListener(prevButton, "click", () => {
- if (this.currentPreviewPage > 0) {
- this.currentPreviewPage--;
- this.updateDisplay();
- this.resetFocusToFirstError();
- }
- });
- }
- if (nextButton) {
- this.addEventListener(nextButton, "click", () => {
- if (this.currentPreviewPage < this.totalPreviewPages - 1) {
- this.currentPreviewPage++;
- this.updateDisplay();
- this.resetFocusToFirstError();
- }
- });
- }
- }
- /**
- * 오류 토글 이벤트를 바인딩합니다.
- */
- bindErrorToggleEvents() {
- const toggleElement = this.element.querySelector(".error-summary-toggle");
- if (toggleElement) {
- this.addEventListener(toggleElement, "click", () => {
- const errorSummary = this.element.querySelector("#errorSummary");
- if (errorSummary) {
- errorSummary.classList.toggle("collapsed");
- setTimeout(() => {
- this.recalculatePagination();
- this.updateDisplay();
- }, 350);
- }
- });
- }
- }
- /**
- * 교정 클릭 이벤트를 바인딩합니다.
- */
- bindCorrectionEvents() {
- this.addEventListener(this.element, "click", (e) => {
- const target = e.target;
- Logger.debug(`\u{1F5B1}\uFE0F \uD074\uB9AD \uC774\uBCA4\uD2B8 \uBC1C\uC0DD: target="${target.tagName}.${target.className}", textContent="${target.textContent}"`);
- if (target.classList.contains("clickable-error")) {
- Logger.debug(`\u{1F5B1}\uFE0F \uBBF8\uB9AC\uBCF4\uAE30 \uD074\uB9AD \uCC98\uB9AC: ${target.textContent}`);
- this.handlePreviewClick(target);
- }
- if (target.classList.contains("error-original-compact")) {
- Logger.debug(`\u{1F5B1}\uFE0F \uC624\uB958 \uCE74\uB4DC \uD14D\uC2A4\uD2B8 \uD074\uB9AD \uAC10\uC9C0: ${target.textContent}`);
- this.handleCardTextClick(target);
- }
- if (target.classList.contains("suggestion-compact")) {
- this.handleSuggestionClick(target);
- }
- });
- this.addEventListener(this.element, "contextmenu", (e) => {
- const target = e.target;
- if (target.classList.contains("clickable-error")) {
- e.preventDefault();
- Logger.debug(`\u{1F5B1}\uFE0F \uBBF8\uB9AC\uBCF4\uAE30 \uC6B0\uD074\uB9AD \uD3B8\uC9D1 \uBAA8\uB4DC: ${target.textContent}`);
- this.handlePreviewRightClick(target);
- }
- });
- this.bindTouchHoldEvents();
- }
- /**
- * 모바일용 터치홀드 이벤트를 바인딩합니다.
- */
- bindTouchHoldEvents() {
- if (!import_obsidian4.Platform.isMobile) {
- Logger.debug("\uB370\uC2A4\uD06C\uD1B1 \uD658\uACBD\uC5D0\uC11C\uB294 \uD130\uCE58\uD640\uB4DC \uC774\uBCA4\uD2B8\uB97C \uB4F1\uB85D\uD558\uC9C0 \uC54A\uC74C");
- return;
- }
- let touchTimer = null;
- let touchTarget = null;
- const TOUCH_HOLD_DURATION = 500;
- this.addEventListener(this.element, "touchstart", (e) => {
- const target = e.target;
- if (target.classList.contains("clickable-error") || target.classList.contains("error-original-compact")) {
- touchTarget = target;
- touchTimer = setTimeout(() => {
- if (touchTarget) {
- Logger.log(`\u{1F4F1} \uD130\uCE58\uD640\uB4DC \uD3B8\uC9D1 \uBAA8\uB4DC \uC9C4\uC785: ${touchTarget.textContent}`);
- if ("vibrate" in navigator) {
- navigator.vibrate(50);
- }
- let editingStarted = false;
- if (touchTarget.classList.contains("clickable-error")) {
- editingStarted = this.handlePreviewRightClick(touchTarget);
- } else if (touchTarget.classList.contains("error-original-compact")) {
- editingStarted = this.handleCardTextClick(touchTarget);
- }
- if (editingStarted) {
- this.enterMobileEditingMode();
- }
- touchTarget = null;
- touchTimer = null;
- }
- }, TOUCH_HOLD_DURATION);
- Logger.debug(`\u{1F4F1} \uD130\uCE58\uD640\uB4DC \uD0C0\uC774\uBA38 \uC2DC\uC791: ${target.textContent}`);
- }
- });
- this.addEventListener(this.element, "touchend", () => {
- if (touchTimer) {
- clearTimeout(touchTimer);
- touchTimer = null;
- Logger.debug("\u{1F4F1} \uD130\uCE58\uD640\uB4DC \uD0C0\uC774\uBA38 \uCDE8\uC18C (touchend)");
- }
- touchTarget = null;
- });
- this.addEventListener(this.element, "touchcancel", () => {
- if (touchTimer) {
- clearTimeout(touchTimer);
- touchTimer = null;
- Logger.debug("\u{1F4F1} \uD130\uCE58\uD640\uB4DC \uD0C0\uC774\uBA38 \uCDE8\uC18C (touchcancel)");
- }
- touchTarget = null;
- });
- this.addEventListener(this.element, "touchmove", (e) => {
- if (touchTimer && touchTarget) {
- const touch = e.touches[0];
- const rect = touchTarget.getBoundingClientRect();
- const moveThreshold = 10;
- const distanceX = Math.abs(touch.clientX - (rect.left + rect.width / 2));
- const distanceY = Math.abs(touch.clientY - (rect.top + rect.height / 2));
- if (distanceX > moveThreshold || distanceY > moveThreshold) {
- clearTimeout(touchTimer);
- touchTimer = null;
- touchTarget = null;
- Logger.debug("\u{1F4F1} \uD130\uCE58\uD640\uB4DC \uD0C0\uC774\uBA38 \uCDE8\uC18C (\uC774\uB3D9 \uAC10\uC9C0)");
- }
- }
- });
- Logger.log("\u{1F4F1} \uBAA8\uBC14\uC77C \uD130\uCE58\uD640\uB4DC \uC774\uBCA4\uD2B8 \uB4F1\uB85D \uC644\uB8CC");
- }
- /**
- * 적용 버튼 이벤트를 바인딩합니다.
- */
- bindApplyEvents() {
- const applyButton = this.element.querySelector("#applyCorrectionsButton");
- if (applyButton) {
- this.addEventListener(applyButton, "click", async () => {
- await this.applyCorrections();
- });
- }
- }
- /**
- * AI 분석 버튼 상태를 업데이트합니다.
- */
- async updateAiButtonState(aiBtn) {
- try {
- if (this.isAiAnalyzing) {
- aiBtn.textContent = "\u{1F916} \uBD84\uC11D \uC911...";
- aiBtn.disabled = true;
- aiBtn.classList.remove("ai-disabled");
- aiBtn.title = "AI \uBD84\uC11D\uC774 \uC9C4\uD589 \uC911\uC785\uB2C8\uB2E4...";
- } else if (this.aiService && await this.aiService.isAvailable()) {
- aiBtn.textContent = "\u{1F916} AI \uBD84\uC11D";
- aiBtn.disabled = false;
- aiBtn.classList.remove("ai-disabled");
- aiBtn.title = "AI\uAC00 \uCD5C\uC801\uC758 \uC218\uC815\uC0AC\uD56D\uC744 \uC790\uB3D9\uC73C\uB85C \uC120\uD0DD\uD569\uB2C8\uB2E4 (Shift+Cmd+A)";
- } else {
- aiBtn.textContent = "\u{1F916} AI \uBBF8\uC124\uC815";
- aiBtn.disabled = true;
- aiBtn.classList.add("ai-disabled");
- if (!this.aiService) {
- aiBtn.title = "AI \uC11C\uBE44\uC2A4\uB97C \uCD08\uAE30\uD654\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD50C\uB7EC\uADF8\uC778\uC744 \uB2E4\uC2DC \uB85C\uB4DC\uD574\uBCF4\uC138\uC694.";
- } else {
- const providerInfo = this.aiService.getProviderInfo();
- if (!providerInfo.available) {
- aiBtn.title = `AI \uAE30\uB2A5\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC124\uC815\uC5D0\uC11C ${providerInfo.provider} API \uD0A4\uB97C \uC785\uB825\uD558\uACE0 AI \uAE30\uB2A5\uC744 \uD65C\uC131\uD654\uD558\uC138\uC694.`;
- } else {
- aiBtn.title = "AI \uC11C\uBE44\uC2A4\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC124\uC815\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694.";
- }
- }
- }
- } catch (error) {
- Logger.error("AI \uBC84\uD2BC \uC0C1\uD0DC \uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328:", error);
- aiBtn.textContent = "\u{1F916} AI \uC624\uB958";
- aiBtn.disabled = true;
- aiBtn.classList.add("ai-disabled");
- aiBtn.title = "AI \uC11C\uBE44\uC2A4 \uC0C1\uD0DC \uD655\uC778 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
- }
- }
- /**
- * AI 분석 버튼 이벤트를 바인딩합니다.
- */
- bindAIAnalysisEvents() {
- const aiAnalyzeBtn = this.element.querySelector("#aiAnalyzeBtn");
- if (aiAnalyzeBtn && this.aiService) {
- this.addEventListener(aiAnalyzeBtn, "click", async () => {
- await this.performAIAnalysis();
- });
- }
- }
- /**
- * 미리보기 클릭을 처리합니다.
- */
- handlePreviewClick(target) {
- const correctionIndex = parseInt(target.dataset.correctionIndex || "-1");
- if (correctionIndex >= 0 && correctionIndex < this.config.corrections.length) {
- this.stateManager.toggleState(correctionIndex);
- this.updateDisplay();
- }
- }
- /**
- * 제안 버튼 클릭을 처리합니다.
- */
- handleSuggestionClick(target) {
- var _a;
- const correctionIndex = parseInt(target.dataset.correction || "0");
- const value = target.dataset.value || "";
- this.stateManager.setState(correctionIndex, value, value === ((_a = this.config.corrections[correctionIndex]) == null ? void 0 : _a.original), false);
- this.updateDisplay();
- }
- /**
- * 현재 편집 모드인지 확인합니다.
- */
- isInEditMode() {
- const editingInput = document.querySelector('input[data-edit-mode="true"]');
- return editingInput !== null && document.activeElement === editingInput;
- }
- /**
- * 미리보기 영역에서 우클릭 시 편집 모드로 전환합니다.
- * 일괄 동작: 펼치기 + 오토스크롤 + 편집 모드 진입
- */
- handlePreviewRightClick(target) {
- const correctionIndex = parseInt(target.dataset.correctionIndex || "0");
- Logger.debug(`\u{1F527} handlePreviewRightClick \uD638\uCD9C: index=${correctionIndex}, text="${target.textContent}"`);
- if (isNaN(correctionIndex) || correctionIndex < 0 || correctionIndex >= this.config.corrections.length) {
- Logger.debug("Invalid correction index for preview right click:", correctionIndex);
- return false;
- }
- const errorSummary = this.element.querySelector("#errorSummary");
- const wasCollapsed = errorSummary && errorSummary.classList.contains("collapsed");
- if (wasCollapsed) {
- errorSummary.classList.remove("collapsed");
- Logger.debug("\u{1F527} \uC624\uB958 \uC0C1\uC138 \uC601\uC5ED \uD3BC\uCE68");
- this.updateDisplay();
- }
- setTimeout(() => {
- const errorCard = this.element.querySelector(`[data-correction-index="${correctionIndex}"] .error-original-compact`);
- if (errorCard) {
- Logger.debug(`\u{1F527} \uD3B8\uC9D1 \uBAA8\uB4DC \uC9C4\uC785 - \uC624\uB958 \uC0C1\uC138 \uCE74\uB4DC \uCC3E\uC74C: index=${correctionIndex}`);
- errorCard.scrollIntoView({
- behavior: "smooth",
- block: "center"
- });
- Logger.debug("\u{1F527} \uC624\uD1A0\uC2A4\uD06C\uB864 \uC218\uD589");
- setTimeout(() => {
- this.enterCardEditMode(errorCard, correctionIndex);
- }, 300);
- } else {
- Logger.debug(`\u{1F527} \uC624\uB958 \uC0C1\uC138 \uCE74\uB4DC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C: index=${correctionIndex}`);
- }
- }, wasCollapsed ? 100 : 0);
- return true;
- }
- /**
- * 오류 상세 카드의 원본 텍스트 클릭 시 편집 모드로 전환합니다.
- */
- handleCardTextClick(target) {
- const correctionIndex = parseInt(target.dataset.correctionIndex || "0");
- Logger.debug(`\u{1F527} handleCardTextClick \uD638\uCD9C: index=${correctionIndex}, text="${target.textContent}"`);
- Logger.debug(`\u{1F527} target.dataset: ${JSON.stringify(target.dataset)}`);
- Logger.debug(`\u{1F527} target \uD074\uB798\uC2A4: ${target.className}`);
- if (isNaN(correctionIndex) || correctionIndex < 0 || correctionIndex >= this.config.corrections.length) {
- Logger.debug("Invalid correction index for card text click:", correctionIndex);
- return false;
- }
- Logger.debug(`\u{1F527} enterCardEditMode \uD638\uCD9C \uC608\uC815: index=${correctionIndex}`);
- this.enterCardEditMode(target, correctionIndex);
- return true;
- }
- /**
- * 카드 편집 모드로 진입합니다.
- */
- enterCardEditMode(originalElement, correctionIndex) {
- const currentText = originalElement.textContent || "";
- Logger.debug(`\u{1F527} enterCardEditMode \uC2DC\uC791: index=${correctionIndex}, currentText="${currentText}"`);
- const input = document.createElement("input");
- input.type = "text";
- input.value = currentText;
- input.className = "error-original-input";
- input.dataset.correctionIndex = correctionIndex.toString();
- input.dataset.editMode = "true";
- let isFinished = false;
- if (import_obsidian4.Platform.isMobile) {
- this.createMobileEditContainer(originalElement, input, correctionIndex, () => isFinished, (flag) => isFinished = flag);
- } else {
- this.createDesktopEditMode(originalElement, input, correctionIndex, () => isFinished, (flag) => isFinished = flag);
- }
- }
- /**
- * 데스크톱용 편집 모드를 생성합니다.
- */
- createDesktopEditMode(originalElement, input, correctionIndex, getIsFinished, setIsFinished) {
- var _a;
- const errorCard = originalElement.closest(".error-card");
- const hiddenElements = [];
- const hideElement = (element, className = HIDDEN_CLASS, logMessage) => {
- if (!element)
- return;
- element.classList.add(className);
- hiddenElements.push({ element, className });
- if (logMessage) {
- Logger.debug(logMessage);
- }
- };
- if (errorCard) {
- hideElement(
- errorCard.querySelector(".error-suggestions-compact"),
- HIDDEN_CLASS,
- `\u{1F5A5}\uFE0F \uC218\uC815 \uC81C\uC548 \uBC84\uD2BC \uC228\uAE40: index=${correctionIndex}`
- );
- hideElement(
- errorCard.querySelector(".error-exception-btn"),
- HIDDEN_CLASS,
- `\u{1F5A5}\uFE0F \uC608\uC678 \uCC98\uB9AC \uBC84\uD2BC \uC228\uAE40: index=${correctionIndex}`
- );
- }
- const finishEdit = () => {
- if (getIsFinished())
- return;
- setIsFinished(true);
- hiddenElements.forEach(({ element, className }) => {
- element.classList.remove(className);
- Logger.debug(`\u{1F5A5}\uFE0F \uC228\uACA8\uC9C4 \uC694\uC18C \uBCF5\uC6D0: ${element.className}`);
- });
- this.finishCardEdit(input, correctionIndex);
- };
- const cancelEdit = () => {
- if (getIsFinished())
- return;
- setIsFinished(true);
- hiddenElements.forEach(({ element, className }) => {
- element.classList.remove(className);
- Logger.debug(`\u{1F5A5}\uFE0F \uC228\uACA8\uC9C4 \uC694\uC18C \uBCF5\uC6D0 (\uCDE8\uC18C): ${element.className}`);
- });
- this.cancelCardEdit(input, correctionIndex);
- };
- (_a = originalElement.parentElement) == null ? void 0 : _a.replaceChild(input, originalElement);
- input.focus();
- input.select();
- input.addEventListener("keydown", (e) => {
- if (e.key === "Enter") {
- e.preventDefault();
- e.stopPropagation();
- finishEdit();
- } else if (e.key === "Escape") {
- e.preventDefault();
- e.stopPropagation();
- cancelEdit();
- }
- });
- input.addEventListener("blur", () => {
- finishEdit();
- });
- }
- /**
- * 모바일 편집 모드로 진입합니다.
- */
- enterMobileEditingMode() {
- if (!import_obsidian4.Platform.isMobile)
- return;
- const errorSummary = document.getElementById("errorSummary");
- if (errorSummary) {
- errorSummary.classList.add(ERROR_SUMMARY_EXPANDED_CLASS);
- errorSummary.classList.remove("collapsed");
- Logger.debug(`\u{1F4F1} \uC624\uB958 \uC0C1\uC138 \uC601\uC5ED \uC804\uCCB4 \uD655\uC7A5 (\uD3B8\uC9D1 \uBAA8\uB4DC)`);
- }
- }
- /**
- * 모바일 편집 모드에서 복원합니다.
- */
- exitMobileEditingMode() {
- if (!import_obsidian4.Platform.isMobile)
- return;
- const errorSummary = document.getElementById("errorSummary");
- if (errorSummary) {
- errorSummary.classList.remove(ERROR_SUMMARY_EXPANDED_CLASS);
- Logger.debug(`\u{1F4F1} \uC624\uB958 \uC0C1\uC138 \uC601\uC5ED \uC6D0\uB798 \uD06C\uAE30\uB85C \uBCF5\uC6D0`);
- }
- }
- /**
- * 모바일용 편집 컨테이너를 생성합니다.
- */
- createMobileEditContainer(originalElement, input, correctionIndex, getIsFinished, setIsFinished) {
- var _a;
- const hiddenElements = [];
- const stashElement = (element, className = FORCE_HIDDEN_CLASS, logMessage) => {
- if (!element)
- return;
- element.classList.add(className);
- hiddenElements.push({ element, className });
- if (logMessage) {
- Logger.debug(logMessage);
- }
- };
- const errorCard = originalElement.closest(".error-card");
- if (errorCard) {
- errorCard.classList.add("editing-mode");
- Logger.debug(`\u{1F4F1} editing-mode \uD074\uB798\uC2A4 \uCD94\uAC00: index=${correctionIndex}`);
- const suggestions = errorCard.querySelectorAll(".suggestion-compact");
- const keepOriginals = errorCard.querySelectorAll(".keep-original");
- const suggestionsContainer = errorCard.querySelector(".error-suggestions-compact");
- const exceptionBtn = errorCard.querySelector(".error-exception-btn");
- suggestions.forEach((btn) => {
- stashElement(btn);
- });
- keepOriginals.forEach((btn) => {
- stashElement(btn);
- });
- stashElement(
- suggestionsContainer,
- FORCE_HIDDEN_CLASS,
- `\u{1F4F1} \uC218\uC815 \uC81C\uC548 \uCEE8\uD14C\uC774\uB108 \uAC15\uC81C \uC228\uAE40: index=${correctionIndex}`
- );
- stashElement(
- exceptionBtn,
- FORCE_HIDDEN_CLASS,
- `\u{1F4F1} \uC608\uC678 \uCC98\uB9AC \uBC84\uD2BC \uAC15\uC81C \uC228\uAE40: index=${correctionIndex}`
- );
- }
- const container = document.createElement("div");
- container.className = "mobile-edit-container";
- const saveBtn = document.createElement("button");
- saveBtn.className = "mobile-edit-btn save";
- saveBtn.textContent = "\u2713";
- saveBtn.title = "\uC800\uC7A5";
- const cancelBtn = document.createElement("button");
- cancelBtn.className = "mobile-edit-btn cancel";
- cancelBtn.textContent = "\u2715";
- cancelBtn.title = "\uCDE8\uC18C";
- const finishEdit = () => {
- if (getIsFinished())
- return;
- setIsFinished(true);
- this.exitMobileEditingMode();
- if (errorCard) {
- errorCard.classList.remove("editing-mode");
- Logger.debug(`\u{1F4F1} editing-mode \uD074\uB798\uC2A4 \uC81C\uAC70: index=${correctionIndex}`);
- }
- hiddenElements.forEach(({ element, className }) => {
- element.classList.remove(className);
- Logger.debug(`\u{1F4F1} \uC228\uACA8\uC9C4 \uC694\uC18C \uBCF5\uC6D0: ${element.className}`);
- });
- Logger.debug(`\u{1F4F1} \uBAA8\uBC14\uC77C \uD3B8\uC9D1 \uBAA8\uB4DC \uC885\uB8CC - \uB808\uC774\uC544\uC6C3 \uBCF5\uC6D0`);
- this.finishCardEdit(input, correctionIndex);
- };
- const cancelEdit = () => {
- if (getIsFinished())
- return;
- setIsFinished(true);
- this.exitMobileEditingMode();
- if (errorCard) {
- errorCard.classList.remove("editing-mode");
- Logger.debug(`\u{1F4F1} editing-mode \uD074\uB798\uC2A4 \uC81C\uAC70 (\uCDE8\uC18C): index=${correctionIndex}`);
- }
- hiddenElements.forEach(({ element, className }) => {
- element.classList.remove(className);
- Logger.debug(`\u{1F4F1} \uC228\uACA8\uC9C4 \uC694\uC18C \uBCF5\uC6D0 (\uCDE8\uC18C): ${element.className}`);
- });
- Logger.debug(`\u{1F4F1} \uBAA8\uBC14\uC77C \uD3B8\uC9D1 \uBAA8\uB4DC \uCDE8\uC18C - \uB808\uC774\uC544\uC6C3 \uBCF5\uC6D0`);
- this.cancelCardEdit(input, correctionIndex);
- };
- saveBtn.addEventListener("click", (e) => {
- e.preventDefault();
- e.stopPropagation();
- finishEdit();
- });
- cancelBtn.addEventListener("click", (e) => {
- e.preventDefault();
- e.stopPropagation();
- cancelEdit();
- });
- input.addEventListener("keydown", (e) => {
- if (e.key === "Enter") {
- e.preventDefault();
- e.stopPropagation();
- finishEdit();
- } else if (e.key === "Escape") {
- e.preventDefault();
- e.stopPropagation();
- cancelEdit();
- }
- });
- container.appendChild(input);
- container.appendChild(saveBtn);
- container.appendChild(cancelBtn);
- (_a = originalElement.parentElement) == null ? void 0 : _a.replaceChild(container, originalElement);
- setTimeout(() => {
- input.focus();
- input.select();
- }, 100);
- Logger.log(`\u{1F4F1} \uBAA8\uBC14\uC77C \uD3B8\uC9D1 \uCEE8\uD14C\uC774\uB108 \uC0DD\uC131 \uC644\uB8CC: index=${correctionIndex}`);
- }
- /**
- * 카드 편집을 완료합니다.
- */
- finishCardEdit(input, correctionIndex) {
- const newValue = input.value.trim();
- const currentValue = this.stateManager.getValue(correctionIndex);
- Logger.debug(`\u{1F527} finishCardEdit \uD638\uCD9C: index=${correctionIndex}, newValue="${newValue}", currentValue="${currentValue}"`);
- if (newValue === "") {
- Logger.debug(`\u{1F527} \uBE48 \uAC12\uC73C\uB85C \uD3B8\uC9D1 \uCDE8\uC18C: index=${correctionIndex}`);
- this.cancelCardEdit(input, correctionIndex);
- return;
- }
- if (newValue === currentValue) {
- Logger.debug(`\u{1F527} \uAC12\uC774 \uBCC0\uACBD\uB418\uC9C0 \uC54A\uC544\uC11C \uD3B8\uC9D1 \uCDE8\uC18C: index=${correctionIndex}, value="${newValue}"`);
- this.cancelCardEdit(input, correctionIndex);
- return;
- }
- Logger.debug(`\u{1F527} setUserEdited \uD638\uCD9C \uC608\uC815: index=${correctionIndex}, value="${newValue}"`);
- this.stateManager.setUserEdited(correctionIndex, newValue);
- Logger.debug(`\u{1F527} updateDisplay \uD638\uCD9C \uC608\uC815`);
- this.updateDisplay();
- this.focusPreviewWordAfterEdit(correctionIndex);
- }
- /**
- * 카드 편집을 취소합니다.
- */
- cancelCardEdit(input, correctionIndex) {
- this.updateDisplay();
- }
- /**
- * 편집 완료 후 미리보기의 해당 단어로 포커스를 이동합니다.
- */
- focusPreviewWordAfterEdit(correctionIndex) {
- Logger.debug(`\u{1F3AF} \uD3B8\uC9D1 \uC644\uB8CC \uD6C4 \uBBF8\uB9AC\uBCF4\uAE30 \uD3EC\uCEE4\uC2A4 \uC774\uB3D9: index=${correctionIndex}`);
- setTimeout(() => {
- const rawCorrections = this.getCurrentCorrections();
- const uniqueCorrections = this.removeDuplicateCorrections(rawCorrections);
- const targetCorrectionIndex = uniqueCorrections.findIndex(
- (pc) => pc.originalIndex === correctionIndex
- );
- if (targetCorrectionIndex >= 0) {
- this.currentFocusIndex = targetCorrectionIndex;
- Logger.debug(`\u{1F3AF} \uD3EC\uCEE4\uC2A4 \uC778\uB371\uC2A4 \uC124\uC815: ${targetCorrectionIndex} (correctionIndex: ${correctionIndex})`);
- this.updateFocusHighlight();
- const previewElement = this.element.querySelector(".preview-text");
- if (previewElement) {
- const targetSpan = previewElement.querySelector(`[data-correction-index="${correctionIndex}"]`);
- if (targetSpan) {
- targetSpan.scrollIntoView({
- behavior: "smooth",
- block: "center",
- inline: "nearest"
- });
- Logger.debug(`\u{1F3AF} \uBBF8\uB9AC\uBCF4\uAE30 \uC2A4\uD06C\uB864 \uC644\uB8CC: \uB2E8\uC5B4 "${targetSpan.textContent}"`);
- targetSpan.classList.add("edit-completion-highlight");
- setTimeout(() => {
- targetSpan.classList.remove("edit-completion-highlight");
- }, 2e3);
- } else {
- Logger.debug(`\u{1F3AF} \uBBF8\uB9AC\uBCF4\uAE30\uC5D0\uC11C \uD574\uB2F9 \uB2E8\uC5B4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C: index=${correctionIndex}`);
- }
- }
- } else {
- Logger.debug(`\u{1F3AF} \uD604\uC7AC \uD398\uC774\uC9C0\uC5D0\uC11C \uD574\uB2F9 \uAD50\uC815\uC0AC\uD56D\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C: index=${correctionIndex}`);
- }
- }, 100);
- }
- /**
- * 페이지네이션을 재계산합니다.
- */
- recalculatePagination() {
- const errorSummary = this.element.querySelector("#errorSummary");
- const isErrorExpanded = errorSummary ? !errorSummary.classList.contains("collapsed") : false;
- const previewElement = this.element.querySelector("#resultPreview");
- this.charsPerPage = calculateDynamicCharsPerPage(previewElement, isErrorExpanded);
- const trimmedText = this.config.selectedText.trim();
- this.pageBreaks = splitTextIntoPages(trimmedText, this.charsPerPage);
- this.totalPreviewPages = this.pageBreaks.length;
- if (this.currentPreviewPage >= this.totalPreviewPages) {
- this.currentPreviewPage = Math.max(0, this.totalPreviewPages - 1);
- }
- Logger.debug(`Recalculated pagination: Chars per page: ${this.charsPerPage}, Total pages: ${this.totalPreviewPages}, Current page: ${this.currentPreviewPage}`);
- }
- /**
- * 디스플레이를 업데이트합니다.
- */
- updateDisplay() {
- const previewElement = this.element.querySelector("#resultPreview");
- if (previewElement) {
- this.updatePreviewContent(previewElement);
- }
- const errorSummaryContent = this.element.querySelector("#errorSummaryContent");
- if (errorSummaryContent) {
- clearElement(errorSummaryContent);
- const errorSummaryDOM = this.generateErrorSummaryDOM();
- errorSummaryContent.appendChild(errorSummaryDOM);
- }
- this.updateErrorDetailStyles();
- this.updatePaginationControls();
- const errorCountBadge = this.element.querySelector("#errorCountBadge");
- if (errorCountBadge) {
- errorCountBadge.textContent = this.getErrorStateCount().toString();
- }
- }
- /**
- * 오류 상세 항목의 스타일을 상태에 따라 업데이트합니다.
- */
- updateErrorDetailStyles() {
- const errorItems = this.element.querySelectorAll(".error-item-compact");
- errorItems.forEach((item, index) => {
- const correctionIndex = parseInt(item.getAttribute("data-correction-index") || "0");
- const originalText = item.querySelector(".error-original-compact");
- if (originalText) {
- originalText.classList.remove("corrected", "exception-processed", "original-kept");
- const currentValue = this.stateManager.getValue(correctionIndex);
- const isException = this.stateManager.isExceptionState(correctionIndex);
- const isOriginalKept = this.stateManager.isOriginalKeptState(correctionIndex);
- const correction = this.config.corrections[correctionIndex];
- if (correction) {
- if (isException) {
- originalText.classList.add("exception-processed");
- } else if (isOriginalKept) {
- originalText.classList.add("original-kept");
- } else if (currentValue !== correction.original) {
- originalText.classList.add("corrected");
- }
- }
- }
- });
- }
- /**
- * 페이지네이션 컨트롤을 업데이트합니다.
- */
- updatePaginationControls() {
- const paginationContainer = this.element.querySelector("#paginationContainer");
- const prevButton = this.element.querySelector("#prevPreviewPage");
- const nextButton = this.element.querySelector("#nextPreviewPage");
- const pageInfo = this.element.querySelector("#previewPageInfo");
- const pageCharsInfo = this.element.querySelector("#pageCharsInfo");
- if (paginationContainer) {
- if (this.isLongText && this.totalPreviewPages > 1) {
- paginationContainer.className = "pagination-controls";
- if (!prevButton || !nextButton) {
- clearElement(paginationContainer);
- const paginationFragment = this.createPaginationControls();
- paginationContainer.appendChild(paginationFragment);
- this.bindPaginationEvents();
- }
- } else {
- paginationContainer.className = "pagination-container-hidden";
- }
- }
- const updatedPrevButton = this.element.querySelector("#prevPreviewPage");
- const updatedNextButton = this.element.querySelector("#nextPreviewPage");
- const updatedPageInfo = this.element.querySelector("#previewPageInfo");
- const updatedPageCharsInfo = this.element.querySelector("#pageCharsInfo");
- if (updatedPrevButton)
- updatedPrevButton.disabled = this.currentPreviewPage === 0;
- if (updatedNextButton)
- updatedNextButton.disabled = this.currentPreviewPage === this.totalPreviewPages - 1;
- if (updatedPageInfo)
- updatedPageInfo.textContent = `${this.currentPreviewPage + 1} / ${this.totalPreviewPages}`;
- if (updatedPageCharsInfo)
- updatedPageCharsInfo.textContent = `${this.charsPerPage}\uC790`;
- }
- /**
- * 교정사항을 적용합니다.
- */
- async applyCorrections() {
- Logger.log("\u{1F680} applyCorrections \uC2DC\uC791");
- const markdownView = this.app.workspace.getActiveViewOfType(import_obsidian4.MarkdownView);
- const currentMode = (markdownView == null ? void 0 : markdownView.getMode) ? markdownView.getMode() : "unknown";
- Logger.log(`\u{1F4DD} \uD604\uC7AC \uC5D0\uB514\uD130 \uBAA8\uB4DC: ${currentMode}`);
- const result = this.stateManager.applyCorrections(this.config.selectedText);
- Logger.log("\u{1F504} \uC5D0\uB514\uD130 \uC801\uC6A9 \uC2DC\uC791:", {
- originalTextLength: this.config.selectedText.length,
- finalTextLength: result.finalText.length,
- start: this.config.start,
- end: this.config.end,
- changed: this.config.selectedText !== result.finalText,
- exceptionWordsCount: result.exceptionWords.length,
- mode: currentMode
- });
- try {
- if (currentMode === "preview") {
- Logger.log("\u{1F4D6} \uC77D\uAE30\uBAA8\uB4DC \uAC10\uC9C0 - Vault.process() \uC0AC\uC6A9");
- const file = markdownView == null ? void 0 : markdownView.file;
- if (!file) {
- throw new Error("\uD30C\uC77C \uC815\uBCF4\uB97C \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- await this.app.vault.process(file, (content) => {
- const lines = content.split("\n");
- let currentLine = 0;
- let currentCol = 0;
- for (let i = 0; i < this.config.start.line; i++) {
- currentLine++;
- }
- const beforeStart = content.substring(0, this.getOffsetFromPosition(content, this.config.start));
- const afterEnd = content.substring(this.getOffsetFromPosition(content, this.config.end));
- return beforeStart + result.finalText + afterEnd;
- });
- Logger.log("\u2705 Vault.process() \uC131\uACF5\uC801\uC73C\uB85C \uC644\uB8CC\uB428");
- } else {
- this.config.editor.replaceRange(result.finalText, this.config.start, this.config.end);
- Logger.log("\u2705 editor.replaceRange \uC131\uACF5\uC801\uC73C\uB85C \uD638\uCD9C\uB428");
- const appliedText = this.config.editor.getRange(this.config.start, this.config.end);
- const actuallyApplied = appliedText === result.finalText;
- Logger.log(`\u{1F50D} \uC801\uC6A9 \uAC80\uC99D: \uC131\uACF5=${actuallyApplied}`, {
- expected: result.finalText.substring(0, 50) + (result.finalText.length > 50 ? "..." : ""),
- actual: appliedText.substring(0, 50) + (appliedText.length > 50 ? "..." : ""),
- lengthMatch: appliedText.length === result.finalText.length
- });
- }
- } catch (error) {
- Logger.error("\u274C \uD14D\uC2A4\uD2B8 \uC801\uC6A9 \uC2E4\uD328:", error);
- new import_obsidian4.Notice("\uD14D\uC2A4\uD2B8 \uC801\uC6A9 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
- return;
- }
- if (result.exceptionWords.length > 0 && this.config.onExceptionWordsAdded) {
- this.config.onExceptionWordsAdded(result.exceptionWords);
- }
- this.close();
- }
- /**
- * 에디터 위치를 문자열 오프셋으로 변환합니다.
- */
- getOffsetFromPosition(content, pos) {
- const lines = content.split("\n");
- let offset = 0;
- for (let i = 0; i < pos.line; i++) {
- offset += lines[i].length + 1;
- }
- offset += pos.ch;
- return offset;
- }
- /**
- * 팝업을 표시합니다.
- */
- show() {
- document.body.appendChild(this.element);
- if (import_obsidian4.Platform.isMobile) {
- this.element.classList.add("mobile-popup");
- Logger.log("Mobile mode detected, added mobile-popup class");
- }
- requestAnimationFrame(() => {
- Logger.log("DOM \uCD94\uAC00 \uD6C4 \uD398\uC774\uC9C0\uB124\uC774\uC158 \uC7AC\uACC4\uC0B0 \uC2DC\uC791");
- this.recalculatePagination();
- this.updateDisplay();
- Logger.log("DOM \uCD94\uAC00 \uD6C4 \uD398\uC774\uC9C0\uB124\uC774\uC158 \uC7AC\uACC4\uC0B0 \uC644\uB8CC");
- });
- }
- /**
- * AI 분석을 수행합니다.
- * ⭐ NEW: 형태소 정보와 함께 분석
- */
- async performAIAnalysis() {
- var _a, _b, _c, _d, _e, _f, _g, _h;
- Logger.log("performAIAnalysis \uD638\uCD9C\uB428:", {
- hasAiService: !!this.aiService,
- isAiAnalyzing: this.isAiAnalyzing,
- aiServiceAvailable: (_a = this.aiService) == null ? void 0 : _a.isAvailable(),
- aiServiceSettings: (_b = this.aiService) == null ? void 0 : _b.getSettings()
- });
- if (!this.aiService || this.isAiAnalyzing) {
- Logger.warn("AI \uBD84\uC11D \uC911\uB2E8: aiService \uC5C6\uC74C \uB610\uB294 \uC774\uBBF8 \uBD84\uC11D \uC911");
- return;
- }
- if (!this.aiService.isAvailable()) {
- Logger.error("AI \uC11C\uBE44\uC2A4 \uC0AC\uC6A9 \uBD88\uAC00: \uAE30\uB2A5 \uBE44\uD65C\uC131\uD654 \uB610\uB294 API \uD0A4 \uC5C6\uC74C");
- new import_obsidian4.Notice("\u274C AI \uAE30\uB2A5\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uAC70\uB098 API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694.", 5e3);
- return;
- }
- try {
- Logger.debug("\u{1F50D} performAIAnalysis \uBA54\uC778 try \uBE14\uB85D \uC9C4\uC785");
- this.isAiAnalyzing = true;
- const aiBtn = this.element.querySelector("#aiAnalyzeBtn");
- if (aiBtn) {
- aiBtn.disabled = true;
- aiBtn.textContent = "\u{1F916} \uBD84\uC11D \uC911...";
- }
- Logger.log("AI \uBD84\uC11D \uC2DC\uC791 \uC911...");
- Logger.debug("\u{1F50D} \uD615\uD0DC\uC18C \uBD84\uC11D \uC815\uBCF4 \uD655\uC778 \uC911...");
- let morphemeInfo = this.config.morphemeInfo || null;
- if (morphemeInfo) {
- Logger.debug("\u2705 orchestrator\uC5D0\uC11C \uD615\uD0DC\uC18C \uBD84\uC11D \uC815\uBCF4 \uC804\uB2EC\uBC1B\uC74C:", {
- hasMorphemeInfo: !!morphemeInfo,
- sentencesCount: ((_c = morphemeInfo == null ? void 0 : morphemeInfo.sentences) == null ? void 0 : _c.length) || 0,
- tokensCount: ((_d = morphemeInfo == null ? void 0 : morphemeInfo.sentences) == null ? void 0 : _d.reduce((sum, s) => {
- var _a2;
- return sum + (((_a2 = s.tokens) == null ? void 0 : _a2.length) || 0);
- }, 0)) || 0,
- firstFewTokens: ((_h = (_g = (_f = (_e = morphemeInfo == null ? void 0 : morphemeInfo.sentences) == null ? void 0 : _e[0]) == null ? void 0 : _f.tokens) == null ? void 0 : _g.slice(0, 3)) == null ? void 0 : _h.map((t) => {
- var _a2;
- return (_a2 = t.text) == null ? void 0 : _a2.content;
- })) || []
- });
- } else {
- Logger.warn("\u274C \uD615\uD0DC\uC18C \uBD84\uC11D \uC815\uBCF4 \uC5C6\uC74C - \uD328\uD134 \uB9E4\uCE6D\uB9CC \uC0AC\uC6A9");
- }
- const currentStates = this.stateManager.getAllStates();
- const analysisRequest = {
- originalText: this.config.selectedText,
- corrections: this.config.corrections,
- contextWindow: morphemeInfo ? 30 : 100,
- // ⭐ NEW: 형태소 정보 있으면 더 적은 컨텍스트 (토큰 절약)
- currentStates,
- // 현재 상태 전달
- editor: this.config.editor,
- // ⭐ NEW: Editor 인스턴스 전달 (구조화된 컨텍스트 추출용)
- file: this.config.file,
- // ⭐ NEW: File 인스턴스 전달 (메타데이터 정보용)
- enhancedContext: true,
- // ⭐ NEW: 향상된 컨텍스트 추출 활성화
- onProgress: (current, total, status) => {
- const aiBtn2 = this.element.querySelector("#aiAnalyzeBtn");
- if (aiBtn2) {
- aiBtn2.textContent = `\u{1F916} ${status}`;
- }
- }
- };
- if (await this.checkTokenUsageWarning(analysisRequest) === false) {
- return;
- }
- this.aiAnalysisResults = await this.aiService.analyzeCorrections(analysisRequest, morphemeInfo != null ? morphemeInfo : void 0);
- Logger.log("AI \uBD84\uC11D \uC644\uB8CC:", this.aiAnalysisResults);
- this.applyAIAnalysisResults();
- this.updateDisplay();
- new import_obsidian4.Notice(`\u{1F916} AI\uAC00 ${this.aiAnalysisResults.length}\uAC1C\uC758 \uC218\uC815 \uC81C\uC548\uC744 \uBD84\uC11D\uD588\uC2B5\uB2C8\uB2E4.`, 3e3);
- } catch (error) {
- Logger.error("AI \uBD84\uC11D \uC2E4\uD328:", error);
- new import_obsidian4.Notice(`\u274C AI \uBD84\uC11D \uC2E4\uD328: ${error.message}`, 5e3);
- } finally {
- this.isAiAnalyzing = false;
- const aiBtn = this.element.querySelector("#aiAnalyzeBtn");
- if (aiBtn) {
- await this.updateAiButtonState(aiBtn);
- }
- }
- }
- /**
- * 오류 요약 섹션의 DOM 구조를 생성합니다.
- */
- generateErrorSummaryDOM() {
- Logger.debug("========= generateErrorSummaryDOM \uC2DC\uC791 =========");
- const container = document.createElement("div");
- const rawCorrections = this.getCurrentCorrections();
- Logger.debug(`RAW corrections: ${rawCorrections.length}\uAC1C`);
- const currentCorrections = this.removeDuplicateCorrections(rawCorrections);
- Logger.debug(`\uC911\uBCF5 \uC81C\uAC70 \uD6C4 corrections: ${currentCorrections.length}\uAC1C`);
- if (currentCorrections.length === 0) {
- const placeholder = document.createElement("div");
- placeholder.className = "error-placeholder";
- const icon = document.createElement("div");
- icon.className = "placeholder-icon";
- icon.textContent = "\u2713";
- placeholder.appendChild(icon);
- const text = document.createElement("div");
- text.className = "placeholder-text";
- text.textContent = "\uC774 \uD398\uC774\uC9C0\uC5D0\uB294 \uBC1C\uACAC\uB41C \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4";
- placeholder.appendChild(text);
- const subtext = document.createElement("div");
- subtext.className = "placeholder-subtext";
- subtext.textContent = "\uB2E4\uB978 \uD398\uC774\uC9C0\uC5D0\uC11C \uC624\uB958\uB97C \uD655\uC778\uD558\uC138\uC694";
- placeholder.appendChild(subtext);
- container.appendChild(placeholder);
- Logger.debug("\uC624\uB958 \uC5C6\uC74C - \uD50C\uB808\uC774\uC2A4\uD640\uB354 \uC0DD\uC131");
- Logger.debug("========= generateErrorSummaryDOM \uC885\uB8CC (\uC624\uB958 \uC5C6\uC74C) =========");
- return container;
- }
- Logger.debug("DOM \uC0DD\uC131 \uC2DC\uC791 - \uC911\uBCF5 \uC81C\uAC70 \uC644\uB8CC\uB41C corrections \uC0AC\uC6A9");
- currentCorrections.forEach((pageCorrection, index) => {
- const actualIndex = pageCorrection.originalIndex;
- const correction = pageCorrection.correction;
- const isOriginalKept = this.stateManager.isOriginalKeptState(actualIndex);
- const suggestions = correction.corrected.slice(0, 3);
- Logger.debug(`[${index}] DOM \uC0DD\uC131 \uC911: "${correction.original}" (\uACE0\uC720ID: ${pageCorrection.uniqueId}, \uC2E4\uC81C\uC778\uB371\uC2A4: ${actualIndex})`);
- const errorItem = document.createElement("div");
- errorItem.className = `error-item-compact ${isOriginalKept ? "spell-original-kept" : ""}`;
- errorItem.setAttribute("data-correction-index", actualIndex.toString());
- errorItem.setAttribute("data-unique-id", pageCorrection.uniqueId);
- Logger.debug(`[${index}] DOM \uC18D\uC131 \uC124\uC815: data-correction-index="${actualIndex}", data-unique-id="${pageCorrection.uniqueId}"`);
- const errorRow = document.createElement("div");
- errorRow.className = "error-row";
- const errorOriginal = document.createElement("div");
- const isUserEdited = this.stateManager.isUserEditedState(actualIndex);
- const stateClass = isUserEdited ? "user-edited" : isOriginalKept ? "original-kept" : this.stateManager.isExceptionState(actualIndex) ? "exception-processed" : this.stateManager.getValue(actualIndex) !== correction.original ? "corrected" : "";
- errorOriginal.className = `error-original-compact ${stateClass}`;
- errorOriginal.setAttribute("data-correction-index", actualIndex.toString());
- errorOriginal.textContent = this.stateManager.getValue(actualIndex);
- errorRow.appendChild(errorOriginal);
- const suggestionsContainer = document.createElement("div");
- suggestionsContainer.className = "error-suggestions-compact";
- suggestions.forEach((suggestion) => {
- const suggestionSpan = document.createElement("span");
- suggestionSpan.className = `suggestion-compact ${this.stateManager.isSelected(actualIndex, suggestion) ? "selected" : ""}`;
- suggestionSpan.setAttribute("data-value", suggestion);
- suggestionSpan.setAttribute("data-correction", actualIndex.toString());
- if (isOriginalKept) {
- suggestionSpan.setAttribute("disabled", "");
- }
- suggestionSpan.textContent = suggestion;
- suggestionsContainer.appendChild(suggestionSpan);
- });
- const keepOriginalSpan = document.createElement("span");
- keepOriginalSpan.className = `suggestion-compact ${this.stateManager.isSelected(actualIndex, correction.original) ? "selected" : ""} keep-original`;
- keepOriginalSpan.setAttribute("data-value", correction.original);
- keepOriginalSpan.setAttribute("data-correction", actualIndex.toString());
- if (isOriginalKept) {
- keepOriginalSpan.setAttribute("disabled", "");
- }
- keepOriginalSpan.textContent = "\uC608\uC678\uCC98\uB9AC";
- suggestionsContainer.appendChild(keepOriginalSpan);
- errorRow.appendChild(suggestionsContainer);
- errorItem.appendChild(errorRow);
- const errorHelp = document.createElement("div");
- errorHelp.className = "error-help-compact";
- errorHelp.textContent = correction.help;
- errorItem.appendChild(errorHelp);
- const aiResult = this.aiAnalysisResults.find((result) => result.correctionIndex === actualIndex);
- if (aiResult || isOriginalKept) {
- const aiAnalysis = document.createElement("div");
- aiAnalysis.className = "ai-analysis-result";
- if (aiResult) {
- const aiConfidence = document.createElement("div");
- aiConfidence.className = "ai-confidence";
- aiConfidence.textContent = "\u{1F916} \uC2E0\uB8B0\uB3C4: ";
- const confidenceScore = document.createElement("span");
- confidenceScore.className = "confidence-score";
- confidenceScore.textContent = `${aiResult.confidence}%`;
- aiConfidence.appendChild(confidenceScore);
- aiAnalysis.appendChild(aiConfidence);
- const aiReasoning = document.createElement("div");
- aiReasoning.className = "ai-reasoning";
- aiReasoning.textContent = aiResult.reasoning;
- aiAnalysis.appendChild(aiReasoning);
- } else if (isOriginalKept) {
- const aiReasoning = document.createElement("div");
- aiReasoning.className = "ai-reasoning";
- aiReasoning.textContent = "\uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uC120\uD0DD\uD588\uAC70\uB098, \uC608\uC678 \uB2E8\uC5B4\uB85C \uB4F1\uB85D\uB41C \uD56D\uBAA9\uC785\uB2C8\uB2E4.";
- aiAnalysis.appendChild(aiReasoning);
- }
- errorItem.appendChild(aiAnalysis);
- }
- container.appendChild(errorItem);
- Logger.debug(`[${index}] DOM \uC0DD\uC131 \uC644\uB8CC: "${correction.original}"`);
- });
- Logger.debug(`DOM \uC0DD\uC131 \uC644\uB8CC: \uCD1D ${currentCorrections.length}\uAC1C \uC624\uB958 \uD56D\uBAA9`);
- Logger.debug("========= generateErrorSummaryDOM \uC885\uB8CC =========");
- return container;
- }
- /**
- * 페이지네이션 컨트롤의 DOM 구조를 생성합니다.
- */
- createPaginationControls() {
- const fragment = document.createDocumentFragment();
- const prevButton = document.createElement("button");
- prevButton.className = "pagination-btn";
- prevButton.id = "prevPreviewPage";
- prevButton.textContent = "\uC774\uC804";
- if (this.currentPreviewPage === 0) {
- prevButton.disabled = true;
- }
- fragment.appendChild(prevButton);
- const pageInfo = document.createElement("span");
- pageInfo.className = "page-info";
- pageInfo.id = "previewPageInfo";
- pageInfo.textContent = `${this.currentPreviewPage + 1} / ${this.totalPreviewPages}`;
- fragment.appendChild(pageInfo);
- const nextButton = document.createElement("button");
- nextButton.className = "pagination-btn";
- nextButton.id = "nextPreviewPage";
- nextButton.textContent = "\uB2E4\uC74C";
- if (this.currentPreviewPage === this.totalPreviewPages - 1) {
- nextButton.disabled = true;
- }
- fragment.appendChild(nextButton);
- const charsInfo = document.createElement("span");
- charsInfo.className = "page-chars-info";
- charsInfo.id = "pageCharsInfo";
- charsInfo.textContent = `${this.charsPerPage}\uC790`;
- fragment.appendChild(charsInfo);
- return fragment;
- }
- /**
- * 토큰 경고 모달의 DOM 구조를 생성합니다.
- * ⭐ 형태소 최적화 정보 포함
- */
- createTokenWarningModal(tokenUsage, isOverMaxTokens, maxTokens) {
- const content = document.createElement("div");
- content.className = "token-warning-content";
- const header = content.appendChild(document.createElement("div"));
- header.className = "token-warning-header";
- const headerIcon = header.appendChild(document.createElement("div"));
- headerIcon.className = "token-warning-header-icon";
- headerIcon.textContent = "\u26A1";
- const headerInfo = header.appendChild(document.createElement("div"));
- const title = headerInfo.appendChild(document.createElement("h3"));
- title.className = "token-warning-title";
- title.textContent = isOverMaxTokens ? "\uD1A0\uD070 \uC0AC\uC6A9\uB7C9 \uD655\uC778" : "\uD1A0\uD070 \uC0AC\uC6A9\uB7C9 \uC548\uB0B4";
- const description = headerInfo.appendChild(document.createElement("p"));
- description.className = "token-warning-description";
- description.textContent = isOverMaxTokens ? "\uC124\uC815\uB41C \uD55C\uACC4\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4" : "\uC608\uC0C1 \uC0AC\uC6A9\uB7C9\uC774 \uB192\uC2B5\uB2C8\uB2E4";
- const details = content.appendChild(document.createElement("div"));
- details.className = "token-warning-details";
- const stats = details.appendChild(document.createElement("div"));
- stats.className = "token-warning-stats";
- const totalTokenItem = stats.appendChild(document.createElement("div"));
- totalTokenItem.className = "token-stat-item";
- const totalTokenNumber = totalTokenItem.appendChild(document.createElement("div"));
- totalTokenNumber.className = "token-stat-number";
- totalTokenNumber.textContent = tokenUsage.totalEstimated.toLocaleString();
- const totalTokenLabel = totalTokenItem.appendChild(document.createElement("div"));
- totalTokenLabel.className = "token-stat-label";
- totalTokenLabel.textContent = "\uCD1D \uD1A0\uD070";
- const costItem = stats.appendChild(document.createElement("div"));
- costItem.className = "token-stat-item";
- const costNumber = costItem.appendChild(document.createElement("div"));
- costNumber.className = "token-stat-number orange";
- costNumber.textContent = tokenUsage.estimatedCost;
- const costLabel = costItem.appendChild(document.createElement("div"));
- costLabel.className = "token-stat-label";
- costLabel.textContent = "\uC608\uC0C1 \uBE44\uC6A9";
- const recommendation = details.appendChild(document.createElement("div"));
- recommendation.className = "token-warning-recommendation";
- const recHeader = recommendation.appendChild(document.createElement("div"));
- recHeader.className = "token-warning-recommendation-header";
- const recContent = recHeader.appendChild(document.createElement("div"));
- recContent.className = "token-warning-recommendation-content";
- const recTitle = recContent.appendChild(document.createElement("div"));
- recTitle.className = "token-warning-recommendation-title";
- recTitle.textContent = "\uC0AC\uC6A9\uB7C9 \uC138\uBD80\uC0AC\uD56D";
- const recText = recContent.appendChild(document.createElement("div"));
- recText.className = "token-warning-recommendation-text";
- const detailText = `\uC785\uB825: ${tokenUsage.inputTokens.toLocaleString()} \u2022 \uCD9C\uB825: ${tokenUsage.estimatedOutputTokens.toLocaleString()}`;
- recText.textContent = detailText;
- if (isOverMaxTokens) {
- const overLimit = content.appendChild(document.createElement("div"));
- overLimit.className = "token-warning-over-limit";
- const overLimitContent = overLimit.appendChild(document.createElement("div"));
- overLimitContent.className = "token-warning-over-limit-content";
- const overLimitIcon = overLimitContent.appendChild(document.createElement("div"));
- overLimitIcon.className = "token-warning-over-limit-icon";
- overLimitIcon.textContent = "!";
- const overLimitText = overLimitContent.appendChild(document.createElement("div"));
- overLimitText.className = "token-warning-over-limit-text";
- const overLimitTitle = overLimitText.appendChild(document.createElement("div"));
- overLimitTitle.className = "token-warning-over-limit-title";
- overLimitTitle.textContent = "\uC124\uC815\uB41C \uCD5C\uB300 \uD1A0\uD070\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4";
- const overLimitDesc = overLimitText.appendChild(document.createElement("div"));
- overLimitDesc.className = "token-warning-over-limit-description";
- overLimitDesc.textContent = `\uD604\uC7AC \uC124\uC815: ${maxTokens.toLocaleString()} \uD1A0\uD070 \u2192 \uCD08\uACFC\uB7C9: ${(tokenUsage.totalEstimated - maxTokens).toLocaleString()} \uD1A0\uD070`;
- }
- const actions = content.appendChild(document.createElement("div"));
- actions.className = "token-warning-actions";
- const cancelBtn = actions.appendChild(document.createElement("button"));
- cancelBtn.id = "token-warning-cancel";
- cancelBtn.className = "token-warning-btn token-warning-btn-cancel";
- cancelBtn.textContent = "\uCDE8\uC18C";
- if (isOverMaxTokens) {
- const updateSettingsBtn = actions.appendChild(document.createElement("button"));
- updateSettingsBtn.id = "token-warning-update-settings";
- updateSettingsBtn.className = "token-warning-btn token-warning-btn-settings";
- updateSettingsBtn.textContent = "\uC124\uC815 \uC5C5\uB370\uC774\uD2B8";
- }
- const proceedBtn = actions.appendChild(document.createElement("button"));
- proceedBtn.id = "token-warning-proceed";
- proceedBtn.className = "token-warning-btn token-warning-btn-proceed";
- proceedBtn.textContent = isOverMaxTokens ? "\uC774\uBC88\uB9CC \uC9C4\uD589" : "\uACC4\uC18D \uC9C4\uD589";
- const keyboardHint = content.appendChild(document.createElement("div"));
- keyboardHint.className = "token-warning-keyboard-hint";
- keyboardHint.textContent = "\u{1F4A1} \uD0A4\uBCF4\uB4DC \uB2E8\uCD95\uD0A4: enter(\uC9C4\uD589), esc(\uCDE8\uC18C)";
- return content;
- }
- /**
- * 형태소 최적화를 고려한 토큰 사용량을 추정합니다.
- * ⭐ NEW: 실제 사용될 프롬프트 기반 정확한 추정
- */
- async estimateTokenUsageWithMorphemes(request) {
- var _a, _b;
- try {
- const hasMultipleCorrections = request.corrections.length > 1;
- const morphemeOptimized = hasMultipleCorrections;
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0\uC6A9 \uD615\uD0DC\uC18C \uCD5C\uC801\uD654 \uCD94\uC815:", {
- correctionsCount: request.corrections.length,
- estimatedOptimization: morphemeOptimized,
- reason: morphemeOptimized ? "\uBCF5\uC218 \uAD50\uC815\uC73C\uB85C \uCEE8\uD14D\uC2A4\uD2B8 \uCD95\uC18C \uC608\uC0C1" : "\uB2E8\uC77C \uAD50\uC815\uC73C\uB85C \uCD5C\uC801\uD654 \uBD88\uD544\uC694"
- });
- const adjustedRequest = {
- ...request,
- contextWindow: morphemeOptimized ? 30 : request.contextWindow || 100
- };
- const baseEstimation = ((_a = this.aiService) == null ? void 0 : _a.estimateTokenUsage(adjustedRequest)) || {
- inputTokens: 0,
- estimatedOutputTokens: 0,
- totalEstimated: 0,
- estimatedCost: "$0.00"
- };
- const morphemeTokens = morphemeOptimized ? 50 : 0;
- const finalEstimation = {
- inputTokens: baseEstimation.inputTokens + morphemeTokens,
- estimatedOutputTokens: baseEstimation.estimatedOutputTokens,
- totalEstimated: baseEstimation.totalEstimated + morphemeTokens,
- estimatedCost: baseEstimation.estimatedCost,
- morphemeOptimized
- };
- Logger.debug("\uD615\uD0DC\uC18C \uCD5C\uC801\uD654 \uBC18\uC601 \uD1A0\uD070 \uCD94\uC815:", {
- before: baseEstimation.totalEstimated,
- after: finalEstimation.totalEstimated,
- contextReduction: morphemeOptimized ? 100 - 30 : 0,
- // 70토큰 절약
- morphemeTokens,
- netChange: morphemeOptimized ? morphemeTokens - 70 : 0,
- // 순 변화량
- optimized: morphemeOptimized
- });
- return finalEstimation;
- } catch (error) {
- Logger.error("\uD1A0\uD070 \uCD94\uC815 \uC2E4\uD328, \uAE30\uBCF8\uAC12 \uC0AC\uC6A9:", error);
- Logger.error("\uC5D0\uB7EC \uC2A4\uD0DD:", error == null ? void 0 : error.stack);
- const fallbackEstimation = ((_b = this.aiService) == null ? void 0 : _b.estimateTokenUsage(request)) || {
- inputTokens: 0,
- estimatedOutputTokens: 0,
- totalEstimated: 0,
- estimatedCost: "$0.00"
- };
- Logger.warn("\uD3F4\uBC31 \uD1A0\uD070 \uCD94\uC815 \uC0AC\uC6A9:", fallbackEstimation);
- return {
- ...fallbackEstimation,
- morphemeOptimized: false
- };
- }
- }
- /**
- * 토큰 사용량 경고를 확인하고 사용자 확인을 받습니다.
- */
- async checkTokenUsageWarning(request) {
- var _a;
- const aiSettings = (_a = this.aiService) == null ? void 0 : _a.getProviderInfo();
- if (!this.aiService || !(aiSettings == null ? void 0 : aiSettings.available)) {
- return true;
- }
- const aiServiceSettings = this.aiService.getSettings();
- const showWarning = aiServiceSettings.showTokenWarning;
- const threshold = aiServiceSettings.tokenWarningThreshold;
- const maxTokens = aiServiceSettings.maxTokens;
- if (!showWarning) {
- return true;
- }
- const tokenUsage = await this.estimateTokenUsageWithMorphemes(request);
- const isOverMaxTokens = tokenUsage.totalEstimated > maxTokens;
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC \uD1A0\uD070 \uC0AC\uC6A9\uB7C9:", {
- total: tokenUsage.totalEstimated,
- input: tokenUsage.inputTokens,
- output: tokenUsage.estimatedOutputTokens,
- cost: tokenUsage.estimatedCost,
- morphemeOptimized: tokenUsage.morphemeOptimized,
- threshold,
- maxTokens
- });
- if (tokenUsage.totalEstimated < threshold && !isOverMaxTokens) {
- return true;
- }
- return new Promise((resolve) => {
- var _a2, _b, _c;
- const modal = document.createElement("div");
- modal.className = "token-warning-modal";
- const modalContent = this.createTokenWarningModal(tokenUsage, isOverMaxTokens, maxTokens);
- modal.appendChild(modalContent);
- document.body.appendChild(modal);
- modal.setAttribute("tabindex", "-1");
- modal.classList.add(NO_OUTLINE_CLASS);
- setTimeout(() => {
- modal.focus();
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uD3EC\uCEE4\uC2A4 \uC124\uC815 \uC644\uB8CC");
- }, 10);
- let handleResponse = (action) => {
- modal.remove();
- if (action === "cancel") {
- resolve(false);
- } else if (action === "updateSettings") {
- const recommendedTokens = Math.ceil(tokenUsage.totalEstimated / 100) * 100;
- this.updateMaxTokenSetting(recommendedTokens);
- resolve(true);
- } else {
- resolve(true);
- }
- };
- const handleKeyboard = (e) => {
- Logger.debug(`\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uD0A4 \uC774\uBCA4\uD2B8 \uAC10\uC9C0 - ${e.key} (\uCF54\uB4DC: ${e.code})`);
- e.preventDefault();
- e.stopPropagation();
- e.stopImmediatePropagation();
- if (e.key === "Enter") {
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: Enter\uD0A4 \uAC10\uC9C0 - \uC9C4\uD589");
- handleResponse("proceed");
- } else if (e.key === "Escape") {
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: Escape\uD0A4 \uAC10\uC9C0 - \uCDE8\uC18C");
- handleResponse("cancel");
- }
- };
- modal.addEventListener("keydown", handleKeyboard, { capture: true });
- modal.addEventListener("keyup", handleKeyboard, { capture: true });
- const globalKeyHandler = (e) => {
- if (document.body.contains(modal)) {
- Logger.debug(`\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uAE00\uB85C\uBC8C \uD0A4 \uC774\uBCA4\uD2B8 \uCC28\uB2E8 - ${e.key}`);
- e.preventDefault();
- e.stopPropagation();
- e.stopImmediatePropagation();
- if (e.key === "Enter") {
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uAE00\uB85C\uBC8C Enter\uD0A4 \uAC10\uC9C0 - \uC9C4\uD589");
- handleResponse("proceed");
- } else if (e.key === "Escape") {
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uAE00\uB85C\uBC8C Escape\uD0A4 \uAC10\uC9C0 - \uCDE8\uC18C");
- handleResponse("cancel");
- }
- }
- };
- document.addEventListener("keydown", globalKeyHandler, { capture: true });
- document.addEventListener("keyup", globalKeyHandler, { capture: true });
- window.addEventListener("keydown", globalKeyHandler, { capture: true });
- const originalHandleResponse = handleResponse;
- handleResponse = (action) => {
- document.removeEventListener("keydown", globalKeyHandler, { capture: true });
- document.removeEventListener("keyup", globalKeyHandler, { capture: true });
- window.removeEventListener("keydown", globalKeyHandler, { capture: true });
- Logger.debug("\uD1A0\uD070 \uACBD\uACE0 \uBAA8\uB2EC: \uBAA8\uB4E0 \uC774\uBCA4\uD2B8 \uB9AC\uC2A4\uB108 \uC81C\uAC70 \uC644\uB8CC");
- originalHandleResponse(action);
- };
- (_a2 = modal.querySelector("#token-warning-cancel")) == null ? void 0 : _a2.addEventListener("click", () => handleResponse("cancel"));
- (_b = modal.querySelector("#token-warning-proceed")) == null ? void 0 : _b.addEventListener("click", () => handleResponse("proceed"));
- (_c = modal.querySelector("#token-warning-update-settings")) == null ? void 0 : _c.addEventListener("click", () => handleResponse("updateSettings"));
- modal.addEventListener("click", (e) => {
- if (e.target === modal) {
- handleResponse("cancel");
- }
- });
- });
- }
- /**
- * AI 분석 결과를 상태 관리자에 적용합니다.
- */
- applyAIAnalysisResults() {
- for (const result of this.aiAnalysisResults) {
- this.stateManager.setState(
- result.correctionIndex,
- result.selectedValue,
- result.isExceptionProcessed,
- result.isOriginalKept
- );
- }
- }
- /**
- * 최대 토큰 설정을 업데이트합니다.
- */
- updateMaxTokenSetting(newMaxTokens) {
- if (this.onSettingsUpdate) {
- this.onSettingsUpdate(newMaxTokens);
- Logger.debug(`\uCD5C\uB300 \uD1A0\uD070\uC744 ${newMaxTokens}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD588\uC2B5\uB2C8\uB2E4.`);
- new import_obsidian4.Notice(`\u2699\uFE0F \uCD5C\uB300 \uD1A0\uD070\uC774 ${newMaxTokens.toLocaleString()}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`, 3e3);
- } else {
- Logger.warn("\uC124\uC815 \uC5C5\uB370\uC774\uD2B8 \uCF5C\uBC31\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- }
- /**
- * 모든 오류를 일괄로 순환시킵니다.
- */
- batchCycleCorrections(direction) {
- const rawCorrections = this.getCurrentCorrections();
- const uniqueCorrections = this.removeDuplicateCorrections(rawCorrections);
- if (uniqueCorrections.length === 0)
- return;
- let changedCount = 0;
- uniqueCorrections.forEach((pageCorrection) => {
- const actualIndex = pageCorrection.originalIndex;
- if (actualIndex !== -1) {
- this.cycleCorrectionState(actualIndex, direction);
- changedCount++;
- }
- });
- new import_obsidian4.Notice(`\u2728 ${changedCount}\uAC1C \uC624\uB958\uAC00 \uC77C\uAD04 ${direction === "next" ? "\uB2E4\uC74C" : "\uC774\uC804"} \uC81C\uC548\uC73C\uB85C \uBCC0\uACBD\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`, 2e3);
- Logger.log(`\uC77C\uAD04 \uBCC0\uACBD \uC644\uB8CC: ${direction}, ${changedCount}\uAC1C \uD56D\uBAA9`);
- }
- /**
- * 키보드 네비게이션 힌트를 표시합니다.
- */
- showKeyboardHint() {
- if (import_obsidian4.Platform.isMobile) {
- Logger.debug("\uBAA8\uBC14\uC77C \uD658\uACBD\uC5D0\uC11C\uB294 \uD0A4\uBCF4\uB4DC \uD78C\uD2B8\uB97C \uD45C\uC2DC\uD558\uC9C0 \uC54A\uC74C");
- return;
- }
- const hint = document.createElement("div");
- hint.className = "keyboard-navigation-hint";
- hint.id = "keyboard-hint";
- const header = document.createElement("div");
- header.className = "hint-header";
- const title = document.createElement("div");
- title.className = "hint-title";
- title.textContent = "\u2328\uFE0F \uD0A4\uBCF4\uB4DC \uB2E8\uCD95\uD0A4";
- header.appendChild(title);
- const closeBtn = document.createElement("button");
- closeBtn.className = "hint-close-btn";
- closeBtn.textContent = "\xD7";
- closeBtn.title = "\uB2E8\uCD95\uD0A4 \uAC00\uC774\uB4DC \uB2EB\uAE30";
- closeBtn.addEventListener("click", () => {
- hint.classList.add(FADE_OUT_CLASS);
- setTimeout(() => hint.remove(), 200);
- });
- header.appendChild(closeBtn);
- hint.appendChild(header);
- const shortcuts = [
- { key: "Tab", desc: "\uB2E4\uC74C \uC624\uB958" },
- { key: "\u2190/\u2192", desc: "\uC218\uC815 \uC81C\uC548 \uC21C\uD658" },
- { key: "Enter", desc: "\uC801\uC6A9" },
- { key: "\u2318Enter", desc: "\uBAA8\uB4E0 \uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5" },
- { key: "\u2318E", desc: "\uD3B8\uC9D1 \uBAA8\uB4DC" },
- { key: "\u21E7\u2318A", desc: "AI \uBD84\uC11D" },
- { key: "\u2318\u21E7E", desc: "\uC624\uB958 \uC0C1\uC138 \uD1A0\uAE00" },
- { key: "\u2318\u21E7\u2190/\u2192", desc: "\uC77C\uAD04 \uBCC0\uACBD" },
- { key: "\u2191/\u2193", desc: "\uD398\uC774\uC9C0 \uC774\uB3D9" },
- { key: "Esc", desc: "\uB2EB\uAE30" }
- ];
- shortcuts.forEach((shortcut) => {
- const item = document.createElement("div");
- item.className = "hint-item";
- const key = document.createElement("span");
- key.className = "hint-key";
- key.textContent = shortcut.key;
- const desc = document.createElement("span");
- desc.className = "hint-desc";
- desc.textContent = shortcut.desc;
- item.appendChild(key);
- item.appendChild(desc);
- hint.appendChild(item);
- });
- document.body.appendChild(hint);
- Logger.log("\uD0A4\uBCF4\uB4DC \uB124\uBE44\uAC8C\uC774\uC158 \uD78C\uD2B8 \uD45C\uC2DC\uB428 (\uB370\uC2A4\uD06C\uD1B1 \uC804\uC6A9)");
- }
- /**
- * 오류 상세부분 펼침/접힘을 토글합니다.
- */
- toggleErrorSummary() {
- Logger.log("\uC624\uB958 \uC0C1\uC138\uBD80\uBD84 \uD1A0\uAE00 \uD2B8\uB9AC\uAC70\uB428 (\uD0A4\uBCF4\uB4DC \uB2E8\uCD95\uD0A4: \u2318\u21E7E)");
- const errorSummary = document.getElementById("errorSummary");
- if (!errorSummary) {
- Logger.warn("errorSummary \uC694\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const isCurrentlyCollapsed = errorSummary.classList.contains("collapsed");
- if (isCurrentlyCollapsed) {
- errorSummary.classList.remove("collapsed");
- Logger.log("\uC624\uB958 \uC0C1\uC138\uBD80\uBD84 \uD3FC\uCE68");
- } else {
- errorSummary.classList.add("collapsed");
- Logger.log("\uC624\uB958 \uC0C1\uC138\uBD80\uBD84 \uC811\uD798");
- }
- this.recalculatePagination();
- this.updateDisplay();
- }
- /**
- * 현재 포커스된 오류로 스크롤합니다.
- * @param forceOpen 강제로 상세부분을 펼칠지 여부 (기본값: false)
- */
- scrollToFocusedError(forceOpen = false) {
- const rawCorrections = this.getCurrentCorrections();
- const uniqueCorrections = this.removeDuplicateCorrections(rawCorrections);
- if (uniqueCorrections.length === 0 || this.currentFocusIndex < 0)
- return;
- const pageCorrection = uniqueCorrections[this.currentFocusIndex];
- if (!pageCorrection)
- return;
- const actualIndex = pageCorrection.originalIndex;
- const errorSummary = document.getElementById("errorSummary");
- if (!errorSummary)
- return;
- const errorItems = errorSummary.querySelectorAll(".error-item-compact");
- let targetItem = null;
- errorItems.forEach((item, index) => {
- const itemPageCorrection = uniqueCorrections[index];
- if (itemPageCorrection && itemPageCorrection.originalIndex === actualIndex) {
- targetItem = item;
- }
- });
- if (targetItem) {
- const isCollapsed = errorSummary.classList.contains("collapsed");
- if (isCollapsed && forceOpen) {
- errorSummary.classList.remove("collapsed");
- this.recalculatePagination();
- this.updateDisplay();
- setTimeout(() => {
- targetItem.scrollIntoView({
- behavior: "smooth",
- block: "center",
- inline: "nearest"
- });
- this.highlightFocusedError(targetItem);
- }, 100);
- } else if (!isCollapsed) {
- targetItem.scrollIntoView({
- behavior: "smooth",
- block: "center",
- inline: "nearest"
- });
- this.highlightFocusedError(targetItem);
- }
- Logger.debug(`\uC624\uB958 \uC0C1\uC138\uBD80\uBD84 \uC790\uB3D9\uC2A4\uD06C\uB864: ${pageCorrection.correction.original} (forceOpen: ${forceOpen}, collapsed: ${isCollapsed})`);
- }
- }
- /**
- * 포커스된 오류 카드를 하이라이트합니다.
- */
- highlightFocusedError(targetItem) {
- const existingHighlight = document.querySelector(".error-item-highlighted");
- if (existingHighlight) {
- existingHighlight.classList.remove("error-item-highlighted");
- }
- targetItem.classList.add("error-item-highlighted");
- setTimeout(() => {
- targetItem.classList.remove("error-item-highlighted");
- }, 2e3);
- Logger.log("\uC624\uB958 \uCE74\uB4DC \uD558\uC774\uB77C\uC774\uD2B8 \uC560\uB2C8\uBA54\uC774\uC158 \uC801\uC6A9");
- }
- /**
- * 현재 포커스된 오류에 대해 편집 모드로 진입합니다.
- */
- enterEditModeForFocusedError() {
- Logger.log(`\u2328\uFE0F enterEditModeForFocusedError \uD638\uCD9C\uB428: currentFocusIndex=${this.currentFocusIndex}`);
- const rawCorrections = this.getCurrentCorrections();
- const uniqueCorrections = this.removeDuplicateCorrections(rawCorrections);
- this.currentCorrections = uniqueCorrections;
- Logger.debug(`\u2328\uFE0F \uAD50\uC815\uC0AC\uD56D \uAC1C\uC218: raw=${rawCorrections.length}, unique=${uniqueCorrections.length}`);
- Logger.debug(`\u2328\uFE0F \uD3EC\uCEE4\uC2A4 \uC778\uB371\uC2A4 \uC720\uD6A8\uC131: currentFocusIndex=${this.currentFocusIndex}, \uBC94\uC704=[0, ${uniqueCorrections.length - 1}]`);
- if (this.currentFocusIndex < 0 || this.currentFocusIndex >= uniqueCorrections.length) {
- if (uniqueCorrections.length > 0) {
- this.currentFocusIndex = 0;
- Logger.debug(`\u2328\uFE0F \uD3EC\uCEE4\uC2A4 \uC778\uB371\uC2A4 \uCD08\uAE30\uD654: ${this.currentFocusIndex}`);
- } else {
- Logger.warn("\u{1F6AB} \uD3B8\uC9D1 \uAC00\uB2A5\uD55C \uC624\uB958\uAC00 \uC5C6\uC74C");
- return;
- }
- }
- const pageCorrection = uniqueCorrections[this.currentFocusIndex];
- const actualIndex = pageCorrection.originalIndex;
- Logger.debug(`\u2328\uFE0F Cmd+E\uD0A4\uB85C \uD3B8\uC9D1 \uBAA8\uB4DC \uC9C4\uC785: index=${actualIndex}, text="${pageCorrection.correction.original}"`);
- const errorSummary = this.element.querySelector("#errorSummary");
- const wasCollapsed = errorSummary && errorSummary.classList.contains("collapsed");
- if (wasCollapsed) {
- errorSummary.classList.remove("collapsed");
- Logger.debug("\u2328\uFE0F \uC624\uB958 \uC0C1\uC138 \uC601\uC5ED \uC790\uB3D9 \uD3BC\uCE68");
- this.updateDisplay();
- }
- setTimeout(() => {
- const errorCard = this.element.querySelector(`[data-correction-index="${actualIndex}"] .error-original-compact`);
- if (errorCard) {
- Logger.debug(`\u2328\uFE0F \uD3B8\uC9D1 \uBAA8\uB4DC \uC9C4\uC785 - \uC624\uB958 \uC0C1\uC138 \uCE74\uB4DC \uCC3E\uC74C: index=${actualIndex}`);
- errorCard.scrollIntoView({
- behavior: "smooth",
- block: "center"
- });
- Logger.debug("\u2328\uFE0F \uC624\uD1A0\uC2A4\uD06C\uB864 \uC218\uD589");
- setTimeout(() => {
- this.enterCardEditMode(errorCard, actualIndex);
- }, 300);
- } else {
- Logger.debug(`\u2328\uFE0F \uC624\uB958 \uC0C1\uC138 \uCE74\uB4DC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C: index=${actualIndex}`);
- Logger.debug(`\u2328\uFE0F \uC7AC\uC2DC\uB3C4: \uBAA8\uB4E0 .error-original-compact \uC694\uC18C \uD655\uC778`);
- const allCards = this.element.querySelectorAll(".error-original-compact");
- Logger.debug(`\u2328\uFE0F \uBC1C\uACAC\uB41C \uCE74\uB4DC \uAC1C\uC218: ${allCards.length}`);
- allCards.forEach((card, index) => {
- var _a, _b;
- const cardIndex = (_b = (_a = card.parentElement) == null ? void 0 : _a.dataset) == null ? void 0 : _b.correctionIndex;
- Logger.debug(`\u2328\uFE0F \uCE74\uB4DC ${index}: correctionIndex=${cardIndex}`);
- });
- }
- }, wasCollapsed ? 100 : 0);
- }
- /**
- * 팝업을 닫습니다.
- */
- close() {
- this.app.keymap.popScope(this.keyboardScope);
- const hint = document.getElementById("keyboard-hint");
- if (hint) {
- hint.remove();
- }
- document.body.classList.remove("spell-popup-open");
- this.destroy();
- }
- /**
- * 오류 요약 요소를 생성합니다.
- */
- createErrorSummaryElement(container) {
- container.empty();
- const rawCorrections = this.getCurrentCorrections();
- const currentCorrections = this.removeDuplicateCorrections(rawCorrections);
- if (currentCorrections.length === 0) {
- const placeholder = container.createDiv("error-placeholder");
- placeholder.textContent = "\uD604\uC7AC \uD398\uC774\uC9C0\uC5D0 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.";
- return;
- }
- currentCorrections.forEach((pageCorrection, index) => {
- var _a;
- const actualIndex = pageCorrection.originalIndex;
- if (actualIndex === void 0)
- return;
- const errorCard = container.createDiv("error-card-compact");
- errorCard.dataset.correctionIndex = actualIndex.toString();
- const errorHeader = errorCard.createDiv("error-header");
- const errorNumber = errorHeader.createSpan("error-number");
- errorNumber.textContent = (index + 1).toString();
- const errorOriginal = errorHeader.createSpan("error-original-compact");
- errorOriginal.textContent = pageCorrection.correction.original;
- const errorArrow = errorHeader.createSpan("error-arrow");
- errorArrow.textContent = "\u2192";
- const errorCorrected = errorHeader.createSpan("error-corrected-compact");
- const currentValue = this.stateManager.getValue(actualIndex);
- const displayClass = this.stateManager.getDisplayClass(actualIndex);
- errorCorrected.textContent = currentValue || pageCorrection.correction.original;
- errorCorrected.className = `error-corrected-compact ${displayClass}`;
- if (pageCorrection.correction.help) {
- const helpDiv = errorCard.createDiv("error-help-compact");
- helpDiv.textContent = pageCorrection.correction.help;
- }
- const aiResult = (_a = this.aiAnalysisResults) == null ? void 0 : _a.find((result) => result.correctionIndex === actualIndex);
- if (aiResult) {
- const aiDiv = errorCard.createDiv("ai-analysis-compact");
- const confidenceBadge = aiDiv.createSpan("ai-confidence-badge");
- confidenceBadge.textContent = `${aiResult.confidence}%`;
- if (aiResult.reasoning) {
- const reasoningSpan = aiDiv.createSpan("ai-reasoning-compact");
- reasoningSpan.textContent = aiResult.reasoning;
- }
- }
- });
- }
- /**
- * 미리보기 요소를 생성합니다.
- */
- createPreviewElement(container) {
- container.empty();
- const previewText = this.isLongText ? this.getCurrentPreviewText() : this.config.selectedText.trim();
- const rawCorrections = this.getCurrentCorrections();
- const currentCorrections = this.removeDuplicateCorrections(rawCorrections);
- const segments = this.createTextSegments(previewText, currentCorrections);
- segments.forEach((segment) => {
- const span = container.createSpan();
- if (segment.correctionIndex !== void 0) {
- const actualIndex = segment.correctionIndex;
- const displayClass = this.stateManager.getDisplayClass(actualIndex);
- const currentValue = this.stateManager.getValue(actualIndex);
- const pageCorrection = currentCorrections.find((pc) => pc.originalIndex === actualIndex);
- const uniqueId = (pageCorrection == null ? void 0 : pageCorrection.uniqueId) || "unknown";
- span.textContent = currentValue;
- span.className = `clickable-error ${displayClass}`;
- span.dataset.correctionIndex = actualIndex.toString();
- span.dataset.uniqueId = uniqueId;
- span.setAttribute("tabindex", "0");
- } else {
- span.textContent = segment.text;
- }
- });
- }
- /**
- * 텍스트를 세그먼트로 분할합니다.
- */
- createTextSegments(previewText, currentCorrections) {
- const segments = [];
- let lastEnd = 0;
- currentCorrections.forEach((pageCorrection, index) => {
- const actualIndex = pageCorrection.originalIndex;
- if (actualIndex === void 0)
- return;
- const positionInPage = pageCorrection.positionInPage || 0;
- const originalText = pageCorrection.correction.original;
- const currentValue = this.stateManager.getValue(actualIndex);
- if (positionInPage > lastEnd) {
- segments.push({
- text: previewText.slice(lastEnd, positionInPage)
- });
- }
- segments.push({
- text: currentValue || originalText,
- correctionIndex: actualIndex
- });
- lastEnd = positionInPage + originalText.length;
- });
- if (lastEnd < previewText.length) {
- segments.push({
- text: previewText.slice(lastEnd)
- });
- }
- return segments;
- }
-};
-
-// src/orchestrator.ts
-init_aiAnalysisService();
-
-// src/ui/loadingManager.ts
-var import_obsidian9 = require("obsidian");
-init_logger();
-var _LoadingManager = class {
- constructor() {
- this.currentState = "idle";
- this.currentNotice = null;
- this.progressElement = null;
- this.startTime = 0;
- this.currentStep = 0;
- this.steps = [];
- // 기본 로딩 단계들
- this.defaultSteps = [
- {
- id: "text_analysis",
- title: "\uD14D\uC2A4\uD2B8 \uBD84\uC11D",
- description: "\uC785\uB825\uB41C \uD14D\uC2A4\uD2B8\uB97C \uCC98\uB9AC\uD558\uACE0 \uBD84\uC11D\uD569\uB2C8\uB2E4",
- estimatedDuration: 1e3,
- icon: "\u{1F4DD}"
- },
- {
- id: "api_request",
- title: "API \uC694\uCCAD",
- description: "Bareun.ai \uC11C\uBC84\uC5D0 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC\uB97C \uC694\uCCAD\uD569\uB2C8\uB2E4",
- estimatedDuration: 3e3,
- icon: "\u{1F310}"
- },
- {
- id: "result_parsing",
- title: "\uACB0\uACFC \uCC98\uB9AC",
- description: "\uAC80\uC0AC \uACB0\uACFC\uB97C \uBD84\uC11D\uD558\uACE0 \uC624\uB958\uB97C \uBD84\uB958\uD569\uB2C8\uB2E4",
- estimatedDuration: 800,
- icon: "\u2699\uFE0F"
- },
- {
- id: "ui_preparation",
- title: "UI \uC900\uBE44",
- description: "\uAD50\uC815 \uC778\uD130\uD398\uC774\uC2A4\uB97C \uC900\uBE44\uD569\uB2C8\uB2E4",
- estimatedDuration: 500,
- icon: "\u{1F3A8}"
- }
- ];
- this.aiSteps = [
- {
- id: "ai_context_preparation",
- title: "AI \uCEE8\uD14D\uC2A4\uD2B8 \uC900\uBE44",
- description: "\uB9DE\uCDA4\uBC95 \uC624\uB958\uC640 \uC8FC\uBCC0 \uB9E5\uB77D\uC744 AI\uC5D0\uAC8C \uC81C\uACF5\uD560 \uD615\uD0DC\uB85C \uAC00\uACF5\uD569\uB2C8\uB2E4",
- estimatedDuration: 500,
- icon: "\u{1F916}"
- },
- {
- id: "ai_analysis",
- title: "AI \uBD84\uC11D",
- description: "AI\uAC00 \uCD5C\uC801\uC758 \uC218\uC815\uC0AC\uD56D\uC744 \uBD84\uC11D\uD558\uACE0 \uC788\uC2B5\uB2C8\uB2E4",
- estimatedDuration: 5e3,
- icon: "\u{1F9E0}"
- },
- {
- id: "ai_result_processing",
- title: "AI \uACB0\uACFC \uCC98\uB9AC",
- description: "AI \uBD84\uC11D \uACB0\uACFC\uB97C \uAC80\uC99D\uD558\uACE0 \uC801\uC6A9\uD569\uB2C8\uB2E4",
- estimatedDuration: 800,
- icon: "\u2728"
- }
- ];
- Logger.debug("LoadingManager \uCD08\uAE30\uD654");
- }
- /**
- * 싱글톤 인스턴스 반환
- */
- static getInstance() {
- if (!_LoadingManager.instance) {
- _LoadingManager.instance = new _LoadingManager();
- }
- return _LoadingManager.instance;
- }
- /**
- * 상태 변경 콜백 등록
- */
- onStateChange(callback) {
- this.onStateChangeCallback = callback;
- }
- /**
- * 로딩 시작 (기본 맞춤법 검사)
- */
- startLoading(includeAI = false) {
- this.steps = includeAI ? [...this.defaultSteps, ...this.aiSteps] : [...this.defaultSteps];
- this.currentState = "analyzing";
- this.currentStep = 0;
- this.startTime = Date.now();
- Logger.debug("\uB85C\uB529 \uC2DC\uC791:", { includeAI, totalSteps: this.steps.length });
- this.showProgressNotice();
- this.updateProgress();
- }
- /**
- * 다음 단계로 진행
- */
- nextStep() {
- if (this.currentStep < this.steps.length - 1) {
- this.currentStep++;
- this.updateProgress();
- Logger.debug(`\uB2E8\uACC4 \uC9C4\uD589: ${this.currentStep + 1}/${this.steps.length}`, {
- currentStep: this.steps[this.currentStep]
- });
- }
- }
- /**
- * 특정 단계로 점프
- */
- setStep(stepId) {
- const stepIndex = this.steps.findIndex((step) => step.id === stepId);
- if (stepIndex !== -1) {
- this.currentStep = stepIndex;
- this.updateProgress();
- Logger.debug("\uB2E8\uACC4 \uC124\uC815:", { stepId, stepIndex });
- }
- }
- /**
- * 로딩 완료
- */
- complete() {
- this.currentState = "idle";
- this.hideNotice();
- const totalTime = Date.now() - this.startTime;
- Logger.debug("\uB85C\uB529 \uC644\uB8CC:", { totalTime: `${totalTime}ms` });
- if (this.onStateChangeCallback) {
- this.onStateChangeCallback("idle");
- }
- }
- /**
- * 에러 상태로 변경
- */
- error(message) {
- this.currentState = "error";
- this.hideNotice();
- new import_obsidian9.Notice(`\u274C ${message}`, 5e3);
- Logger.error("\uB85C\uB529 \uC5D0\uB7EC:", { message });
- if (this.onStateChangeCallback) {
- this.onStateChangeCallback("error");
- }
- }
- /**
- * 현재 상태 반환
- */
- getState() {
- return this.currentState;
- }
- /**
- * 진행률 정보 반환
- */
- getProgress() {
- const elapsedTime = Date.now() - this.startTime;
- const currentStepInfo = this.steps[this.currentStep];
- const totalEstimatedTime = this.steps.reduce((sum, step) => sum + step.estimatedDuration, 0);
- const completedTime = this.steps.slice(0, this.currentStep).reduce((sum, step) => sum + step.estimatedDuration, 0);
- const currentStepProgress = Math.min(elapsedTime - completedTime, (currentStepInfo == null ? void 0 : currentStepInfo.estimatedDuration) || 0);
- const totalProgress = completedTime + currentStepProgress;
- const percentage = Math.min(95, Math.round(totalProgress / totalEstimatedTime * 100));
- const estimatedRemaining = Math.max(0, totalEstimatedTime - totalProgress);
- return {
- currentStep: this.currentStep + 1,
- totalSteps: this.steps.length,
- percentage,
- message: (currentStepInfo == null ? void 0 : currentStepInfo.description) || "\uCC98\uB9AC \uC911...",
- elapsedTime,
- estimatedRemaining
- };
- }
- /**
- * 수동으로 진행률 업데이트
- */
- setCustomProgress(percentage, message) {
- if (this.progressElement) {
- this.updateProgressDisplay(percentage, message);
- }
- }
- /**
- * 로딩 취소
- */
- cancel() {
- this.currentState = "idle";
- this.hideNotice();
- Logger.debug("\uB85C\uB529 \uCDE8\uC18C\uB428");
- if (this.onStateChangeCallback) {
- this.onStateChangeCallback("idle");
- }
- }
- /**
- * 컴팩트한 토스트 스타일 진행률 표시 생성
- */
- showProgressNotice() {
- this.hideNotice();
- const toastContainer = document.body.createEl("div", {
- cls: "spell-check-toast"
- });
- this.progressElement = toastContainer;
- this.currentNotice = {
- hide: () => {
- toastContainer.addClass("kga-slide-out-down");
- setTimeout(() => {
- toastContainer.remove();
- }, 200);
- }
- };
- }
- /**
- * 진행률 업데이트
- */
- updateProgress() {
- if (!this.progressElement)
- return;
- const progress = this.getProgress();
- this.updateProgressDisplay(progress.percentage, progress.message);
- if (this.onStateChangeCallback) {
- this.onStateChangeCallback(this.currentState, progress);
- }
- }
- /**
- * 컴팩트한 진행률 디스플레이 업데이트
- */
- updateProgressDisplay(percentage, message) {
- if (!this.progressElement)
- return;
- const currentStepInfo = this.steps[this.currentStep];
- const progress = this.getProgress();
- this.progressElement.empty();
- const header = this.progressElement.createEl("div", {
- attr: {
- style: "display: flex; align-items: center; margin-bottom: 8px;"
- }
- });
- header.createEl("span", {
- text: (currentStepInfo == null ? void 0 : currentStepInfo.icon) || "\u23F3",
- attr: { style: "font-size: 16px; margin-right: 8px;" }
- });
- const titleContainer = header.createEl("div", {
- attr: { style: "flex: 1;" }
- });
- titleContainer.createEl("div", {
- text: (currentStepInfo == null ? void 0 : currentStepInfo.title) || "\uCC98\uB9AC \uC911",
- attr: { style: "font-weight: 500; font-size: 13px; line-height: 1.2;" }
- });
- const progressBarContainer = this.progressElement.createEl("div", {
- attr: {
- style: "background: var(--background-modifier-border); border-radius: 6px; height: 4px; overflow: hidden; margin-bottom: 6px;"
- }
- });
- progressBarContainer.createEl("div", {
- attr: {
- style: `
- background: var(--interactive-accent);
- height: 100%;
- width: ${percentage}%;
- transition: width 0.3s ease;
- border-radius: 6px;
- `
- }
- });
- const footer = this.progressElement.createEl("div", {
- attr: {
- style: "display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); line-height: 1;"
- }
- });
- footer.createEl("span", { text: `${percentage}%` });
- footer.createEl("span", { text: `${progress.currentStep}/${progress.totalSteps}` });
- }
- /**
- * Notice 숨기기
- */
- hideNotice() {
- if (this.currentNotice) {
- this.currentNotice.hide();
- this.currentNotice = null;
- this.progressElement = null;
- }
- }
- /**
- * 리소스 정리
- */
- static destroy() {
- if (_LoadingManager.instance) {
- _LoadingManager.instance.hideNotice();
- _LoadingManager.instance = null;
- Logger.debug("LoadingManager \uC815\uB9AC\uB428");
- }
- }
-};
-var LoadingManager = _LoadingManager;
-LoadingManager.instance = null;
-
-// src/orchestrator.ts
-init_logger();
-var SpellCheckOrchestrator = class {
- constructor(app, settings, onSettingsUpdated) {
- this.app = app;
- this.settings = settings;
- this.apiService = new OptimizedSpellCheckService(
- 5,
- // maxBatchSize
- 2e3,
- // batchTimeoutMs
- 15e3,
- // requestTimeoutMs
- 3,
- // maxConcurrentBatches
- {
- maxSize: 1e3,
- // 캐시 최대 1000개
- ttlMinutes: 30,
- // 30분 TTL
- cleanupIntervalMinutes: 5
- // 5분마다 정리
- }
- );
- this.aiService = new AIAnalysisService(settings.ai);
- this.onSettingsUpdated = onSettingsUpdated;
- }
- /**
- * 맞춤법 검사를 실행합니다.
- */
- async execute() {
- var _a;
- try {
- const { editor, selectedText, selectionStart, selectionEnd, file } = this.getEditorInfo();
- if (!selectedText || selectedText.trim().length === 0) {
- new import_obsidian10.Notice("\uAC80\uC0AC\uD560 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const validation = SettingsService.validateSettings(this.settings);
- if (!validation.isValid) {
- new import_obsidian10.Notice(`\uC124\uC815 \uC624\uB958: ${validation.errors.join(", ")}`);
- return;
- }
- const loadingManager = LoadingManager.getInstance();
- loadingManager.startLoading(false);
- try {
- loadingManager.setStep("text_analysis");
- await this.sleep(300);
- loadingManager.setStep("api_request");
- const priority = selectedText.length > 1e3 ? "medium" : "high";
- const result = await this.apiService.checkSpelling(selectedText, this.settings, priority);
- let morphemeInfo = null;
- if (result.corrections && result.corrections.length > 0) {
- try {
- Logger.log("\uD615\uD0DC\uC18C \uBD84\uC11D \uC218\uD589 \uC911...");
- morphemeInfo = await this.apiService.analyzeMorphemes(selectedText, this.settings);
- Logger.log("\uD615\uD0DC\uC18C \uBD84\uC11D \uC644\uB8CC:", {
- hasMorphemeInfo: !!morphemeInfo,
- sentences: ((_a = morphemeInfo == null ? void 0 : morphemeInfo.sentences) == null ? void 0 : _a.length) || 0
- });
- if (result.corrections.length > 1) {
- Logger.log("\uD615\uD0DC\uC18C \uAE30\uBC18 \uAD50\uC815 \uAC1C\uC120 \uC218\uD589...");
- result.corrections = await this.apiService.improveCorrectionsWithMorphemeData(
- selectedText,
- result.corrections,
- this.settings,
- morphemeInfo
- );
- Logger.log(`\uAD50\uC815 \uAC1C\uC120 \uC644\uB8CC: ${result.corrections.length}\uAC1C \uC624\uB958`);
- }
- } catch (morphemeError) {
- Logger.warn("\uD615\uD0DC\uC18C \uBD84\uC11D \uC2E4\uD328, \uC6D0\uBCF8 \uAD50\uC815 \uBC0F \uD328\uD134 \uB9E4\uCE6D \uC0AC\uC6A9:", morphemeError);
- morphemeInfo = null;
- }
- }
- loadingManager.setStep("result_parsing");
- await this.sleep(200);
- loadingManager.setStep("ui_preparation");
- await this.sleep(100);
- loadingManager.complete();
- this.handleSpellCheckResult(result, selectedText, selectionStart, selectionEnd, editor, file, morphemeInfo);
- } catch (error) {
- loadingManager.error("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4");
- Logger.error("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC2E4\uD589 \uC624\uB958:", error);
- }
- } catch (error) {
- Logger.error("Spell check orchestrator error:", error);
- new import_obsidian10.Notice(`\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4: ${error.message}`);
- }
- }
- /**
- * 에디터 정보를 가져옵니다.
- */
- getEditorInfo() {
- const markdownView = this.app.workspace.getActiveViewOfType(import_obsidian10.MarkdownView);
- const editor = markdownView == null ? void 0 : markdownView.editor;
- const file = markdownView == null ? void 0 : markdownView.file;
- if (!editor) {
- throw new Error("\uC5D0\uB514\uD130\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- let selectedText = editor.getSelection();
- let selectionStart = editor.getCursor("from");
- let selectionEnd = editor.getCursor("to");
- if (!selectedText || selectedText.trim().length === 0) {
- const fullText = editor.getValue();
- if (!fullText || fullText.trim().length === 0) {
- throw new Error("\uBB38\uC11C\uC5D0 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- selectedText = fullText;
- try {
- const totalLines = editor.lineCount();
- const lastLine = totalLines - 1;
- const lastLineText = editor.getLine(lastLine);
- selectionStart = { line: 0, ch: 0 };
- selectionEnd = { line: lastLine, ch: lastLineText.length };
- } catch (error) {
- Logger.warn("Failed to get document bounds using API methods, using fallback:", error);
- const lines = selectedText.split("\n");
- selectionStart = { line: 0, ch: 0 };
- selectionEnd = { line: lines.length - 1, ch: lines[lines.length - 1].length };
- }
- }
- return { editor, selectedText, selectionStart, selectionEnd, file };
- }
- /**
- * 맞춤법 검사 결과를 처리합니다.
- */
- handleSpellCheckResult(result, selectedText, selectionStart, selectionEnd, editor, file, morphemeInfo) {
- if (result.corrections.length === 0) {
- new import_obsidian10.Notice("\uC218\uC815\uD560 \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uD6CC\uB96D\uD569\uB2C8\uB2E4!");
- return;
- }
- const cleanedText = selectedText.trim();
- Logger.debug("handleSpellCheckResult \uD14D\uC2A4\uD2B8 \uC815\uB9AC:", {
- originalLength: selectedText.length,
- cleanedLength: cleanedText.length,
- correctionsCount: result.corrections.length
- });
- const popup = new CorrectionPopup(this.app, {
- corrections: result.corrections,
- selectedText: cleanedText,
- start: selectionStart,
- end: selectionEnd,
- editor,
- file,
- // ⭐ NEW: File 인스턴스 전달 (메타데이터 정보용)
- morphemeInfo,
- // ⭐ NEW: 형태소 분석 정보 전달 (AI 분석용)
- ignoredWords: IgnoredWordsService.getIgnoredWords(this.settings),
- onExceptionWordsAdded: (words) => this.handleExceptionWords(words)
- }, this.aiService, (newMaxTokens) => this.handleMaxTokensUpdate(newMaxTokens));
- popup.render();
- popup.show();
- }
- /**
- * API 오류를 처리합니다.
- */
- handleApiError(error) {
- var _a, _b, _c;
- Logger.error("API Error:", error);
- if ((_a = error.message) == null ? void 0 : _a.includes("API \uD0A4")) {
- new import_obsidian10.Notice("API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC5D0\uC11C Bareun.ai API \uD0A4\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
- } else if ((_b = error.message) == null ? void 0 : _b.includes("API \uC694\uCCAD \uC2E4\uD328")) {
- new import_obsidian10.Notice(`API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: ${error.message}`);
- } else if ((_c = error.message) == null ? void 0 : _c.includes("\uB124\uD2B8\uC6CC\uD06C")) {
- new import_obsidian10.Notice("\uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694.");
- } else {
- new import_obsidian10.Notice(`\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4: ${error.message}`);
- }
- }
- /**
- * 예외 처리된 단어들을 설정에 추가합니다.
- */
- handleExceptionWords(words) {
- if (words.length === 0)
- return;
- const updatedSettings = IgnoredWordsService.addMultipleIgnoredWords(words, this.settings);
- if (updatedSettings.ignoredWords.length > this.settings.ignoredWords.length) {
- this.settings = updatedSettings;
- if (this.onSettingsUpdated) {
- this.onSettingsUpdated(this.settings);
- }
- const addedCount = updatedSettings.ignoredWords.length - this.settings.ignoredWords.length + words.length;
- new import_obsidian10.Notice(`${words.length}\uAC1C\uC758 \uB2E8\uC5B4\uAC00 \uC608\uC678 \uCC98\uB9AC \uBAA9\uB85D\uC5D0 \uCD94\uAC00\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
- }
- }
- /**
- * 설정을 업데이트합니다.
- */
- updateSettings(newSettings) {
- this.settings = newSettings;
- this.aiService.updateSettings(newSettings.ai);
- }
- /**
- * API 서비스 성능 메트릭을 반환합니다.
- */
- getPerformanceMetrics() {
- return this.apiService.getMetrics();
- }
- /**
- * 캐시를 수동으로 정리합니다.
- */
- clearCache() {
- this.apiService.clearCache();
- new import_obsidian10.Notice("\uCE90\uC2DC\uAC00 \uC815\uB9AC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- }
- /**
- * 대기 중인 모든 요청을 취소합니다.
- */
- cancelPendingRequests() {
- this.apiService.cancelPendingRequests();
- new import_obsidian10.Notice("\uB300\uAE30 \uC911\uC778 \uC694\uCCAD\uB4E4\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- }
- /**
- * 오케스트레이터를 종료하고 리소스를 정리합니다.
- */
- destroy() {
- this.apiService.destroy();
- LoadingManager.destroy();
- }
- /**
- * 비동기 대기 헬퍼 함수
- */
- sleep(ms) {
- return new Promise((resolve) => setTimeout(resolve, ms));
- }
- /**
- * 현재 문단의 맞춤법을 검사합니다.
- */
- async executeCurrentParagraph() {
- const activeView = this.app.workspace.getActiveViewOfType(import_obsidian10.MarkdownView);
- if (!activeView) {
- new import_obsidian10.Notice("\uD65C\uC131\uD654\uB41C \uB9C8\uD06C\uB2E4\uC6B4 \uD3B8\uC9D1\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const editor = activeView.editor;
- if (!editor) {
- new import_obsidian10.Notice("\uD3B8\uC9D1\uAE30\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- try {
- const paragraphData = getCurrentParagraph(editor);
- const selectedText = paragraphData.text.trim();
- if (!selectedText) {
- new import_obsidian10.Notice("\uD604\uC7AC \uBB38\uB2E8\uC5D0 \uAC80\uC0AC\uD560 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- Logger.debug(`\uD604\uC7AC \uBB38\uB2E8 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC2DC\uC791: ${selectedText.length}\uC790`);
- await this.performSpellCheck(selectedText, editor, paragraphData.from, paragraphData.to);
- } catch (error) {
- Logger.error("\uD604\uC7AC \uBB38\uB2E8 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958 \uBC1C\uC0DD:", error);
- new import_obsidian10.Notice(`\uD604\uC7AC \uBB38\uB2E8 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4: ${error.message}`);
- }
- }
- /**
- * 현재 단어의 맞춤법을 검사합니다.
- */
- async executeCurrentWord() {
- const activeView = this.app.workspace.getActiveViewOfType(import_obsidian10.MarkdownView);
- if (!activeView) {
- new import_obsidian10.Notice("\uD65C\uC131\uD654\uB41C \uB9C8\uD06C\uB2E4\uC6B4 \uD3B8\uC9D1\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const editor = activeView.editor;
- if (!editor) {
- new import_obsidian10.Notice("\uD3B8\uC9D1\uAE30\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- try {
- const wordData = getCurrentWord(editor);
- if (!wordData) {
- new import_obsidian10.Notice("\uD604\uC7AC \uC704\uCE58\uC5D0 \uAC80\uC0AC\uD560 \uB2E8\uC5B4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const selectedText = wordData.text.trim();
- if (!selectedText) {
- new import_obsidian10.Notice("\uD604\uC7AC \uB2E8\uC5B4\uC5D0 \uAC80\uC0AC\uD560 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- Logger.debug(`\uD604\uC7AC \uB2E8\uC5B4 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC2DC\uC791: "${selectedText}"`);
- await this.performSpellCheck(selectedText, editor, wordData.from, wordData.to);
- } catch (error) {
- Logger.error("\uD604\uC7AC \uB2E8\uC5B4 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958 \uBC1C\uC0DD:", error);
- new import_obsidian10.Notice(`\uD604\uC7AC \uB2E8\uC5B4 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4: ${error.message}`);
- }
- }
- /**
- * 현재 문장의 맞춤법을 검사합니다.
- */
- async executeCurrentSentence() {
- const activeView = this.app.workspace.getActiveViewOfType(import_obsidian10.MarkdownView);
- if (!activeView) {
- new import_obsidian10.Notice("\uD65C\uC131\uD654\uB41C \uB9C8\uD06C\uB2E4\uC6B4 \uD3B8\uC9D1\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const editor = activeView.editor;
- if (!editor) {
- new import_obsidian10.Notice("\uD3B8\uC9D1\uAE30\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- try {
- const sentenceData = getCurrentSentence(editor);
- const selectedText = sentenceData.text.trim();
- if (!selectedText) {
- new import_obsidian10.Notice("\uD604\uC7AC \uBB38\uC7A5\uC5D0 \uAC80\uC0AC\uD560 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- Logger.log(`\uD604\uC7AC \uBB38\uC7A5 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC2DC\uC791: "${selectedText}"`);
- await this.performSpellCheck(selectedText, editor, sentenceData.from, sentenceData.to);
- } catch (error) {
- Logger.error("\uD604\uC7AC \uBB38\uC7A5 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958 \uBC1C\uC0DD:", error);
- new import_obsidian10.Notice(`\uD604\uC7AC \uBB38\uC7A5 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4: ${error.message}`);
- }
- }
- /**
- * 맞춤법 검사를 수행하는 공통 메서드
- */
- async performSpellCheck(selectedText, editor, from, to) {
- const activeView = this.app.workspace.getActiveViewOfType(import_obsidian10.MarkdownView);
- const file = activeView == null ? void 0 : activeView.file;
- const cleanedText = selectedText.trim();
- const validation = SettingsService.validateSettings(this.settings);
- if (!validation.isValid) {
- new import_obsidian10.Notice(`\uC124\uC815 \uC624\uB958: ${validation.errors.join(", ")}`);
- return;
- }
- LoadingManager.getInstance().startLoading();
- try {
- const result = await this.apiService.checkSpelling(cleanedText, this.settings);
- if (result.corrections && result.corrections.length > 0) {
- Logger.log(`\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC644\uB8CC: ${result.corrections.length}\uAC1C \uC624\uB958 \uBC1C\uACAC`);
- Logger.debug("API \uD638\uCD9C \uD14D\uC2A4\uD2B8:", {
- originalLength: selectedText.length,
- cleanedLength: cleanedText.length,
- originalFirst20: selectedText.substring(0, 20),
- cleanedFirst20: cleanedText.substring(0, 20)
- });
- const popupConfig = {
- selectedText: cleanedText,
- corrections: result.corrections,
- ignoredWords: this.settings.ignoredWords || [],
- editor,
- file,
- // ⭐ NEW: File 인스턴스 전달
- start: from || { line: 0, ch: 0 },
- end: to || { line: 0, ch: cleanedText.length }
- };
- const popup = new CorrectionPopup(
- this.app,
- popupConfig,
- this.aiService,
- (newMaxTokens) => this.handleMaxTokensUpdate(newMaxTokens)
- );
- popup.render();
- popup.show();
- } else {
- new import_obsidian10.Notice("\uC218\uC815\uD560 \uAC83\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uD6CC\uB96D\uD569\uB2C8\uB2E4!");
- }
- } catch (error) {
- Logger.error("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958 \uBC1C\uC0DD:", error);
- if (error.message.includes("API \uD0A4")) {
- new import_obsidian10.Notice("API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC5D0\uC11C Bareun.ai API \uD0A4\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
- } else if (error.message.includes("\uC694\uCCAD")) {
- new import_obsidian10.Notice(`API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: ${error.message}`);
- } else if (error.message.includes("\uB124\uD2B8\uC6CC\uD06C")) {
- new import_obsidian10.Notice("\uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694.");
- } else {
- new import_obsidian10.Notice(`\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4: ${error.message}`);
- }
- } finally {
- LoadingManager.getInstance().complete();
- }
- }
- /**
- * 최대 토큰 설정을 업데이트합니다.
- */
- handleMaxTokensUpdate(newMaxTokens) {
- this.settings.ai.maxTokens = newMaxTokens;
- this.aiService.updateSettings(this.settings.ai);
- if (this.onSettingsUpdated) {
- this.onSettingsUpdated(this.settings);
- }
- }
-};
-
-// src/ui/settingsTab.ts
-var import_obsidian12 = require("obsidian");
-init_logger();
-var ModernSettingsTab = class extends import_obsidian12.PluginSettingTab {
- constructor(app, plugin) {
- super(app, plugin);
- this.currentTab = "basic";
- this.tabContainer = null;
- this.contentContainer = null;
- this.plugin = plugin;
- }
- display() {
- const { containerEl } = this;
- containerEl.empty();
- const mainWrapper = containerEl.createEl("div", { cls: "ksc-settings-wrapper" });
- this.createHeader(mainWrapper);
- this.createTabNavigation(mainWrapper);
- this.contentContainer = mainWrapper.createEl("div", { cls: "ksc-content-container" });
- this.renderCurrentTab();
- }
- /**
- * 현재 탭 콘텐츠 렌더링
- */
- renderCurrentTab() {
- if (!this.contentContainer)
- return;
- this.contentContainer.empty();
- switch (this.currentTab) {
- case "basic":
- this.createBasicSettingsTab(this.contentContainer);
- break;
- case "ai":
- this.createAISettingsTab(this.contentContainer);
- break;
- case "advanced":
- this.createAdvancedManagementTab(this.contentContainer);
- break;
- case "performance":
- this.createMonitoringTab(this.contentContainer);
- break;
- case "beta":
- this.createBetaFeaturesTab(this.contentContainer);
- break;
- }
- }
- /**
- * 헤더 섹션 생성
- */
- createHeader(containerEl) {
- const header = containerEl.createEl("div", { cls: "ksc-header" });
- const titleSetting = new import_obsidian12.Setting(header).setName("\u{1F4DD} \uD55C\uAD6D\uC5B4 \uB9DE\uCDA4\uBC95 \uB3C4\uC6B0\uBBF8").setHeading();
- titleSetting.settingEl.addClasses(["ksc-header-title"]);
- header.createEl("p", {
- text: "\uD50C\uB7EC\uADF8\uC778 \uB3D9\uC791\uC744 \uCEE4\uC2A4\uD130\uB9C8\uC774\uC9D5\uD558\uACE0 AI \uAE30\uB2A5\uC744 \uC124\uC815\uD558\uC138\uC694",
- cls: "ksc-header-subtitle"
- });
- }
- /**
- * 탭 네비게이션 생성
- */
- createTabNavigation(containerEl) {
- this.tabContainer = containerEl.createEl("div", { cls: "ksc-tab-nav" });
- const tabs = [
- { id: "basic", label: "\uAE30\uBCF8 \uC124\uC815", icon: "\u2699\uFE0F", desc: "API \uD0A4 \uBC0F \uAE30\uBCF8 \uC635\uC158" },
- { id: "ai", label: "AI \uC124\uC815", icon: "\u{1F916}", desc: "AI \uC790\uB3D9 \uAD50\uC815 \uAE30\uB2A5" },
- { id: "advanced", label: "\uACE0\uAE09 \uAD00\uB9AC", icon: "\u{1F527}", desc: "\uBC31\uC5C5, \uBCF5\uC6D0, \uAC80\uC99D" },
- { id: "performance", label: "\uC131\uB2A5 \uBAA8\uB2C8\uD130\uB9C1", icon: "\u{1F4CA}", desc: "\uD1B5\uACC4 \uBC0F \uCD5C\uC801\uD654" },
- { id: "beta", label: "\uBCA0\uD0C0 \uAE30\uB2A5", icon: "\u{1F9EA}", desc: "\uC778\uB77C\uC778 \uBAA8\uB4DC \uB4F1 \uC2E4\uD5D8\uC801 \uAE30\uB2A5" }
- ];
- tabs.forEach((tab) => {
- const isActive = this.currentTab === tab.id;
- const tabButton = this.tabContainer.createEl("button", {
- cls: `ksc-tab-button ${isActive ? "active" : ""}`,
- attr: { "data-tab": tab.id }
- });
- tabButton.createEl("span", {
- text: tab.icon,
- cls: "ksc-tab-icon"
- });
- tabButton.createEl("span", {
- text: tab.label,
- cls: "ksc-tab-label"
- });
- tabButton.addEventListener("click", () => {
- this.switchTab(tab.id);
- });
- tabButton.title = tab.desc;
- });
- }
- /**
- * 탭 전환
- */
- switchTab(tabId) {
- this.currentTab = tabId;
- if (this.tabContainer) {
- this.tabContainer.querySelectorAll(".ksc-tab-button").forEach((button) => {
- const isActive = button.getAttribute("data-tab") === tabId;
- if (isActive) {
- button.addClass("active");
- } else {
- button.removeClass("active");
- }
- });
- }
- this.renderCurrentTab();
- }
- /**
- * 기본 설정 탭을 생성합니다
- */
- createBasicSettingsTab(containerEl) {
- this.createUsageGuideSection(containerEl);
- this.createAPISettingsSection(containerEl);
- this.createFilteringOptionsSection(containerEl);
- this.createIgnoredWordsSection(containerEl);
- }
- /**
- * 사용법 안내 섹션을 생성합니다
- */
- createUsageGuideSection(containerEl) {
- const section = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(section).setName("\u{1F4D6} \uC0AC\uC6A9\uBC95 \uC548\uB0B4").setDesc("\uD50C\uB7EC\uADF8\uC778\uC758 \uAE30\uBCF8 \uC0AC\uC6A9 \uBC29\uBC95\uC744 \uC548\uB0B4\uD569\uB2C8\uB2E4.").setHeading();
- const infoBox = section.createEl("div", { cls: "ksc-info-box" });
- const steps = [
- "1\uFE0F\u20E3 \uD14D\uC2A4\uD2B8\uB97C \uC120\uD0DD\uD558\uACE0 \uB9AC\uBCF8 \uC544\uC774\uCF58\uC744 \uD074\uB9AD\uD558\uAC70\uB098 \uBA85\uB839 \uD314\uB808\uD2B8\uC5D0\uC11C \uC2E4\uD589",
- "2\uFE0F\u20E3 \uD31D\uC5C5\uC5D0\uC11C \uBE68\uAC04\uC0C9\uC73C\uB85C \uD45C\uC2DC\uB41C \uC624\uB958\uB97C \uD074\uB9AD\uD558\uC5EC \uC218\uC815\uC0AC\uD56D \uD655\uC778",
- "3\uFE0F\u20E3 AI \uBD84\uC11D \uBC84\uD2BC\uC73C\uB85C \uC790\uB3D9 \uAD50\uC815 \uC81C\uC548 \uBC1B\uAE30 (AI \uAE30\uB2A5 \uD65C\uC131\uD654 \uC2DC)",
- "4\uFE0F\u20E3 \uC801\uC6A9 \uBC84\uD2BC\uC73C\uB85C \uBCC0\uACBD\uC0AC\uD56D\uC744 \uC5D0\uB514\uD130\uC5D0 \uBC18\uC601"
- ];
- infoBox.createEl("strong", { text: "\u{1F680} \uAE30\uBCF8 \uC0AC\uC6A9\uBC95" });
- infoBox.createEl("br");
- infoBox.createEl("br");
- steps.forEach((step) => {
- infoBox.createEl("div", {
- text: step,
- attr: { style: "margin-bottom: 4px;" }
- });
- });
- }
- /**
- * API 설정 섹션을 생성합니다
- */
- createAPISettingsSection(containerEl) {
- const section = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(section).setName("\u{1F310} API \uC5F0\uACB0").setDesc("Bareun.ai \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC11C\uBE44\uC2A4 \uC5F0\uACB0\uC744 \uC704\uD55C \uC124\uC815\uC785\uB2C8\uB2E4.").setHeading();
- const settingsGroup = section.createEl("div", { cls: "ksc-setting-group" });
- new import_obsidian12.Setting(settingsGroup).setName("Bareun.ai API \uD0A4").setDesc("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC\uB97C \uC704\uD55C Bareun.ai API \uD0A4\uB97C \uC785\uB825\uD558\uC138\uC694.").addText((text) => {
- text.setPlaceholder("API \uD0A4\uB97C \uC785\uB825\uD558\uC138\uC694").setValue(this.plugin.settings.apiKey).onChange(async (value) => {
- this.plugin.settings.apiKey = value;
- await this.plugin.saveSettings();
- });
- });
- new import_obsidian12.Setting(settingsGroup).setName("API \uD638\uC2A4\uD2B8").setDesc("Bareun.ai API \uC11C\uBC84 \uD638\uC2A4\uD2B8 \uC8FC\uC18C\uC785\uB2C8\uB2E4.").addText((text) => {
- text.setPlaceholder("bareun-api.junlim.org").setValue(this.plugin.settings.apiHost).onChange(async (value) => {
- this.plugin.settings.apiHost = value;
- await this.plugin.saveSettings();
- });
- });
- new import_obsidian12.Setting(settingsGroup).setName("API \uD3EC\uD2B8").setDesc("Bareun.ai API \uC11C\uBC84 \uD3EC\uD2B8 \uBC88\uD638\uC785\uB2C8\uB2E4.").addText((text) => {
- text.setPlaceholder("443").setValue(this.plugin.settings.apiPort.toString()).onChange(async (value) => {
- const port = parseInt(value);
- if (!isNaN(port) && port > 0 && port <= 65535) {
- this.plugin.settings.apiPort = port;
- await this.plugin.saveSettings();
- }
- });
- });
- const apiInfoBox = section.createEl("div", { cls: "ksc-info-box" });
- apiInfoBox.createEl("strong", { text: "\u{1F4A1} API \uD0A4 \uBC1C\uAE09 \uC548\uB0B4" });
- apiInfoBox.createEl("br");
- apiInfoBox.createEl("span", { text: "API \uD0A4\uB294 " });
- const bareunLink = apiInfoBox.createEl("a", {
- text: "Bareun.ai \uC6F9\uC0AC\uC774\uD2B8",
- href: "https://bareun.ai",
- attr: { target: "_blank" }
- });
- apiInfoBox.createEl("span", { text: "\uC5D0\uC11C \uBC1C\uAE09\uBC1B\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4." });
- }
- /**
- * AI 설정 탭을 생성합니다
- */
- createAISettingsTab(containerEl) {
- const aiToggleSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(aiToggleSection).setName("\u{1F916} AI \uC790\uB3D9 \uAD50\uC815").setDesc("AI\uAC00 \uBB38\uB9E5\uC744 \uBD84\uC11D\uD558\uC5EC \uCD5C\uC801\uC758 \uC218\uC815\uC0AC\uD56D\uC744 \uC790\uB3D9\uC73C\uB85C \uC120\uD0DD\uD569\uB2C8\uB2E4.").setHeading();
- const settingsGroup = aiToggleSection.createEl("div", { cls: "ksc-setting-group" });
- new import_obsidian12.Setting(settingsGroup).setName("AI \uAE30\uB2A5 \uD65C\uC131\uD654").setDesc("AI \uC790\uB3D9 \uAD50\uC815 \uAE30\uB2A5\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.").addToggle((toggle) => toggle.setValue(this.plugin.settings.ai.enabled).onChange(async (value) => {
- this.plugin.settings.ai.enabled = value;
- await this.plugin.saveSettings();
- this.display();
- }));
- if (this.plugin.settings.ai.enabled) {
- this.createAIProviderSection(containerEl);
- this.createAIModelSection(containerEl);
- this.createAIAdvancedSection(containerEl);
- } else {
- const disabledInfo = containerEl.createEl("div", { cls: "ksc-info-box" });
- disabledInfo.createEl("strong", { text: "\u{1F4A1} AI \uAE30\uB2A5\uC744 \uD65C\uC131\uD654\uD558\uBA74 \uB2E4\uC74C\uACFC \uAC19\uC740 \uAE30\uB2A5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4:" });
- disabledInfo.createEl("br");
- disabledInfo.createEl("br");
- const features = [
- "\u2022 \uBB38\uB9E5\uC744 \uACE0\uB824\uD55C \uC9C0\uB2A5\uD615 \uC218\uC815 \uC81C\uC548",
- "\u2022 \uACE0\uC720\uBA85\uC0AC \uBC0F \uC804\uBB38\uC6A9\uC5B4 \uC790\uB3D9 \uC778\uC2DD",
- "\u2022 \uC2E0\uB8B0\uB3C4 \uC810\uC218\uC640 \uC0C1\uC138\uD55C \uCD94\uCC9C \uC774\uC720",
- "\u2022 \uC6D0\uD074\uB9AD \uC790\uB3D9 \uAD50\uC815 \uC801\uC6A9"
- ];
- features.forEach((feature) => {
- disabledInfo.createEl("div", { text: feature });
- });
- }
- }
- /**
- * AI 제공자 선택 섹션을 생성합니다
- */
- createAIProviderSection(containerEl) {
- const providerSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(providerSection).setName("\u{1F3E2} AI \uC81C\uACF5\uC790").setDesc("\uC0AC\uC6A9\uD560 AI \uC11C\uBE44\uC2A4\uB97C \uC120\uD0DD\uD558\uACE0 API \uD0A4\uB97C \uC124\uC815\uD558\uC138\uC694.").setHeading();
- const settingsGroup = providerSection.createEl("div", { cls: "ksc-setting-group" });
- new import_obsidian12.Setting(settingsGroup).setName("AI \uC81C\uACF5\uC790").setDesc("\uC0AC\uC6A9\uD560 AI \uC11C\uBE44\uC2A4\uB97C \uC120\uD0DD\uD558\uC138\uC694.").addDropdown((dropdown) => {
- dropdown.addOption("openai", "\u{1F535} OpenAI (GPT)").addOption("anthropic", "\u{1F7E0} Anthropic (Claude)").addOption("google", "\u{1F534} Google (Gemini)").addOption("ollama", "\u{1F7E1} Ollama (\uB85C\uCEEC)").setValue(this.plugin.settings.ai.provider).onChange(async (value) => {
- this.plugin.settings.ai.provider = value;
- await this.plugin.saveSettings();
- this.display();
- });
- });
- this.createProviderAPIKeySettings(settingsGroup);
- }
- /**
- * 제공자별 API 키 설정을 생성합니다
- */
- createProviderAPIKeySettings(containerEl) {
- const provider = this.plugin.settings.ai.provider;
- if (provider === "openai") {
- new import_obsidian12.Setting(containerEl).setName("OpenAI API \uD0A4").setDesc("OpenAI \uC11C\uBE44\uC2A4 \uC0AC\uC6A9\uC744 \uC704\uD55C API \uD0A4\uC785\uB2C8\uB2E4.").addText((text) => text.setPlaceholder("sk-...").setValue(this.plugin.settings.ai.openaiApiKey).onChange(async (value) => {
- this.plugin.settings.ai.openaiApiKey = value;
- await this.plugin.saveSettings();
- }));
- } else if (provider === "anthropic") {
- new import_obsidian12.Setting(containerEl).setName("Anthropic API \uD0A4").setDesc("Anthropic (Claude) \uC11C\uBE44\uC2A4 \uC0AC\uC6A9\uC744 \uC704\uD55C API \uD0A4\uC785\uB2C8\uB2E4.").addText((text) => text.setPlaceholder("sk-ant-...").setValue(this.plugin.settings.ai.anthropicApiKey).onChange(async (value) => {
- this.plugin.settings.ai.anthropicApiKey = value;
- await this.plugin.saveSettings();
- }));
- } else if (provider === "google") {
- new import_obsidian12.Setting(containerEl).setName("Google AI API \uD0A4").setDesc("Google Gemini \uC11C\uBE44\uC2A4 \uC0AC\uC6A9\uC744 \uC704\uD55C API \uD0A4\uC785\uB2C8\uB2E4.").addText((text) => text.setPlaceholder("AIza...").setValue(this.plugin.settings.ai.googleApiKey).onChange(async (value) => {
- this.plugin.settings.ai.googleApiKey = value;
- await this.plugin.saveSettings();
- }));
- } else if (provider === "ollama") {
- new import_obsidian12.Setting(containerEl).setName("Ollama \uC5D4\uB4DC\uD3EC\uC778\uD2B8").setDesc("\uB85C\uCEEC Ollama \uC11C\uBC84\uC758 \uC5D4\uB4DC\uD3EC\uC778\uD2B8 \uC8FC\uC18C\uC785\uB2C8\uB2E4.").addText((text) => text.setPlaceholder("http://localhost:11434").setValue(this.plugin.settings.ai.ollamaEndpoint).onChange(async (value) => {
- this.plugin.settings.ai.ollamaEndpoint = value;
- await this.plugin.saveSettings();
- }));
- }
- }
- /**
- * AI 모델 설정 섹션을 생성합니다
- */
- createAIModelSection(containerEl) {
- const modelSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(modelSection).setName("\u{1F3AF} \uBAA8\uB378 \uC120\uD0DD").setDesc("\uC0AC\uC6A9\uD560 AI \uBAA8\uB378\uACFC \uC138\uBD80 \uC124\uC815\uC744 \uAD6C\uC131\uD558\uC138\uC694.").setHeading();
- const settingsGroup = modelSection.createEl("div", { cls: "ksc-setting-group" });
- const provider = this.plugin.settings.ai.provider;
- const modelOptions = this.getModelOptions(provider);
- new import_obsidian12.Setting(settingsGroup).setName("AI \uBAA8\uB378").setDesc(`${provider.toUpperCase()} \uC81C\uACF5\uC790\uC758 \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBAA8\uB378\uC785\uB2C8\uB2E4.`).addDropdown((dropdown) => {
- modelOptions.forEach((model) => {
- dropdown.addOption(model.id, model.name);
- });
- dropdown.setValue(this.plugin.settings.ai.model).onChange(async (value) => {
- this.plugin.settings.ai.model = value;
- await this.plugin.saveSettings();
- });
- });
- }
- /**
- * 제공자별 모델 옵션을 반환합니다
- */
- getModelOptions(provider) {
- switch (provider) {
- case "openai":
- return [
- { id: "gpt-4o", name: "GPT-4 Omni" },
- { id: "gpt-4o-mini", name: "GPT-4 Omni Mini" },
- { id: "gpt-4-turbo", name: "GPT-4 Turbo" }
- ];
- case "anthropic":
- return [
- { id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet" },
- { id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku" }
- ];
- case "google":
- return [
- { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" },
- { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash" }
- ];
- case "ollama":
- return [
- { id: "llama3.2:3b", name: "Llama 3.2 3B" },
- { id: "mistral:7b", name: "Mistral 7B" },
- { id: "qwen2.5:7b", name: "Qwen 2.5 7B" }
- ];
- default:
- return [];
- }
- }
- /**
- * AI 고급 설정 섹션을 생성합니다
- */
- createAIAdvancedSection(containerEl) {
- const advancedSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(advancedSection).setName("\u2699\uFE0F \uACE0\uAE09 \uC635\uC158").setDesc("AI \uBAA8\uB378\uC758 \uC0C1\uC138\uD55C \uB3D9\uC791\uC744 \uC870\uC815\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.").setHeading();
- const settingsGroup = advancedSection.createEl("div", { cls: "ksc-setting-group" });
- new import_obsidian12.Setting(settingsGroup).setName("\uCD5C\uB300 \uD1A0\uD070 \uC218").setDesc("AI \uC694\uCCAD \uC2DC \uC0AC\uC6A9\uD560 \uCD5C\uB300 \uD1A0\uD070 \uC218\uC785\uB2C8\uB2E4. (\uAD8C\uC7A5: 1000-4000, \uB192\uC744\uC218\uB85D \uB354 \uC0C1\uC138\uD55C \uBD84\uC11D)").addText(
- (text) => text.setPlaceholder("\uC608: 2000").setValue(this.plugin.settings.ai.maxTokens.toString()).onChange(async (value) => {
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue > 0) {
- this.plugin.settings.ai.maxTokens = numValue;
- await this.plugin.saveSettings();
- }
- })
- ).then((setting) => {
- setting.controlEl.querySelector("input").type = "number";
- setting.controlEl.querySelector("input").min = "100";
- setting.controlEl.querySelector("input").step = "100";
- });
- new import_obsidian12.Setting(settingsGroup).setName("\uCC3D\uC758\uC131 (temperature)").setDesc("AI\uC758 \uCC3D\uC758\uC131 \uC218\uC900\uC785\uB2C8\uB2E4. \uB0AE\uC744\uC218\uB85D \uC77C\uAD00\uB41C \uACB0\uACFC, \uB192\uC744\uC218\uB85D \uB2E4\uC591\uD55C \uACB0\uACFC").addSlider((slider) => slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.ai.temperature).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.ai.temperature = value;
- await this.plugin.saveSettings();
- }));
- new import_obsidian12.Setting(settingsGroup).setName("\uD1A0\uD070 \uC0AC\uC6A9\uB7C9 \uACBD\uACE0").setDesc("\uD1A0\uD070 \uC0AC\uC6A9\uB7C9\uC774 \uC784\uACC4\uAC12\uC744 \uCD08\uACFC\uD560 \uB54C \uACBD\uACE0\uB97C \uD45C\uC2DC\uD569\uB2C8\uB2E4.").addToggle((toggle) => toggle.setValue(this.plugin.settings.ai.showTokenWarning).onChange(async (value) => {
- this.plugin.settings.ai.showTokenWarning = value;
- await this.plugin.saveSettings();
- }));
- if (this.plugin.settings.ai.showTokenWarning) {
- new import_obsidian12.Setting(settingsGroup).setName("\uACBD\uACE0 \uC784\uACC4\uAC12").setDesc("\uC774 \uD1A0\uD070 \uC218\uB97C \uCD08\uACFC\uD558\uBA74 \uACBD\uACE0\uB97C \uD45C\uC2DC\uD569\uB2C8\uB2E4.").addSlider((slider) => slider.setLimits(500, 5e3, 100).setValue(this.plugin.settings.ai.tokenWarningThreshold).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.ai.tokenWarningThreshold = value;
- await this.plugin.saveSettings();
- }));
- }
- }
- /**
- * 예외 단어 관리 섹션을 생성합니다
- */
- createIgnoredWordsSection(containerEl) {
- const section = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(section).setName("\u{1F4DD} \uC608\uC678 \uB2E8\uC5B4 \uAD00\uB9AC").setDesc("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC\uC5D0\uC11C \uC81C\uC678\uD560 \uB2E8\uC5B4\uB4E4\uC744 \uAD00\uB9AC\uD569\uB2C8\uB2E4.").setHeading();
- const countInfo = section.createEl("div", {
- cls: "ksc-info-box",
- text: `\uD604\uC7AC ${IgnoredWordsService.getIgnoredWordsCount(this.plugin.settings)}\uAC1C\uC758 \uB2E8\uC5B4\uAC00 \uC608\uC678 \uCC98\uB9AC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`
- });
- const tagCloudContainer = section.createEl("div", { cls: "ksc-tag-cloud" });
- const inputContainer = section.createEl("div", { cls: "ksc-setting-group" });
- new import_obsidian12.Setting(inputContainer).setName("\uC608\uC678 \uB2E8\uC5B4 \uCD94\uAC00").setDesc("\uC27C\uD45C\uB85C \uAD6C\uBD84\uD558\uC5EC \uC5EC\uB7EC \uB2E8\uC5B4\uB97C \uD55C \uBC88\uC5D0 \uCD94\uAC00\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.").addText((text) => {
- text.setPlaceholder("\uC608: \uCE74\uCE74\uC624\uD1A1, \uB124\uC774\uBC84, \uACE0\uC720\uBA85\uC0AC").onChange(async (value) => {
- if (value.includes(",")) {
- const words = value.split(",").map((w) => w.trim()).filter((w) => w.length > 0);
- if (words.length > 0) {
- const updatedSettings = IgnoredWordsService.addMultipleIgnoredWords(words, this.plugin.settings);
- this.plugin.settings = updatedSettings;
- await this.plugin.saveSettings();
- text.setValue("");
- this.renderIgnoredWordsCloud(tagCloudContainer);
- countInfo.textContent = `\uD604\uC7AC ${IgnoredWordsService.getIgnoredWordsCount(this.plugin.settings)}\uAC1C\uC758 \uB2E8\uC5B4\uAC00 \uC608\uC678 \uCC98\uB9AC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`;
- }
- }
- });
- text.inputEl.addEventListener("keydown", async (e) => {
- if (e.key === "Enter") {
- e.preventDefault();
- const value = text.getValue().trim();
- if (value) {
- const words = value.split(",").map((w) => w.trim()).filter((w) => w.length > 0);
- if (words.length > 0) {
- const updatedSettings = IgnoredWordsService.addMultipleIgnoredWords(words, this.plugin.settings);
- this.plugin.settings = updatedSettings;
- await this.plugin.saveSettings();
- text.setValue("");
- this.renderIgnoredWordsCloud(tagCloudContainer);
- countInfo.textContent = `\uD604\uC7AC ${IgnoredWordsService.getIgnoredWordsCount(this.plugin.settings)}\uAC1C\uC758 \uB2E8\uC5B4\uAC00 \uC608\uC678 \uCC98\uB9AC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`;
- }
- }
- }
- });
- });
- const buttonContainer = section.createEl("div", { cls: "ksc-button-group" });
- const clearAllButton = buttonContainer.createEl("button", {
- text: "\u{1F5D1}\uFE0F \uC804\uCCB4 \uC0AD\uC81C",
- cls: "mod-warning"
- });
- clearAllButton.onclick = async () => {
- if (confirm("\uBAA8\uB4E0 \uC608\uC678 \uB2E8\uC5B4\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
- this.plugin.settings.ignoredWords = [];
- await this.plugin.saveSettings();
- this.renderIgnoredWordsCloud(tagCloudContainer);
- countInfo.textContent = `\uD604\uC7AC ${IgnoredWordsService.getIgnoredWordsCount(this.plugin.settings)}\uAC1C\uC758 \uB2E8\uC5B4\uAC00 \uC608\uC678 \uCC98\uB9AC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`;
- }
- };
- this.renderIgnoredWordsCloud(tagCloudContainer);
- }
- /**
- * 예외 단어 클라우드를 렌더링합니다
- */
- renderIgnoredWordsCloud(container) {
- clearElement(container);
- if (this.plugin.settings.ignoredWords.length === 0) {
- container.createEl("div", {
- text: "\uC608\uC678 \uCC98\uB9AC\uB41C \uB2E8\uC5B4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
- attr: { style: "color: var(--text-muted); text-align: center; padding: 20px;" }
- });
- return;
- }
- this.plugin.settings.ignoredWords.forEach((word) => {
- const tag = container.createEl("span", {
- text: word,
- attr: {
- style: `
- display: inline-block;
- background: var(--interactive-accent);
- color: var(--text-on-accent);
- padding: 4px 8px;
- margin: 2px;
- border-radius: 12px;
- font-size: 12px;
- cursor: pointer;
- transition: all 0.2s ease;
- `
- }
- });
- tag.onclick = async () => {
- if (confirm(`"${word}"\uB97C \uC608\uC678 \uBAA9\uB85D\uC5D0\uC11C \uC81C\uAC70\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`)) {
- const updatedSettings = IgnoredWordsService.removeIgnoredWord(word, this.plugin.settings);
- this.plugin.settings = updatedSettings;
- await this.plugin.saveSettings();
- this.renderIgnoredWordsCloud(container);
- }
- };
- });
- }
- /**
- * 고급 관리 탭을 생성합니다
- */
- createAdvancedManagementTab(containerEl) {
- this.createValidationSection(containerEl);
- this.createBackupSection(containerEl);
- this.createImportExportSection(containerEl);
- }
- /**
- * 설정 검증 섹션을 생성합니다
- */
- createValidationSection(containerEl) {
- const { AdvancedSettingsService: AdvancedSettingsService2 } = (init_advancedSettingsService(), __toCommonJS(advancedSettingsService_exports));
- const validationSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(validationSection).setName("\u2705 \uAD6C\uC131 \uAC80\uC99D").setDesc("\uD604\uC7AC \uC124\uC815\uC758 \uC720\uD6A8\uC131\uC744 \uAC80\uC0AC\uD558\uACE0 \uCD5C\uC801\uD654 \uC81C\uC548\uC744 \uBC1B\uC2B5\uB2C8\uB2E4.").setHeading();
- const buttonGroup = validationSection.createEl("div", { cls: "ksc-button-group" });
- const validateButton = buttonGroup.createEl("button", { text: "\uC124\uC815 \uAC80\uC99D", cls: "mod-cta" });
- const validationResult = validationSection.createEl("div", {
- attr: {
- style: "display: none; margin-top: 16px;",
- id: "validation-result-container"
- }
- });
- validateButton.onclick = () => {
- try {
- const validation = AdvancedSettingsService2.validateSettings(this.plugin.settings);
- const suggestions = AdvancedSettingsService2.getOptimizationSuggestions(this.plugin.settings);
- this.createImprovedValidationDisplay(validationResult, validation, suggestions);
- validationResult.removeClass("kga-hidden");
- validationResult.addClass("kga-block");
- validationResult.className = "";
- } catch (error) {
- Logger.error("\uC124\uC815 \uAC80\uC99D \uC624\uB958:", error);
- validationResult.removeClass("kga-hidden");
- validationResult.addClass("kga-block");
- validationResult.className = "ksc-warning-box";
- validationResult.textContent = "\uC124\uC815 \uAC80\uC99D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
- }
- };
- }
- /**
- * 백업 관리 섹션을 생성합니다
- */
- createBackupSection(containerEl) {
- const { AdvancedSettingsService: AdvancedSettingsService2 } = (init_advancedSettingsService(), __toCommonJS(advancedSettingsService_exports));
- const backupSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(backupSection).setName("\u{1F4BE} \uBC31\uC5C5 \uAD00\uB9AC").setDesc("\uC124\uC815\uC744 \uBC31\uC5C5\uD558\uACE0 \uD544\uC694\uC2DC \uC774\uC804 \uC0C1\uD0DC\uB85C \uBCF5\uC6D0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.").setHeading();
- const buttonGroup = backupSection.createEl("div", { cls: "ksc-button-group" });
- const createBackupBtn = buttonGroup.createEl("button", { text: "\uBC31\uC5C5 \uC0DD\uC131" });
- const showBackupsBtn = buttonGroup.createEl("button", { text: "\uBC31\uC5C5 \uBAA9\uB85D" });
- const backupListContainer = backupSection.createEl("div", {
- attr: { style: "display: none; margin-top: 16px;" }
- });
- createBackupBtn.onclick = () => {
- AdvancedSettingsService2.backupSettings(this.plugin.settings, "\uC218\uB3D9 \uBC31\uC5C5");
- new import_obsidian12.Notice("\uC124\uC815\uC774 \uBC31\uC5C5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- if (!backupListContainer.hasClass("kga-hidden")) {
- updateBackupList();
- }
- };
- const updateBackupList = () => {
- const backups = AdvancedSettingsService2.getBackups();
- clearElement(backupListContainer);
- if (backups.length === 0) {
- backupListContainer.createEl("div", {
- text: "\uBC31\uC5C5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
- cls: "ksc-info-box"
- });
- } else {
- backups.forEach((backup) => {
- const backupItem = backupListContainer.createEl("div", {
- cls: "ksc-info-box",
- attr: { style: "margin-bottom: 8px;" }
- });
- backupItem.createEl("strong", { text: backup.reason });
- backupItem.createEl("span", { text: ` (${backup.age})` });
- backupItem.createEl("br");
- backupItem.createEl("small", { text: `${backup.timestamp} - \uBC84\uC804 ${backup.version}` });
- backupItem.createEl("br");
- const restoreBtn = backupItem.createEl("button", {
- text: "\uBCF5\uC6D0",
- attr: { style: "margin-top: 8px; font-size: 12px;" }
- });
- restoreBtn.onclick = async () => {
- const restored = AdvancedSettingsService2.restoreSettings(backup.index);
- if (restored) {
- this.plugin.settings = restored;
- await this.plugin.saveSettings();
- this.display();
- new import_obsidian12.Notice("\uC124\uC815\uC774 \uBCF5\uC6D0\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- }
- };
- });
- }
- };
- showBackupsBtn.onclick = () => {
- const isVisible = !backupListContainer.hasClass("kga-hidden");
- backupListContainer.toggleClass("kga-hidden", isVisible);
- if (!isVisible) {
- updateBackupList();
- }
- };
- }
- /**
- * 내보내기/가져오기 섹션을 생성합니다
- */
- createImportExportSection(containerEl) {
- const { AdvancedSettingsService: AdvancedSettingsService2 } = (init_advancedSettingsService(), __toCommonJS(advancedSettingsService_exports));
- const importExportSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(importExportSection).setName("\u{1F4E4} \uB0B4\uBCF4\uB0B4\uAE30/\uAC00\uC838\uC624\uAE30").setDesc("\uC124\uC815\uC744 JSON \uD30C\uC77C\uB85C \uB0B4\uBCF4\uB0B4\uAC70\uB098 \uB2E4\uB978 \uAE30\uAE30\uC5D0\uC11C \uAC00\uC838\uC62C \uC218 \uC788\uC2B5\uB2C8\uB2E4.").setHeading();
- const buttonGroup = importExportSection.createEl("div", { cls: "ksc-button-group" });
- const exportBtn = buttonGroup.createEl("button", { text: "\uC124\uC815 \uB0B4\uBCF4\uB0B4\uAE30" });
- const importBtn = buttonGroup.createEl("button", { text: "\uC124\uC815 \uAC00\uC838\uC624\uAE30" });
- const resetBtn = buttonGroup.createEl("button", {
- text: "\uAE30\uBCF8\uAC12\uC73C\uB85C \uC7AC\uC124\uC815",
- cls: "mod-warning"
- });
- exportBtn.onclick = () => {
- const jsonData = AdvancedSettingsService2.exportSettings(this.plugin.settings);
- const blob = new Blob([jsonData], { type: "application/json" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = `korean-grammar-settings-${new Date().toISOString().split("T")[0]}.json`;
- a.click();
- URL.revokeObjectURL(url);
- new import_obsidian12.Notice("\uC124\uC815\uC774 \uB0B4\uBCF4\uB0B4\uAE30\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- };
- importBtn.onclick = () => {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = ".json";
- input.onchange = (e) => {
- var _a;
- const file = (_a = e.target.files) == null ? void 0 : _a[0];
- if (!file)
- return;
- const reader = new FileReader();
- reader.onload = async (e2) => {
- var _a2;
- const content = (_a2 = e2.target) == null ? void 0 : _a2.result;
- const result = AdvancedSettingsService2.importSettings(content);
- if (result.success && result.settings) {
- AdvancedSettingsService2.backupSettings(this.plugin.settings, "\uAC00\uC838\uC624\uAE30 \uC804 \uBC31\uC5C5");
- this.plugin.settings = result.settings;
- await this.plugin.saveSettings();
- this.display();
- new import_obsidian12.Notice("\uC124\uC815\uC774 \uAC00\uC838\uC624\uAE30\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- } else {
- new import_obsidian12.Notice(`\uC124\uC815 \uAC00\uC838\uC624\uAE30 \uC2E4\uD328: ${result.error}`);
- }
- };
- reader.readAsText(file);
- };
- input.click();
- };
- resetBtn.onclick = async () => {
- const confirmed = confirm("\uBAA8\uB4E0 \uC124\uC815\uC744 \uAE30\uBCF8\uAC12\uC73C\uB85C \uC7AC\uC124\uC815\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? \uD604\uC7AC \uC124\uC815\uC740 \uBC31\uC5C5\uB429\uB2C8\uB2E4.");
- if (confirmed) {
- const defaultSettings = AdvancedSettingsService2.resetToDefaults(this.plugin.settings);
- this.plugin.settings = defaultSettings;
- await this.plugin.saveSettings();
- this.display();
- new import_obsidian12.Notice("\uC124\uC815\uC774 \uAE30\uBCF8\uAC12\uC73C\uB85C \uC7AC\uC124\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- }
- };
- }
- /**
- * 성능 모니터링 탭을 생성합니다
- */
- createMonitoringTab(containerEl) {
- this.createMetricsSection(containerEl);
- this.createLogManagementSection(containerEl);
- this.createPerformanceControlSection(containerEl);
- }
- /**
- * 실시간 메트릭 섹션을 생성합니다
- */
- createMetricsSection(containerEl) {
- const metricsSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(metricsSection).setName("\u{1F4CA} \uC2E4\uC2DC\uAC04 \uC131\uB2A5 \uBA54\uD2B8\uB9AD").setDesc("API \uD638\uCD9C \uC131\uB2A5\uACFC \uCE90\uC2DC \uD6A8\uC728\uC131\uC744 \uC2E4\uC2DC\uAC04\uC73C\uB85C \uBAA8\uB2C8\uD130\uB9C1\uD569\uB2C8\uB2E4.").setHeading();
- const metricsDisplay = metricsSection.createEl("div", {
- cls: "ksc-metrics",
- attr: { id: "metrics-display-container" }
- });
- const additionalStatsContainer = metricsSection.createEl("div", {
- attr: { id: "additional-stats-container" }
- });
- const buttonGroup = metricsSection.createEl("div", { cls: "ksc-button-group" });
- const refreshBtn = buttonGroup.createEl("button", { text: "\uC0C8\uB85C\uACE0\uCE68", cls: "mod-cta" });
- const updateMetrics = () => {
- try {
- const metrics = this.plugin.orchestrator.getPerformanceMetrics();
- const { Logger: Logger2 } = (init_logger(), __toCommonJS(logger_exports));
- const { ErrorHandlerService: ErrorHandlerService2 } = (init_errorHandler(), __toCommonJS(errorHandler_exports));
- const logStats = Logger2.getStats();
- const errorStats = ErrorHandlerService2.getErrorStats();
- const extendedMetrics = { ...metrics, logStats, errorStats };
- createMetricsDisplay(metricsDisplay, extendedMetrics);
- clearElement(additionalStatsContainer);
- const additionalStats = additionalStatsContainer.createEl("div", { cls: "ksc-info-box" });
- additionalStats.createEl("strong", { text: "\uB85C\uADF8 \uD1B5\uACC4:" });
- additionalStats.createEl("br");
- const logMetrics = [
- `\uCD1D \uB85C\uADF8: ${logStats.total}`,
- `\uC5D0\uB7EC: ${logStats.ERROR}`,
- `\uACBD\uACE0: ${logStats.WARN}`,
- `\uC815\uBCF4: ${logStats.INFO}`
- ];
- logMetrics.forEach((metric) => {
- additionalStats.createEl("div", { text: `\u2022 ${metric}` });
- });
- additionalStats.createEl("br");
- additionalStats.createEl("strong", { text: "\uC5D0\uB7EC \uBD84\uB958:" });
- additionalStats.createEl("br");
- const errorMetrics = [
- `\uB124\uD2B8\uC6CC\uD06C: ${errorStats.NETWORK_ERROR}`,
- `API \uD0A4: ${errorStats.API_KEY_ERROR}`,
- `\uC694\uCCAD \uC81C\uD55C: ${errorStats.API_RATE_LIMIT}`,
- `\uC11C\uBC84: ${errorStats.API_SERVER_ERROR}`,
- `\uD0C0\uC784\uC544\uC6C3: ${errorStats.TIMEOUT_ERROR}`
- ];
- errorMetrics.forEach((metric) => {
- additionalStats.createEl("div", { text: `\u2022 ${metric}` });
- });
- } catch (error) {
- clearElement(metricsDisplay);
- metricsDisplay.createEl("div", { text: "\uBA54\uD2B8\uB9AD\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." });
- }
- };
- refreshBtn.onclick = updateMetrics;
- updateMetrics();
- const metricsInterval = setInterval(updateMetrics, 15e3);
- const originalHide = this.hide.bind(this);
- this.hide = () => {
- clearInterval(metricsInterval);
- originalHide();
- };
- }
- /**
- * 로그 관리 섹션을 생성합니다
- */
- createLogManagementSection(containerEl) {
- const { Logger: Logger2 } = (init_logger(), __toCommonJS(logger_exports));
- const logSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(logSection).setName("\u{1F4CB} \uB85C\uADF8 \uAD00\uB9AC").setDesc("\uD50C\uB7EC\uADF8\uC778 \uC2E4\uD589 \uB85C\uADF8\uB97C \uD655\uC778\uD558\uACE0 \uB2E4\uC6B4\uB85C\uB4DC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.").setHeading();
- const updateLogStats = () => {
- const stats = Logger2.getStats();
- const memUsage = Logger2.getMemoryUsage();
- const statsContainer2 = logSection.querySelector("#log-stats-container");
- if (statsContainer2) {
- clearElement(statsContainer2);
- const statsBox = statsContainer2.createEl("div", { cls: "ksc-info-box" });
- statsBox.createEl("strong", { text: "\u{1F4CA} \uB85C\uADF8 \uD1B5\uACC4" });
- statsBox.createEl("br");
- statsBox.createEl("div", { text: `\u2022 \uCD1D \uB85C\uADF8: ${stats.total}\uAC1C` });
- statsBox.createEl("div", { text: `\u2022 \uC815\uBCF4: ${stats.INFO}\uAC1C` });
- statsBox.createEl("div", { text: `\u2022 \uACBD\uACE0: ${stats.WARN}\uAC1C` });
- statsBox.createEl("div", { text: `\u2022 \uC624\uB958: ${stats.ERROR}\uAC1C` });
- statsBox.createEl("div", { text: `\u2022 \uB514\uBC84\uADF8: ${stats.DEBUG}\uAC1C` });
- statsBox.createEl("br");
- statsBox.createEl("div", { text: `\u2022 \uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9: ${Math.round(memUsage.estimatedBytes / 1024)}KB` });
- statsBox.createEl("div", { text: `\u2022 \uCD5C\uB300 \uBCF4\uAD00: 1000\uAC1C` });
- }
- };
- const statsContainer = logSection.createEl("div", {
- attr: { id: "log-stats-container" }
- });
- const logViewerContainer = logSection.createEl("div", {
- attr: {
- id: "log-viewer-container",
- style: "display: none; margin-top: 16px;"
- }
- });
- const buttonGroup = logSection.createEl("div", { cls: "ksc-button-group" });
- const refreshStatsBtn = buttonGroup.createEl("button", { text: "\uD1B5\uACC4 \uC0C8\uB85C\uACE0\uCE68", cls: "mod-cta" });
- const viewLogsBtn = buttonGroup.createEl("button", { text: "\uB85C\uADF8 \uBCF4\uAE30" });
- const downloadLogsBtn = buttonGroup.createEl("button", { text: "\uB85C\uADF8 \uB2E4\uC6B4\uB85C\uB4DC" });
- const clearLogsBtn = buttonGroup.createEl("button", { text: "\uB85C\uADF8 \uC9C0\uC6B0\uAE30", cls: "mod-warning" });
- updateLogStats();
- refreshStatsBtn.onclick = updateLogStats;
- viewLogsBtn.onclick = () => {
- const isVisible = !logViewerContainer.hasClass("kga-hidden");
- if (isVisible) {
- logViewerContainer.addClass("kga-hidden");
- viewLogsBtn.textContent = "\uB85C\uADF8 \uBCF4\uAE30";
- } else {
- this.displayLogViewer(logViewerContainer);
- logViewerContainer.removeClass("kga-hidden");
- viewLogsBtn.textContent = "\uB85C\uADF8 \uC228\uAE30\uAE30";
- }
- };
- downloadLogsBtn.onclick = () => {
- this.downloadLogs();
- };
- clearLogsBtn.onclick = () => {
- if (confirm("\uBAA8\uB4E0 \uB85C\uADF8\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
- Logger2.clearHistory();
- updateLogStats();
- if (!logViewerContainer.hasClass("kga-hidden")) {
- this.displayLogViewer(logViewerContainer);
- }
- }
- };
- }
- /**
- * 로그 뷰어를 표시합니다
- */
- displayLogViewer(container) {
- const { Logger: Logger2 } = (init_logger(), __toCommonJS(logger_exports));
- clearElement(container);
- const logs = Logger2.getHistory();
- const filterContainer = container.createEl("div", { cls: "ksc-log-filter" });
- filterContainer.createEl("label", { text: "\uB808\uBCA8 \uD544\uD130:" });
- const levelSelect = filterContainer.createEl("select");
- levelSelect.createEl("option", { text: "\uC804\uCCB4", value: "" });
- levelSelect.createEl("option", { text: "\uC624\uB958", value: "ERROR" });
- levelSelect.createEl("option", { text: "\uACBD\uACE0", value: "WARN" });
- levelSelect.createEl("option", { text: "\uC815\uBCF4", value: "INFO" });
- levelSelect.createEl("option", { text: "\uB514\uBC84\uADF8", value: "DEBUG" });
- const logContent = container.createEl("div", { cls: "ksc-log-content" });
- const displayLogs = (filteredLogs) => {
- clearElement(logContent);
- if (filteredLogs.length === 0) {
- logContent.createEl("div", {
- text: "\uD45C\uC2DC\uD560 \uB85C\uADF8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
- cls: "ksc-log-empty"
- });
- return;
- }
- const sortedLogs = [...filteredLogs].reverse();
- sortedLogs.forEach((log) => {
- const logLine = logContent.createEl("div", {
- cls: `ksc-log-line ksc-log-${log.level.toLowerCase()}`
- });
- const timestamp = log.timestamp.toLocaleString("ko-KR");
- const levelIcon = this.getLogLevelIcon(log.level);
- logLine.createEl("span", {
- text: `[${timestamp}] ${levelIcon} `,
- cls: "ksc-log-timestamp"
- });
- logLine.createEl("span", {
- text: log.message,
- cls: "ksc-log-message"
- });
- if (log.data) {
- logLine.createEl("br");
- logLine.createEl("span", {
- text: ` \uB370\uC774\uD130: ${JSON.stringify(log.data, null, 2)}`,
- cls: "ksc-log-data"
- });
- }
- });
- };
- displayLogs(logs);
- levelSelect.onchange = () => {
- const selectedLevel = levelSelect.value;
- const filteredLogs = selectedLevel ? logs.filter((log) => log.level === selectedLevel) : logs;
- displayLogs(filteredLogs);
- };
- }
- /**
- * 로그 레벨별 아이콘을 반환합니다
- */
- getLogLevelIcon(level) {
- switch (level) {
- case "ERROR":
- return "\u274C";
- case "WARN":
- return "\u26A0\uFE0F";
- case "INFO":
- return "\u2139\uFE0F";
- case "DEBUG":
- return "\u{1F41B}";
- default:
- return "\u{1F4DD}";
- }
- }
- /**
- * 로그를 다운로드합니다
- */
- downloadLogs() {
- const { Logger: Logger2 } = (init_logger(), __toCommonJS(logger_exports));
- try {
- const logs = Logger2.getHistory();
- const stats = Logger2.getStats();
- const memUsage = Logger2.getMemoryUsage();
- const logData = {
- meta: {
- exportDate: new Date().toISOString(),
- pluginVersion: "0.2.0",
- totalLogs: stats.total,
- statistics: stats,
- memoryUsage: memUsage
- },
- logs: logs.map((log) => ({
- timestamp: log.timestamp.toISOString(),
- level: log.level,
- message: log.message,
- data: log.data
- }))
- };
- const jsonString = JSON.stringify(logData, null, 2);
- const blob = new Blob([jsonString], { type: "application/json" });
- const url = URL.createObjectURL(blob);
- const now = new Date();
- const timestamp = now.toISOString().replace(/T/, "_").replace(/:/g, "-").replace(/\.\d{3}Z$/, "");
- const a = document.createElement("a");
- a.href = url;
- a.download = `korean-grammar-logs-${timestamp}.json`;
- a.click();
- URL.revokeObjectURL(url);
- new import_obsidian12.Notice("\uB85C\uADF8\uAC00 \uB2E4\uC6B4\uB85C\uB4DC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- } catch (error) {
- Logger2.error("\uB85C\uADF8 \uB2E4\uC6B4\uB85C\uB4DC \uC624\uB958:", error);
- new import_obsidian12.Notice("\uB85C\uADF8 \uB2E4\uC6B4\uB85C\uB4DC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- /**
- * 성능 제어 섹션을 생성합니다
- */
- createPerformanceControlSection(containerEl) {
- const controlSection = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(controlSection).setName("\u{1F527} \uC131\uB2A5 \uC81C\uC5B4").setDesc("\uCE90\uC2DC \uBC0F \uB300\uAE30 \uC911\uC778 \uC694\uCCAD\uC744 \uAD00\uB9AC\uD558\uC5EC \uC131\uB2A5\uC744 \uCD5C\uC801\uD654\uD569\uB2C8\uB2E4.").setHeading();
- const buttonGroup = controlSection.createEl("div", { cls: "ksc-button-group" });
- const clearCacheBtn = buttonGroup.createEl("button", { text: "\uCE90\uC2DC \uC815\uB9AC" });
- const cancelRequestsBtn = buttonGroup.createEl("button", { text: "\uB300\uAE30 \uC694\uCCAD \uCDE8\uC18C" });
- clearCacheBtn.onclick = () => {
- this.plugin.orchestrator.clearCache();
- const metricsDisplay = document.getElementById("metrics-display-container");
- const additionalStats = document.getElementById("additional-stats-container");
- if (metricsDisplay && additionalStats) {
- try {
- const metrics = this.plugin.orchestrator.getPerformanceMetrics();
- const { Logger: Logger2 } = (init_logger(), __toCommonJS(logger_exports));
- const { ErrorHandlerService: ErrorHandlerService2 } = (init_errorHandler(), __toCommonJS(errorHandler_exports));
- const logStats = Logger2.getStats();
- const errorStats = ErrorHandlerService2.getErrorStats();
- const extendedMetrics = { ...metrics, logStats, errorStats };
- createMetricsDisplay(metricsDisplay, extendedMetrics);
- clearElement(additionalStats);
- const additionalStatsBox = additionalStats.createEl("div", { cls: "ksc-info-box" });
- additionalStatsBox.createEl("strong", { text: "\uB85C\uADF8 \uD1B5\uACC4:" });
- additionalStatsBox.createEl("br");
- const logMetrics = [
- `\uCD1D \uB85C\uADF8: ${logStats.total}`,
- `\uC5D0\uB7EC: ${logStats.ERROR}`,
- `\uACBD\uACE0: ${logStats.WARN}`,
- `\uC815\uBCF4: ${logStats.INFO}`
- ];
- logMetrics.forEach((metric) => {
- additionalStatsBox.createEl("div", { text: `\u2022 ${metric}` });
- });
- additionalStatsBox.createEl("br");
- additionalStatsBox.createEl("strong", { text: "\uC5D0\uB7EC \uBD84\uB958:" });
- additionalStatsBox.createEl("br");
- const errorMetrics = [
- `\uB124\uD2B8\uC6CC\uD06C: ${errorStats.NETWORK_ERROR}`,
- `API \uD0A4: ${errorStats.API_KEY_ERROR}`,
- `\uC694\uCCAD \uC81C\uD55C: ${errorStats.API_RATE_LIMIT}`,
- `\uC11C\uBC84: ${errorStats.API_SERVER_ERROR}`,
- `\uD0C0\uC784\uC544\uC6C3: ${errorStats.TIMEOUT_ERROR}`
- ];
- errorMetrics.forEach((metric) => {
- additionalStatsBox.createEl("div", { text: `\u2022 ${metric}` });
- });
- } catch (error) {
- Logger.error("\uBA54\uD2B8\uB9AD \uC5C5\uB370\uC774\uD2B8 \uC624\uB958:", error);
- }
- }
- };
- cancelRequestsBtn.onclick = () => {
- this.plugin.orchestrator.cancelPendingRequests();
- };
- const tipsBox = controlSection.createEl("div", { cls: "ksc-info-box" });
- tipsBox.createEl("strong", { text: "\u{1F4A1} \uC131\uB2A5 \uCD5C\uC801\uD654 \uD301:" });
- tipsBox.createEl("br");
- tipsBox.createEl("br");
- const tips = [
- "\u2022 \uCE90\uC2DC \uC815\uB9AC: \uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9\uC774 \uB192\uC744 \uB54C \uC2E4\uD589\uD558\uC138\uC694",
- "\u2022 \uC694\uCCAD \uCDE8\uC18C: \uB124\uD2B8\uC6CC\uD06C\uAC00 \uB290\uB9B4 \uB54C \uB300\uAE30 \uC911\uC778 \uC694\uCCAD\uC744 \uC815\uB9AC\uD558\uC138\uC694",
- "\u2022 AI \uD1A0\uD070 \uC218: 2000-3000 \uD1A0\uD070\uC774 \uC18D\uB3C4\uC640 \uD488\uC9C8\uC758 \uADE0\uD615\uC810\uC785\uB2C8\uB2E4",
- "\u2022 \uC608\uC678 \uB2E8\uC5B4: 500\uAC1C \uC774\uC0C1\uC77C \uB54C \uC815\uAE30\uC801\uC73C\uB85C \uC815\uB9AC\uD558\uC138\uC694"
- ];
- tips.forEach((tip) => {
- tipsBox.createEl("div", { text: tip });
- });
- }
- /**
- * 개선된 검증 결과 디스플레이
- */
- createImprovedValidationDisplay(parent, validation, suggestions) {
- clearElement(parent);
- const resultContainer = parent.createEl("div", { cls: "ksc-validation-result" });
- const statusCard = resultContainer.createEl("div", { cls: "ksc-status-card" });
- const statusLine = statusCard.createEl("div", { cls: "ksc-status-line" });
- const statusIcon = statusLine.createEl("span", {
- text: validation.isValid ? "\u2705" : "\u274C",
- cls: "ksc-status-icon"
- });
- const statusText = statusLine.createEl("span", {
- text: validation.isValid ? "\uBAA8\uB4E0 \uC124\uC815\uC774 \uC815\uC0C1\uC785\uB2C8\uB2E4" : "\uC77C\uBD80 \uC124\uC815\uC5D0 \uBB38\uC81C\uAC00 \uC788\uC2B5\uB2C8\uB2E4",
- cls: "ksc-status-text"
- });
- if (!validation.isValid) {
- if (validation.errors && validation.errors.length > 0) {
- const problemsSection = resultContainer.createEl("div", { cls: "ksc-problems-section" });
- problemsSection.createEl("div", {
- text: "\u{1F527} \uD574\uACB0\uD574\uC57C \uD560 \uBB38\uC81C",
- cls: "ksc-section-title"
- });
- const problemsList = problemsSection.createEl("ul", { cls: "ksc-problems-list" });
- validation.errors.forEach((error) => {
- problemsList.createEl("li", { text: error });
- });
- }
- }
- if (validation.warnings && validation.warnings.length > 0) {
- const warningsSection = resultContainer.createEl("div", { cls: "ksc-warnings-section" });
- warningsSection.createEl("div", {
- text: "\u26A0\uFE0F \uC8FC\uC758\uC0AC\uD56D",
- cls: "ksc-section-title"
- });
- const warningsList = warningsSection.createEl("ul", { cls: "ksc-warnings-list" });
- validation.warnings.forEach((warning) => {
- warningsList.createEl("li", { text: warning });
- });
- }
- if (suggestions && suggestions.length > 0) {
- const suggestionsSection = resultContainer.createEl("div", { cls: "ksc-suggestions-section" });
- suggestionsSection.createEl("div", {
- text: "\u{1F4A1} \uAC1C\uC120 \uC81C\uC548",
- cls: "ksc-section-title"
- });
- suggestions.forEach((suggestion) => {
- const suggestionCard = suggestionsSection.createEl("div", { cls: "ksc-suggestion-card" });
- const impactBadge = suggestion.impact === "high" ? "\u{1F534}" : suggestion.impact === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
- suggestionCard.createEl("div", {
- text: `${impactBadge} ${suggestion.title}`,
- cls: "ksc-suggestion-title"
- });
- suggestionCard.createEl("div", {
- text: suggestion.action,
- cls: "ksc-suggestion-desc"
- });
- });
- }
- const summarySection = resultContainer.createEl("div", { cls: "ksc-summary-section" });
- summarySection.createEl("div", {
- text: "\u{1F4CB} \uD604\uC7AC \uC124\uC815 \uC0C1\uD0DC",
- cls: "ksc-section-title"
- });
- const summaryGrid = summarySection.createEl("div", { cls: "ksc-summary-grid" });
- const apiStatus = summaryGrid.createEl("div", { cls: "ksc-summary-item" });
- apiStatus.createEl("span", { text: "API", cls: "ksc-summary-label" });
- apiStatus.createEl("span", {
- text: this.plugin.settings.apiKey ? "\u2713 \uC124\uC815\uB428" : "\u2717 \uBBF8\uC124\uC815",
- cls: this.plugin.settings.apiKey ? "ksc-status-ok" : "ksc-status-error"
- });
- const aiStatus = summaryGrid.createEl("div", { cls: "ksc-summary-item" });
- aiStatus.createEl("span", { text: "AI \uAE30\uB2A5", cls: "ksc-summary-label" });
- const aiText = this.plugin.settings.ai.enabled ? `\u2713 ${this.plugin.settings.ai.provider.toUpperCase()}` : "\u2717 \uBE44\uD65C\uC131\uD654";
- aiStatus.createEl("span", {
- text: aiText,
- cls: this.plugin.settings.ai.enabled ? "ksc-status-ok" : "ksc-status-disabled"
- });
- const ignoredStatus = summaryGrid.createEl("div", { cls: "ksc-summary-item" });
- ignoredStatus.createEl("span", { text: "\uC608\uC678 \uB2E8\uC5B4", cls: "ksc-summary-label" });
- const wordCount = this.plugin.settings.ignoredWords.length;
- const wordStatus = wordCount === 0 ? "\uC5C6\uC74C" : wordCount > 100 ? `${wordCount}\uAC1C (\uB9CE\uC74C)` : `${wordCount}\uAC1C`;
- ignoredStatus.createEl("span", {
- text: wordStatus,
- cls: wordCount > 100 ? "ksc-status-warning" : "ksc-status-ok"
- });
- }
- /**
- * 필터링 옵션 섹션을 생성합니다
- */
- createFilteringOptionsSection(containerEl) {
- const section = containerEl.createEl("div", { cls: "ksc-section" });
- new import_obsidian12.Setting(section).setName("\u{1F50D} \uD544\uD130\uB9C1 \uC635\uC158").setDesc("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uACB0\uACFC\uB97C \uD544\uD130\uB9C1\uD558\uB294 \uC635\uC158\uC744 \uC124\uC815\uD569\uB2C8\uB2E4.").setHeading();
- new import_obsidian12.Setting(section).setName("\uD55C \uAE00\uC790 \uC624\uB958 \uD544\uD130\uB9C1").setDesc("\uD55C \uAE00\uC790\uB85C \uB41C \uB9DE\uCDA4\uBC95 \uC624\uB958 \uC81C\uC548\uC744 \uD544\uD130\uB9C1\uD569\uB2C8\uB2E4. \uC758\uBBF8\uC788\uB294 \uAD50\uC815(\uC870\uC0AC, \uC5B4\uBBF8 \uB4F1)\uC740 \uC608\uC678 \uCC98\uB9AC\uB429\uB2C8\uB2E4.").addToggle((toggle) => toggle.setValue(this.plugin.settings.filterSingleCharErrors).onChange(async (value) => {
- this.plugin.settings.filterSingleCharErrors = value;
- await this.plugin.saveSettings();
- }));
- const infoBox = section.createEl("div", { cls: "ksc-info-box" });
- infoBox.createEl("strong", { text: "\u{1F6E1}\uFE0F \uC608\uC678 \uCC98\uB9AC\uB418\uB294 \uD55C \uAE00\uC790 \uAD50\uC815" });
- infoBox.createEl("br");
- infoBox.createEl("br");
- const exceptions = [
- '\u2022 \uC22B\uC790/\uC601\uBB38 \u2192 \uD55C\uAE00 \uBCC0\uD658 (\uC608: "1" \u2192 "\uC77C")',
- '\u2022 \uD2B9\uC218\uBB38\uC790 \u2192 \uD55C\uAE00 \uBCC0\uD658 (\uC608: "@" \u2192 "\uC5D0")',
- '\u2022 \uC790\uC8FC \uD2C0\uB9AC\uB294 \uC870\uC0AC/\uC5B4\uBBF8 (\uC608: "\uB418" \u2194 "\uB3FC", "\uC548" \u2194 "\uC54A")',
- '\u2022 \uD55C \uAE00\uC790 \u2192 \uC5EC\uB7EC \uAE00\uC790 \uD655\uC7A5 (\uC608: "\uD558" \u2192 "\uD558\uC5EC")'
- ];
- exceptions.forEach((exception) => {
- infoBox.createEl("div", {
- text: exception,
- attr: { style: "margin-bottom: 4px; color: var(--text-muted);" }
- });
- });
- const statsContainer = section.createEl("div", { cls: "ksc-filter-stats" });
- this.updateFilteringStats(statsContainer);
- }
- /**
- * 필터링 통계를 업데이트합니다
- */
- updateFilteringStats(container) {
- container.empty();
- const statsBox = container.createEl("div", {
- cls: "ksc-stats-box",
- attr: { style: "margin-top: 12px; padding: 12px; background: var(--background-secondary); border-radius: 6px;" }
- });
- statsBox.createEl("div", {
- text: "\u{1F4CA} \uD544\uD130\uB9C1 \uD1B5\uACC4",
- attr: { style: "font-weight: 600; margin-bottom: 8px;" }
- });
- const statusText = this.plugin.settings.filterSingleCharErrors ? "\u2705 \uD55C \uAE00\uC790 \uC624\uB958 \uD544\uD130\uB9C1\uC774 \uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4." : "\u26A0\uFE0F \uD55C \uAE00\uC790 \uC624\uB958 \uD544\uD130\uB9C1\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.";
- statsBox.createEl("div", {
- text: statusText,
- attr: { style: "color: var(--text-muted); font-size: 14px;" }
- });
- if (this.plugin.settings.filterSingleCharErrors) {
- statsBox.createEl("div", {
- text: "\u{1F4A1} \uD301: \uC758\uBBF8\uC788\uB294 \uD55C \uAE00\uC790 \uAD50\uC815(\uC870\uC0AC, \uC5B4\uBBF8 \uB4F1)\uC740 \uC790\uB3D9\uC73C\uB85C \uC608\uC678 \uCC98\uB9AC\uB429\uB2C8\uB2E4.",
- attr: { style: "color: var(--text-accent); font-size: 13px; margin-top: 4px;" }
- });
- }
- }
- /**
- * 베타 기능 탭을 생성합니다
- */
- createBetaFeaturesTab(containerEl) {
- this.createBetaWarningSection(containerEl);
- this.createInlineModeSection(containerEl);
- }
- /**
- * 베타 기능 경고 섹션을 생성합니다
- */
- createBetaWarningSection(containerEl) {
- const section = containerEl.createEl("div", { cls: "ksc-section" });
- const warningHeading = new import_obsidian12.Setting(section).setName("\u26A0\uFE0F \uBCA0\uD0C0 \uAE30\uB2A5 \uC548\uB0B4").setHeading();
- warningHeading.settingEl.addClasses(["ksc-section-title"]);
- const warningBox = section.createEl("div", {
- cls: "ksc-warning-box",
- attr: {
- style: "background: rgba(255, 196, 0, 0.1); border: 1px solid rgba(255, 196, 0, 0.3); border-radius: 8px; padding: 16px; margin-bottom: 20px;"
- }
- });
- warningBox.createEl("div", {
- text: "\u{1F9EA} \uC2E4\uD5D8\uC801 \uAE30\uB2A5",
- attr: { style: "font-weight: 600; color: var(--text-warning); margin-bottom: 8px;" }
- });
- const warnings = [
- "\uC774 \uC139\uC158\uC758 \uAE30\uB2A5\uB4E4\uC740 \uBCA0\uD0C0 \uBC84\uC804\uC785\uB2C8\uB2E4.",
- "\uC77C\uBD80 \uAE30\uB2A5\uC774 \uC608\uC0C1\uACFC \uB2E4\uB974\uAC8C \uB3D9\uC791\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
- "\uD53C\uB4DC\uBC31\uACFC \uBC84\uADF8 \uB9AC\uD3EC\uD2B8\uB294 \uC5B8\uC81C\uB098 \uD658\uC601\uD569\uB2C8\uB2E4.",
- "\uC548\uC815\uD654 \uD6C4 \uC815\uC2DD \uAE30\uB2A5\uC73C\uB85C \uC2B9\uACA9\uB420 \uC608\uC815\uC785\uB2C8\uB2E4."
- ];
- warnings.forEach((warning) => {
- warningBox.createEl("div", {
- text: `\u2022 ${warning}`,
- attr: { style: "color: var(--text-muted); margin-bottom: 4px; font-size: 14px;" }
- });
- });
- }
- /**
- * 인라인 모드 설정 섹션을 생성합니다
- */
- createInlineModeSection(containerEl) {
- const section = containerEl.createEl("div", { cls: "ksc-section" });
- const inlineHeading = new import_obsidian12.Setting(section).setName("\u{1F4DD} \uC778\uB77C\uC778 \uBAA8\uB4DC").setHeading();
- inlineHeading.settingEl.addClasses(["ksc-section-title"]);
- const descBox = section.createEl("div", {
- cls: "ksc-info-box",
- attr: {
- style: "background: var(--background-secondary); border-radius: 8px; padding: 16px; margin-bottom: 20px;"
- }
- });
- descBox.createEl("div", {
- text: "\u{1F3AF} \uC5D0\uB514\uD130 \uB0B4 \uC2E4\uC2DC\uAC04 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC",
- attr: { style: "font-weight: 600; margin-bottom: 8px;" }
- });
- const features = [
- "\uC624\uD0C0 \uD14D\uC2A4\uD2B8\uC5D0 \uBC11\uC904 \uD45C\uC2DC",
- "\uD638\uBC84/\uD074\uB9AD\uC73C\uB85C \uC218\uC815 \uC81C\uC548 \uD655\uC778",
- "\uC0AC\uC6A9\uC790 \uD3B8\uC9D1 \uC2DC \uBC11\uC904 \uC790\uB3D9 \uC81C\uAC70",
- "Command Palette\uB85C \uAC80\uC0AC \uC2E4\uD589"
- ];
- features.forEach((feature) => {
- descBox.createEl("div", {
- text: `\u2022 ${feature}`,
- attr: { style: "color: var(--text-muted); margin-bottom: 4px;" }
- });
- });
- new import_obsidian12.Setting(section).setName("\uC778\uB77C\uC778 \uBAA8\uB4DC \uD65C\uC131\uD654").setDesc("\uC5D0\uB514\uD130 \uB0B4\uC5D0\uC11C \uC2E4\uC2DC\uAC04\uC73C\uB85C \uB9DE\uCDA4\uBC95 \uC624\uB958\uB97C \uD45C\uC2DC\uD569\uB2C8\uB2E4.").addToggle((toggle) => toggle.setValue(this.plugin.settings.inlineMode.enabled).onChange(async (value) => {
- this.plugin.settings.inlineMode.enabled = value;
- await this.plugin.saveSettings();
- if (value) {
- this.plugin.enableInlineMode();
- Logger.log("\uC778\uB77C\uC778 \uBAA8\uB4DC \uC989\uC2DC \uD65C\uC131\uD654");
- new import_obsidian12.Notice("\uC778\uB77C\uC778 \uBAA8\uB4DC\uAC00 \uD65C\uC131\uD654\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC5D0\uB514\uD130\uC5D0\uC11C \uD14D\uC2A4\uD2B8\uB97C \uC785\uB825\uD558\uBA74 \uC2E4\uC2DC\uAC04 \uAC80\uC0AC\uAC00 \uC2DC\uC791\uB429\uB2C8\uB2E4.");
- } else {
- this.plugin.disableInlineMode();
- Logger.log("\uC778\uB77C\uC778 \uBAA8\uB4DC \uC989\uC2DC \uBE44\uD65C\uC131\uD654");
- new import_obsidian12.Notice("\uC778\uB77C\uC778 \uBAA8\uB4DC\uAC00 \uBE44\uD65C\uC131\uD654\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- }
- this.display();
- }));
- if (this.plugin.settings.inlineMode.enabled) {
- new import_obsidian12.Setting(section).setName("\uBC11\uC904 \uC2A4\uD0C0\uC77C").setDesc("\uC624\uB958 \uD45C\uC2DC\uC5D0 \uC0AC\uC6A9\uD560 \uBC11\uC904 \uC2A4\uD0C0\uC77C\uC744 \uC120\uD0DD\uD558\uC138\uC694.").addDropdown((dropdown) => dropdown.addOption("wavy", "\uBB3C\uACB0\uC120 (\uCD94\uCC9C)").addOption("solid", "\uC9C1\uC120").addOption("dotted", "\uC810\uC120").addOption("dashed", "\uD30C\uC120").setValue(this.plugin.settings.inlineMode.underlineStyle).onChange(async (value) => {
- this.plugin.settings.inlineMode.underlineStyle = value;
- await this.plugin.saveSettings();
- }));
- new import_obsidian12.Setting(section).setName("\uBC11\uC904 \uC0C9\uC0C1").setDesc("\uC624\uB958 \uD45C\uC2DC\uC5D0 \uC0AC\uC6A9\uD560 \uBC11\uC904 \uC0C9\uC0C1\uC744 \uC124\uC815\uD558\uC138\uC694.").addText((text) => text.setPlaceholder("#ff0000").setValue(this.plugin.settings.inlineMode.underlineColor).onChange(async (value) => {
- if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
- this.plugin.settings.inlineMode.underlineColor = value;
- await this.plugin.saveSettings();
- }
- }));
- new import_obsidian12.Setting(section).setName("\uD234\uD301 \uD45C\uC2DC \uBC29\uC2DD").setDesc('\uBB38\uBC95 \uC624\uB958 \uC218\uC815 \uC81C\uC548\uC744 \uD45C\uC2DC\uD560 \uBC29\uBC95\uC744 \uC120\uD0DD\uD558\uC138\uC694. "\uC790\uB3D9"\uC740 \uD50C\uB7AB\uD3FC\uC5D0 \uCD5C\uC801\uD654\uB41C \uBC29\uC2DD\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.').addDropdown((dropdown) => dropdown.addOption("auto", "\u{1F916} \uC790\uB3D9 (\uAD8C\uC7A5) - \uD50C\uB7AB\uD3FC\uBCC4 \uCD5C\uC801\uD654").addOption("hover", "\u{1F5B1}\uFE0F \uB9C8\uC6B0\uC2A4 \uD638\uBC84 - \uB370\uC2A4\uD06C\uD1B1 \uC804\uC6A9").addOption("click", "\u{1F446} \uD074\uB9AD/\uD0ED - \uBAA8\uBC14\uC77C \uCE5C\uD654\uC801").addOption("disabled", "\u{1F6AB} \uD234\uD301 \uBE44\uD65C\uC131\uD654").setValue(this.plugin.settings.inlineMode.tooltipTrigger || "auto").onChange(async (value) => {
- this.plugin.settings.inlineMode.tooltipTrigger = value;
- switch (value) {
- case "auto":
- this.plugin.settings.inlineMode.showTooltipOnHover = true;
- this.plugin.settings.inlineMode.showTooltipOnClick = true;
- break;
- case "hover":
- this.plugin.settings.inlineMode.showTooltipOnHover = true;
- this.plugin.settings.inlineMode.showTooltipOnClick = false;
- break;
- case "click":
- this.plugin.settings.inlineMode.showTooltipOnHover = false;
- this.plugin.settings.inlineMode.showTooltipOnClick = true;
- break;
- case "disabled":
- this.plugin.settings.inlineMode.showTooltipOnHover = false;
- this.plugin.settings.inlineMode.showTooltipOnClick = false;
- break;
- }
- await this.plugin.saveSettings();
- const modeNames = {
- "auto": "\uC790\uB3D9 \uBAA8\uB4DC (\uD50C\uB7AB\uD3FC\uBCC4 \uCD5C\uC801\uD654)",
- "hover": "\uD638\uBC84 \uBAA8\uB4DC (\uB370\uC2A4\uD06C\uD1B1 \uC804\uC6A9)",
- "click": "\uD074\uB9AD \uBAA8\uB4DC (\uBAA8\uBC14\uC77C \uCE5C\uD654\uC801)",
- "disabled": "\uD234\uD301 \uBE44\uD65C\uC131\uD654"
- };
- new import_obsidian12.Notice(`\uD234\uD301 \uD45C\uC2DC \uBC29\uC2DD: ${modeNames[value]}`);
- }));
- section.createEl("div", {
- text: "\u{1F4A1} \uC790\uB3D9 \uBAA8\uB4DC: \uB370\uC2A4\uD06C\uD1B1\uC5D0\uC11C\uB294 \uD638\uBC84, \uBAA8\uBC14\uC77C\uC5D0\uC11C\uB294 \uD0ED\uC73C\uB85C \uC790\uB3D9 \uB3D9\uC791",
- attr: {
- style: "font-size: 0.9em; color: var(--text-muted); margin-top: 8px; padding: 8px; background: var(--background-secondary); border-radius: 4px;"
- }
- });
- }
- }
-};
-
-// main.ts
-init_logger();
-
-// src/services/inlineModeService.ts
-var import_view = require("@codemirror/view");
-var import_state = require("@codemirror/state");
-init_logger();
-
-// src/ui/inlineTooltip.ts
-init_logger();
-var import_obsidian13 = require("obsidian");
-var InlineTooltip = class {
- constructor() {
- this.tooltip = null;
- this.currentError = null;
- this.isVisible = false;
- this.hoverTimeout = null;
- this.hideTimeout = null;
- this.isHovered = false;
- }
- // 🔍 툴팁 호버 상태 추적
- /**
- * 툴팁 표시
- */
- show(error, targetElement, triggerType, mousePosition) {
- var _a;
- if (this.isVisible && ((_a = this.currentError) == null ? void 0 : _a.uniqueId) === error.uniqueId) {
- Logger.debug(`\uC778\uB77C\uC778 \uD234\uD301 \uC774\uBBF8 \uD45C\uC2DC \uC911: ${error.correction.original}`);
- return;
- }
- this.hide(true);
- this.currentError = error;
- this.hideCursorInBackground();
- if (import_obsidian13.Platform.isMobile) {
- window.tooltipProtected = true;
- setTimeout(() => {
- this.hideKeyboardAndBlurEditor();
- Logger.debug("\u{1F4F1} \uBAA8\uBC14\uC77C \uD234\uD301 \uBCF4\uD638 \uD50C\uB798\uADF8 \uC720\uC9C0 - \uC218\uB3D9 \uB2EB\uAE30\uB9CC \uD5C8\uC6A9");
- }, 100);
- }
- this.createTooltip(error, targetElement, triggerType);
- this.positionTooltip(targetElement, mousePosition);
- this.isVisible = true;
- Logger.log(`\uC778\uB77C\uC778 \uD234\uD301 \uD45C\uC2DC: "${error.correction.original}" (${triggerType})`);
- }
- /**
- * 툴팁 숨김
- */
- hide(forceHide = false) {
- if (import_obsidian13.Platform.isMobile && !forceHide) {
- Logger.debug("\u{1F4F1} \uBAA8\uBC14\uC77C \uD234\uD301: \uC790\uB3D9 \uC228\uAE40 \uBB34\uC2DC - \uC218\uB3D9 \uB2EB\uAE30\uB9CC \uD5C8\uC6A9");
- return;
- }
- if (import_obsidian13.Platform.isMobile && forceHide) {
- window.tooltipProtected = false;
- Logger.debug("\u{1F4F1} \uBAA8\uBC14\uC77C \uD234\uD301: \uC218\uB3D9 \uB2EB\uAE30\uB85C \uBCF4\uD638 \uD50C\uB798\uADF8 \uD574\uC81C");
- }
- if (this.tooltip) {
- this.tooltip.remove();
- this.tooltip = null;
- }
- this.showCursorInBackground();
- this.isVisible = false;
- this.currentError = null;
- this.isHovered = false;
- if (this.hoverTimeout) {
- clearTimeout(this.hoverTimeout);
- this.hoverTimeout = null;
- }
- if (this.hideTimeout) {
- clearTimeout(this.hideTimeout);
- this.hideTimeout = null;
- }
- }
- /**
- * 지연 후 툴팁 숨기기 예약
- */
- scheduleHide(delay) {
- this.clearHideTimeout();
- this.hideTimeout = setTimeout(() => {
- if (!this.isHovered) {
- Logger.debug(`\u{1F550} \uC608\uC57D\uB41C \uD234\uD301 \uC228\uAE30\uAE30 \uC2E4\uD589 (${delay}ms \uD6C4)`);
- this.hide();
- }
- }, delay);
- }
- /**
- * 툴팁 숨기기 타이머 취소
- */
- clearHideTimeout() {
- if (this.hideTimeout) {
- clearTimeout(this.hideTimeout);
- this.hideTimeout = null;
- Logger.debug("\u23F0 \uD234\uD301 \uC228\uAE30\uAE30 \uD0C0\uC774\uBA38 \uCDE8\uC18C");
- }
- }
- /**
- * 툴팁이 표시 중인지 확인
- */
- get visible() {
- return this.isVisible;
- }
- /**
- * 툴팁 생성
- */
- createTooltip(error, targetElement, triggerType) {
- var _a;
- this.tooltip = document.createElement("div");
- this.tooltip.className = "korean-grammar-inline-tooltip";
- const isMobile = import_obsidian13.Platform.isMobile;
- const isMobilePhone = import_obsidian13.Platform.isMobileApp;
- if (isMobile) {
- this.tooltip.classList.add("kga-mobile");
- }
- if (isMobilePhone) {
- this.tooltip.classList.add("kga-mobile-phone");
- }
- if (isMobile) {
- this.tooltip.addEventListener("touchstart", (e) => {
- e.stopPropagation();
- }, { passive: true });
- this.tooltip.addEventListener("touchend", (e) => {
- e.stopPropagation();
- }, { passive: true });
- }
- Logger.debug(`\u{1F50D} \uD234\uD301 \uD0C0\uC785 \uACB0\uC815: "${error.correction.original}"`);
- Logger.debug(` isMerged: ${error.isMerged}`);
- Logger.debug(` originalErrors: ${((_a = error.originalErrors) == null ? void 0 : _a.length) || 0}\uAC1C`);
- Logger.debug(` correction.corrected: [${error.correction.corrected.join(", ")}]`);
- if (error.isMerged && error.originalErrors && error.originalErrors.length > 0) {
- Logger.debug(`\u{1F527} \uBCD1\uD569\uB41C \uD234\uD301 \uC0AC\uC6A9: ${error.originalErrors.length}\uAC1C \uC6D0\uBCF8 \uC624\uB958`);
- this.createMergedErrorTooltip(error, targetElement, triggerType);
- } else {
- Logger.debug(`\u{1F527} \uB2E8\uC77C \uD234\uD301 \uC0AC\uC6A9`);
- this.createSingleErrorTooltip(error, targetElement, triggerType);
- }
- this.tooltip.addEventListener("mouseenter", () => {
- this.isHovered = true;
- this.clearHideTimeout();
- Logger.debug("\u{1F5B1}\uFE0F \uD234\uD301 \uB9C8\uC6B0\uC2A4 \uC9C4\uC785 - \uD638\uBC84 \uC0C1\uD0DC \uC720\uC9C0");
- });
- this.tooltip.addEventListener("mouseleave", () => {
- var _a2, _b, _c;
- this.isHovered = false;
- Logger.debug("\u{1F5B1}\uFE0F \uD234\uD301 \uB9C8\uC6B0\uC2A4 \uC774\uD0C8 - \uD638\uBC84 \uC0C1\uD0DC \uD574\uC81C");
- const isAITooltip = ((_a2 = this.currentError) == null ? void 0 : _a2.aiStatus) === "corrected" || ((_b = this.currentError) == null ? void 0 : _b.aiStatus) === "exception" || ((_c = this.currentError) == null ? void 0 : _c.aiStatus) === "keep-original";
- const hideDelay = isAITooltip ? 800 : 300;
- this.scheduleHide(hideDelay);
- });
- document.body.appendChild(this.tooltip);
- if (isMobile) {
- Logger.log(`\u{1F4F1} \uBAA8\uBC14\uC77C \uD234\uD301 \uC0DD\uC131: ${error.correction.original} (${triggerType})`);
- }
- }
- /**
- * 툴팁 위치 조정 (Obsidian API 기반 고급 처리)
- */
- positionTooltip(targetElement, mousePosition) {
- if (!this.tooltip)
- return;
- const targetRect = targetElement.getBoundingClientRect();
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
- const isMobile = import_obsidian13.Platform.isMobile;
- const isPhone = import_obsidian13.Platform.isPhone || viewportWidth <= 480;
- const isTablet = import_obsidian13.Platform.isTablet || viewportWidth <= 768 && viewportWidth > 480;
- const app = window.app;
- let editorScrollInfo = null;
- let editorContainerRect = null;
- if (app && app.workspace) {
- try {
- const activeView = app.workspace.getActiveViewOfType(import_obsidian13.MarkdownView);
- if (activeView && activeView.editor) {
- editorScrollInfo = activeView.editor.getScrollInfo();
- if (activeView.containerEl) {
- editorContainerRect = activeView.containerEl.getBoundingClientRect();
- }
- }
- } catch (error) {
- Logger.debug("Obsidian API \uC811\uADFC \uC911 \uC624\uB958 (\uBB34\uC2DC\uB428):", error);
- }
- }
- const scrollTop = (editorScrollInfo == null ? void 0 : editorScrollInfo.top) || window.pageYOffset || document.documentElement.scrollTop;
- const scrollLeft = (editorScrollInfo == null ? void 0 : editorScrollInfo.left) || window.pageXOffset || document.documentElement.scrollLeft;
- const baseViewportHeight = window.screen.height || viewportHeight;
- const keyboardVisible = isMobile && viewportHeight < baseViewportHeight * 0.75;
- const keyboardHeight = keyboardVisible ? baseViewportHeight - viewportHeight : 0;
- Logger.debug(`\u{1F527} \uC704\uCE58 \uACC4\uC0B0 \uC815\uBCF4:`, {
- isMobile,
- isPhone,
- isTablet,
- viewportSize: `${viewportWidth}x${viewportHeight}`,
- keyboardVisible,
- keyboardHeight,
- targetRect: `${targetRect.left},${targetRect.top} ${targetRect.width}x${targetRect.height}`,
- scroll: `${scrollLeft},${scrollTop}`,
- editorContainer: editorContainerRect ? `${editorContainerRect.width}x${editorContainerRect.height}` : "none",
- obsidianAPI: !!app
- });
- if (isMobile) {
- const useFixedPosition = isPhone;
- if (useFixedPosition) {
- this.positionTooltipMobileFixed(targetElement, targetRect, viewportWidth, viewportHeight, keyboardHeight, mousePosition);
- } else {
- this.positionTooltipMobile(targetElement, targetRect, viewportWidth, viewportHeight, keyboardHeight, isPhone, editorContainerRect, mousePosition);
- }
- } else {
- this.positionTooltipDesktop(targetElement, targetRect, viewportWidth, viewportHeight, editorContainerRect, mousePosition);
- }
- }
- /**
- * 모바일 고정 위치 툴팁 (화면 하단 고정)
- */
- positionTooltipMobileFixed(targetElement, targetRect, viewportWidth, viewportHeight, keyboardHeight, mousePosition) {
- var _a;
- if (!this.tooltip)
- return;
- Logger.log("\u{1F4F1} \uBAA8\uBC14\uC77C \uACE0\uC815 \uC704\uCE58 \uD234\uD301 \uD65C\uC131\uD654");
- const hasAIAnalysis = this.tooltip.querySelector(".tooltip-ai-area") !== null;
- const fixedWidth = viewportWidth - 16;
- const fixedHeight = hasAIAnalysis ? 110 : 85;
- this.tooltip.classList.add("kga-fixed-position");
- if (hasAIAnalysis) {
- this.tooltip.classList.add("kga-has-ai");
- }
- this.tooltip.style.setProperty("--kga-width", `${fixedWidth}px`);
- this.tooltip.style.setProperty("--kga-fixed-height", `${fixedHeight}px`);
- const safeBottomMargin = hasAIAnalysis ? 90 : 80;
- const bottomOffset = keyboardHeight > 0 ? keyboardHeight + 12 : safeBottomMargin;
- const finalLeft = (viewportWidth - fixedWidth) / 2;
- const finalTop = viewportHeight - fixedHeight - bottomOffset;
- this.tooltip.style.setProperty("--kga-left", `${finalLeft}px`);
- this.tooltip.style.setProperty("--kga-top", `${finalTop}px`);
- Logger.log(`\u{1F4F1} \uACE0\uC815 \uD234\uD301 (${hasAIAnalysis ? "AI+" : "\uAE30\uBCF8"}): ${fixedWidth}x${fixedHeight}px (${Math.round(fixedWidth / viewportWidth * 100)}% \uB108\uBE44)`, {
- position: `(${finalLeft}, ${finalTop})`,
- bottomMargin: `${safeBottomMargin}px (${hasAIAnalysis ? "AI \uD655\uC7A5" : "\uAE30\uBCF8"})`,
- keyboard: keyboardHeight > 0,
- aiAnalysis: hasAIAnalysis,
- target: ((_a = targetElement.textContent) == null ? void 0 : _a.substring(0, 15)) || "unknown"
- });
- }
- /**
- * 모바일 툴팁 위치 계산 (화면 구석 완전 대응)
- */
- positionTooltipMobile(targetElement, targetRect, viewportWidth, viewportHeight, keyboardHeight, isPhone, editorContainerRect = null, mousePosition) {
- if (!this.tooltip)
- return;
- const editorLeft = (editorContainerRect == null ? void 0 : editorContainerRect.left) || 0;
- const editorTop = (editorContainerRect == null ? void 0 : editorContainerRect.top) || 0;
- const editorWidth = (editorContainerRect == null ? void 0 : editorContainerRect.width) || viewportWidth;
- const editorHeight = (editorContainerRect == null ? void 0 : editorContainerRect.height) || viewportHeight;
- const adaptiveSize = this.calculateAdaptiveTooltipSize(
- viewportWidth,
- viewportHeight,
- keyboardHeight,
- isPhone,
- editorWidth,
- editorHeight
- );
- this.tooltip.classList.add("kga-dynamic-position");
- this.tooltip.style.setProperty("--kga-width", `${adaptiveSize.width}px`);
- this.tooltip.style.setProperty("--kga-max-height", `${adaptiveSize.maxHeight}px`);
- this.tooltip.style.setProperty("--kga-min-width", `${adaptiveSize.minWidth}px`);
- this.tooltip.style.setProperty("--kga-font-size", adaptiveSize.fontSize);
- let referenceCenterX;
- let referenceCenterY;
- if (mousePosition) {
- referenceCenterX = mousePosition.x;
- referenceCenterY = mousePosition.y;
- Logger.debug(`\u{1F3AF} \uD130\uCE58 \uC704\uCE58 \uAE30\uBC18 \uD234\uD301 \uBC30\uCE58: (${mousePosition.x}, ${mousePosition.y})`);
- } else {
- referenceCenterX = targetRect.left + targetRect.width / 2;
- referenceCenterY = targetRect.top + targetRect.height / 2;
- Logger.debug(`\u{1F4CD} \uD0C0\uAC9F \uC694\uC18C \uAE30\uBC18 \uD234\uD301 \uBC30\uCE58: (${referenceCenterX}, ${referenceCenterY})`);
- }
- const cornerThreshold = mousePosition ? 40 : 60;
- const effectiveLeft = Math.max(referenceCenterX - 8, editorLeft);
- const effectiveRight = Math.min(referenceCenterX + 8, editorLeft + editorWidth);
- const effectiveTop = Math.max(referenceCenterY - 10, editorTop);
- const effectiveBottom = Math.min(referenceCenterY + 10, editorTop + editorHeight);
- const isLeftEdge = effectiveLeft - editorLeft < cornerThreshold;
- const isRightEdge = editorLeft + editorWidth - effectiveRight < cornerThreshold;
- const isTopEdge = effectiveTop - editorTop < cornerThreshold;
- const isBottomEdge = editorTop + editorHeight - effectiveBottom < cornerThreshold;
- const fingerOffset = mousePosition ? isPhone ? 15 : 12 : isPhone ? 25 : 20;
- const safeMargin = 16;
- let finalLeft = 0;
- let finalTop = 0;
- if (isLeftEdge) {
- finalLeft = Math.max(safeMargin, editorLeft + safeMargin);
- Logger.debug("\u{1F4F1} \uC67C\uCABD \uAD6C\uC11D \uAC10\uC9C0: \uC5D0\uB514\uD130 \uC601\uC5ED \uB0B4 \uC624\uB978\uCABD\uC73C\uB85C \uC774\uB3D9");
- } else if (isRightEdge) {
- finalLeft = Math.min(viewportWidth - adaptiveSize.width - safeMargin, editorLeft + editorWidth - adaptiveSize.width - safeMargin);
- Logger.debug("\u{1F4F1} \uC624\uB978\uCABD \uAD6C\uC11D \uAC10\uC9C0: \uC5D0\uB514\uD130 \uC601\uC5ED \uB0B4 \uC67C\uCABD\uC73C\uB85C \uC774\uB3D9");
- } else {
- if (mousePosition) {
- finalLeft = Math.max(safeMargin, Math.min(
- referenceCenterX - adaptiveSize.width / 2,
- viewportWidth - adaptiveSize.width - safeMargin
- ));
- } else {
- const editorCenterX = editorLeft + editorWidth / 2;
- finalLeft = Math.max(safeMargin, Math.min(
- editorCenterX - adaptiveSize.width / 2,
- viewportWidth - adaptiveSize.width - safeMargin
- ));
- }
- }
- const effectiveViewportHeight = Math.min(viewportHeight - keyboardHeight, editorTop + editorHeight);
- const spaceAbove = referenceCenterY - editorTop;
- const spaceBelow = effectiveViewportHeight - referenceCenterY;
- if (isTopEdge && spaceBelow > adaptiveSize.maxHeight + fingerOffset + safeMargin) {
- finalTop = referenceCenterY + fingerOffset;
- Logger.debug(`\u{1F4F1} \uC0C1\uB2E8 \uAD6C\uC11D: \uC544\uB798\uCABD \uBC30\uCE58 (\uC624\uD504\uC14B: ${fingerOffset}px)`);
- } else if (isBottomEdge && spaceAbove > adaptiveSize.maxHeight + fingerOffset + safeMargin) {
- finalTop = referenceCenterY - adaptiveSize.maxHeight - fingerOffset;
- Logger.debug(`\u{1F4F1} \uD558\uB2E8 \uAD6C\uC11D: \uC704\uCABD \uBC30\uCE58 (\uC624\uD504\uC14B: ${fingerOffset}px)`);
- } else if (spaceAbove > adaptiveSize.maxHeight + fingerOffset + safeMargin) {
- finalTop = referenceCenterY - adaptiveSize.maxHeight - (mousePosition ? 8 : 12);
- Logger.debug(`\u{1F4F1} \uC704\uCABD \uBC30\uCE58 (\uD130\uCE58 \uCD5C\uC801\uD654)`);
- } else if (spaceBelow > adaptiveSize.maxHeight + fingerOffset + safeMargin) {
- finalTop = referenceCenterY + (mousePosition ? 8 : 12);
- Logger.debug(`\u{1F4F1} \uC544\uB798\uCABD \uBC30\uCE58 (\uD130\uCE58 \uCD5C\uC801\uD654)`);
- } else {
- const centerY = effectiveViewportHeight / 2;
- if (Math.abs(centerY - referenceCenterY) < adaptiveSize.maxHeight / 2) {
- finalTop = Math.max(editorTop + safeMargin, referenceCenterY - adaptiveSize.maxHeight - 10);
- } else {
- finalTop = Math.max(editorTop + safeMargin, centerY - adaptiveSize.maxHeight / 2);
- }
- Logger.debug("\u{1F4F1} \uACF5\uAC04 \uBD80\uC871: \uD130\uCE58 \uC9C0\uC810 \uC778\uC811 \uBC30\uCE58");
- }
- finalTop = Math.max(
- Math.max(safeMargin, editorTop),
- Math.min(finalTop, effectiveViewportHeight - adaptiveSize.maxHeight - safeMargin)
- );
- finalLeft = Math.max(safeMargin, Math.min(finalLeft, viewportWidth - adaptiveSize.width - safeMargin));
- this.tooltip.style.setProperty("--kga-left", `${finalLeft}px`);
- this.tooltip.style.setProperty("--kga-top", `${finalTop}px`);
- Logger.log(`\u{1F4F1} \uCD5C\uC885 \uBAA8\uBC14\uC77C \uD234\uD301 \uC704\uCE58: ${adaptiveSize.width}x${adaptiveSize.maxHeight} at (${finalLeft}, ${finalTop})`, {
- corners: { isLeftEdge, isRightEdge, isTopEdge, isBottomEdge },
- keyboard: { visible: keyboardHeight > 0, height: keyboardHeight },
- spaces: { above: spaceAbove, below: spaceBelow },
- editor: editorContainerRect ? `${editorWidth}x${editorHeight} at (${editorLeft}, ${editorTop})` : "none",
- adaptive: `${adaptiveSize.width}px (\uB0B4\uC6A9 \uB9DE\uCDA4)`,
- touchMode: mousePosition ? `touch (${mousePosition.x}, ${mousePosition.y})` : "element center",
- reference: `(${referenceCenterX}, ${referenceCenterY})`
- });
- }
- /**
- * 데스크톱 툴팁 위치 계산 (개선된 구석 처리)
- */
- positionTooltipDesktop(targetElement, targetRect, viewportWidth, viewportHeight, editorContainerRect = null, mousePosition) {
- var _a, _b, _c, _d, _e, _f, _g;
- if (!this.tooltip)
- return;
- const editorLeft = (editorContainerRect == null ? void 0 : editorContainerRect.left) || 0;
- const editorTop = (editorContainerRect == null ? void 0 : editorContainerRect.top) || 0;
- const editorWidth = (editorContainerRect == null ? void 0 : editorContainerRect.width) || viewportWidth;
- const editorHeight = (editorContainerRect == null ? void 0 : editorContainerRect.height) || viewportHeight;
- const adaptiveSize = this.calculateAdaptiveTooltipSize(
- viewportWidth,
- viewportHeight,
- 0,
- false,
- editorWidth,
- editorHeight
- );
- this.tooltip.classList.add("kga-dynamic-position");
- this.tooltip.style.setProperty("--kga-width", `${adaptiveSize.width}px`);
- this.tooltip.style.setProperty("--kga-max-height", `${adaptiveSize.maxHeight}px`);
- this.tooltip.style.setProperty("--kga-min-width", `${adaptiveSize.minWidth}px`);
- this.tooltip.style.setProperty("--kga-font-size", adaptiveSize.fontSize);
- const gap = 8;
- const minSpacing = 12;
- let referenceRect;
- let referenceCenterX;
- let referenceCenterY;
- if (mousePosition) {
- referenceCenterX = mousePosition.x;
- referenceCenterY = mousePosition.y;
- referenceRect = new DOMRect(
- mousePosition.x - 8,
- mousePosition.y - 10,
- 16,
- 20
- );
- Logger.debug(`\u{1F3AF} \uB9C8\uC6B0\uC2A4 \uC704\uCE58 \uAE30\uBC18 \uD234\uD301 \uBC30\uCE58: (${mousePosition.x}, ${mousePosition.y})`);
- } else {
- referenceRect = targetRect;
- referenceCenterX = targetRect.left + targetRect.width / 2;
- referenceCenterY = targetRect.top + targetRect.height / 2;
- Logger.debug(`\u{1F4CD} \uD0C0\uAC9F \uC694\uC18C \uAE30\uBC18 \uD234\uD301 \uBC30\uCE58: (${referenceCenterX}, ${referenceCenterY})`);
- }
- const cornerThreshold = mousePosition ? 60 : 100;
- const isLeftEdge = referenceCenterX - editorLeft < cornerThreshold;
- const isRightEdge = editorLeft + editorWidth - referenceCenterX < cornerThreshold;
- const isTopEdge = referenceCenterY - editorTop < cornerThreshold;
- const isBottomEdge = editorTop + editorHeight - referenceCenterY < cornerThreshold;
- let finalLeft = 0;
- let finalTop = 0;
- const smallOffset = mousePosition ? 5 : gap;
- const availableSpaceBelow = Math.min(viewportHeight, editorTop + editorHeight) - referenceCenterY;
- const availableSpaceAbove = referenceCenterY - editorTop;
- Logger.debug(`\u{1F50D} \uD558\uB2E8 \uAC10\uC9C0: isBottomEdge=${isBottomEdge}, availableSpaceBelow=${availableSpaceBelow}, \uD544\uC694\uACF5\uAC04=${adaptiveSize.maxHeight + smallOffset + minSpacing}, \uB9C8\uC6B0\uC2A4Y=${mousePosition == null ? void 0 : mousePosition.y}, \uC5D0\uB514\uD130\uD558\uB2E8=${editorTop + editorHeight}`);
- if (isBottomEdge || availableSpaceBelow < adaptiveSize.maxHeight + smallOffset + minSpacing) {
- if (mousePosition) {
- const isAIError = ((_a = this.currentError) == null ? void 0 : _a.aiStatus) === "corrected" || ((_b = this.currentError) == null ? void 0 : _b.aiStatus) === "exception" || ((_c = this.currentError) == null ? void 0 : _c.aiStatus) === "keep-original";
- const mouseOffset = isAIError ? 120 : 80;
- finalTop = mousePosition.y - mouseOffset;
- } else {
- const isAIError = ((_d = this.currentError) == null ? void 0 : _d.aiStatus) === "corrected" || ((_e = this.currentError) == null ? void 0 : _e.aiStatus) === "exception" || ((_f = this.currentError) == null ? void 0 : _f.aiStatus) === "keep-original";
- const aiOffset = isAIError ? 100 : smallOffset;
- finalTop = referenceRect.top - adaptiveSize.maxHeight - aiOffset;
- }
- Logger.debug(`\u{1F5A5}\uFE0F\uD558\uB2E8/\uACF5\uAC04\uBD80\uC871: \uBC14\uB85C \uC704\uCABD \uBC30\uCE58 (finalTop: ${finalTop}, \uB9C8\uC6B0\uC2A4: ${mousePosition ? `(${mousePosition.x}, ${mousePosition.y})` : "\uC5C6\uC74C"}, AI\uC0C1\uD0DC: ${((_g = this.currentError) == null ? void 0 : _g.aiStatus) || "none"}, \uACF5\uAC04: ${availableSpaceBelow}px)`);
- } else {
- finalTop = referenceRect.bottom + smallOffset;
- Logger.debug(`\u{1F5A5}\uFE0F \uC544\uB798\uCABD \uBC30\uCE58 (finalTop: ${finalTop}, \uCC38\uC870bottom: ${referenceRect.bottom}, \uACF5\uAC04: ${availableSpaceBelow}px)`);
- }
- if (isLeftEdge) {
- finalLeft = Math.max(referenceCenterX + 5, editorLeft);
- Logger.debug("\u{1F5A5}\uFE0F \uC67C\uCABD \uAD6C\uC11D: \uB9C8\uC6B0\uC2A4 \uC624\uB978\uCABD \uC778\uC811");
- } else if (isRightEdge) {
- finalLeft = Math.min(referenceCenterX - adaptiveSize.width - 5, editorLeft + editorWidth - adaptiveSize.width);
- Logger.debug("\u{1F5A5}\uFE0F \uC624\uB978\uCABD \uAD6C\uC11D: \uB9C8\uC6B0\uC2A4 \uC67C\uCABD \uC778\uC811");
- } else {
- finalLeft = referenceCenterX - adaptiveSize.width / 2;
- }
- finalLeft = Math.max(
- Math.max(minSpacing, editorLeft),
- Math.min(finalLeft, Math.min(viewportWidth, editorLeft + editorWidth) - adaptiveSize.width - minSpacing)
- );
- if (isBottomEdge || availableSpaceBelow < adaptiveSize.maxHeight + smallOffset + minSpacing) {
- finalTop = Math.max(50, finalTop);
- Logger.debug(`\u{1F5A5}\uFE0F \uD558\uB2E8 \uC601\uC5ED: \uB9C8\uC6B0\uC2A4 \uC6B0\uC120 \uBC30\uCE58 (finalTop: ${finalTop}, \uC6D0\uB798\uACC4\uC0B0: ${mousePosition ? mousePosition.y - 150 : "N/A"})`);
- } else {
- finalTop = Math.max(
- Math.max(minSpacing, editorTop),
- Math.min(finalTop, Math.min(viewportHeight, editorTop + editorHeight) - adaptiveSize.maxHeight - minSpacing)
- );
- }
- this.tooltip.classList.add("kga-desktop");
- this.tooltip.style.setProperty("--kga-left", `${finalLeft}px`);
- this.tooltip.style.setProperty("--kga-top", `${finalTop}px`);
- Logger.log(`\u{1F5A5}\uFE0F \uB370\uC2A4\uD06C\uD1B1 \uD234\uD301 \uC704\uCE58: ${adaptiveSize.width}x${adaptiveSize.maxHeight} at (${finalLeft}, ${finalTop})`, {
- corners: { isLeftEdge, isRightEdge, isTopEdge, isBottomEdge },
- editor: editorContainerRect ? `${editorWidth}x${editorHeight} at (${editorLeft}, ${editorTop})` : "none",
- adaptive: `${adaptiveSize.width}px (\uB0B4\uC6A9 \uB9DE\uCDA4)`,
- mouseMode: mousePosition ? `mouse (${mousePosition.x}, ${mousePosition.y})` : "element center",
- reference: `(${referenceCenterX}, ${referenceCenterY})`
- });
- }
- /**
- * 병합된 오류용 툴팁 생성
- */
- createMergedErrorTooltip(mergedError, targetElement, triggerType) {
- if (!this.tooltip || !mergedError.originalErrors)
- return;
- const isMobile = import_obsidian13.Platform.isMobile;
- const isPhone = import_obsidian13.Platform.isPhone;
- Logger.debug(`\u{1F50D} \uBCD1\uD569\uB41C \uD234\uD301 \uC0DD\uC131: ${mergedError.originalErrors.length}\uAC1C \uC6D0\uBCF8 \uC624\uB958`);
- mergedError.originalErrors.forEach((error, index) => {
- Logger.debug(` ${index + 1}. "${error.correction.original}" \u2192 [${error.correction.corrected.join(", ")}]`);
- });
- const uniqueOriginalErrors = mergedError.originalErrors.filter(
- (error, index, arr) => arr.findIndex((e) => e.correction.original === error.correction.original) === index
- );
- Logger.warn(`\u{1F527} \uC911\uBCF5\uB41C \uC6D0\uBCF8 \uC624\uB958 \uC81C\uAC70: ${mergedError.originalErrors.length}\uAC1C \u2192 ${uniqueOriginalErrors.length}\uAC1C`);
- if (mergedError.originalErrors.length !== uniqueOriginalErrors.length) {
- Logger.warn(`\u{1F527} \uC774\uAC83\uC774 \uD234\uD301\uC5D0\uC11C \uB3D9\uC77C\uD55C \uC81C\uC548\uC774 \uC5EC\uB7EC \uBC88 \uB098\uD0C0\uB098\uB294 \uC6D0\uC778\uC785\uB2C8\uB2E4!`);
- }
- const header = this.tooltip.createEl("div", { cls: "tooltip-header" });
- if (isMobile)
- header.classList.add("kga-mobile");
- if (isPhone)
- header.classList.add("kga-mobile-phone");
- const headerText = header.createEl("span", {
- text: `${uniqueOriginalErrors.length}\uAC1C \uC624\uB958 \uBCD1\uD569\uB428`,
- cls: "header-text"
- });
- const headerCloseButton = header.createEl("button", {
- text: "\u2715",
- cls: "header-close-button"
- });
- if (isMobile)
- headerCloseButton.classList.add("kga-mobile");
- if (isPhone)
- headerCloseButton.classList.add("kga-mobile-phone");
- if (isMobile) {
- headerCloseButton.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(10);
- }
- }, { passive: false });
- headerCloseButton.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.hide(true);
- }, { passive: false });
- }
- headerCloseButton.addEventListener("click", (e) => {
- e.stopPropagation();
- this.hide(true);
- });
- const scrollContainer = this.tooltip.createEl("div", { cls: "tooltip-scroll-container" });
- if (isMobile)
- scrollContainer.classList.add("kga-mobile");
- if (isPhone)
- scrollContainer.classList.add("kga-mobile-phone");
- uniqueOriginalErrors.forEach((originalError, index) => {
- const errorSection = scrollContainer.createEl("div", { cls: "error-section" });
- if (isMobile)
- errorSection.classList.add("kga-mobile");
- if (isPhone)
- errorSection.classList.add("kga-mobile-phone");
- if (index > 0)
- errorSection.classList.add("kga-bordered");
- const errorLine = errorSection.createEl("div", { cls: "error-line" });
- if (isMobile)
- errorLine.classList.add("kga-mobile");
- if (isPhone)
- errorLine.classList.add("kga-mobile-phone");
- const errorWord = errorLine.createEl("span", {
- text: originalError.correction.original,
- cls: "error-word"
- });
- if (isMobile)
- errorWord.classList.add("kga-mobile");
- if (isPhone)
- errorWord.classList.add("kga-mobile-phone");
- const arrow = errorLine.createEl("span", { text: "\u2192", cls: "kga-arrow" });
- if (isMobile)
- arrow.classList.add("kga-mobile");
- if (isPhone)
- arrow.classList.add("kga-mobile-phone");
- const suggestionsList = errorLine.createEl("div", { cls: "suggestions-list" });
- if (isMobile)
- suggestionsList.classList.add("kga-mobile");
- if (isPhone)
- suggestionsList.classList.add("kga-mobile-phone");
- const uniqueSuggestions = [...new Set(originalError.correction.corrected)];
- Logger.debug(`\u{1F527} \uBCD1\uD569\uB41C \uD234\uD301 \uC81C\uC548 \uC911\uBCF5 \uC81C\uAC70: ${originalError.correction.corrected.length}\uAC1C \u2192 ${uniqueSuggestions.length}\uAC1C`);
- uniqueSuggestions.forEach((suggestion, index2) => {
- const suggestionButton = suggestionsList.createEl("span", {
- text: suggestion,
- cls: "suggestion-button"
- });
- if (isMobile)
- suggestionButton.classList.add("kga-mobile");
- if (isPhone)
- suggestionButton.classList.add("kga-mobile-phone");
- if (isMobile) {
- suggestionButton.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(10);
- }
- }, { passive: false });
- suggestionButton.addEventListener("touchend", (e) => {
- e.preventDefault();
- Logger.log(`\u{1F4F1} \uBAA8\uBC14\uC77C \uD130\uCE58\uB85C \uC81C\uC548 \uC801\uC6A9: "${suggestion}"`);
- this.applySuggestionKeepOpen(mergedError, suggestion, targetElement);
- }, { passive: false });
- }
- suggestionButton.addEventListener("click", (e) => {
- e.stopPropagation();
- this.applySuggestionKeepOpen(mergedError, suggestion, targetElement);
- });
- });
- if (originalError.correction.help) {
- const helpContainer = errorLine.createEl("div", { cls: "help-container" });
- this.createInlineHelpIcon(originalError.correction.help, helpContainer, () => {
- let helpArea = this.tooltip.querySelector(".tooltip-help-area");
- if (!helpArea) {
- helpArea = this.tooltip.createEl("div", { cls: "tooltip-help-area" });
- if (isMobile)
- helpArea.classList.add("kga-mobile");
- if (isPhone)
- helpArea.classList.add("kga-mobile-phone");
- helpArea.textContent = originalError.correction.help;
- } else {
- const isHidden = helpArea.hasClass("kga-hidden");
- helpArea.toggleClass("kga-hidden", !isHidden);
- if (isHidden) {
- helpArea.textContent = originalError.correction.help;
- }
- }
- });
- }
- });
- const actionsContainer = this.tooltip.createEl("div", { cls: "tooltip-actions" });
- if (isMobile)
- actionsContainer.classList.add("kga-mobile");
- if (isPhone)
- actionsContainer.classList.add("kga-mobile-phone");
- const infoText = actionsContainer.createEl("span", {
- text: isMobile ? isPhone ? "\uAC1C\uBCC4 \uC218\uC815" : "\uAC1C\uBCC4 \uD074\uB9AD \uC218\uC815" : "\uAC1C\uBCC4 \uD074\uB9AD\uC73C\uB85C \uD558\uB098\uC529 \uC218\uC815",
- cls: "info-text"
- });
- if (isMobile)
- infoText.classList.add("kga-mobile");
- if (isPhone)
- infoText.classList.add("kga-mobile-phone");
- const actionButtons = actionsContainer.createEl("div", { cls: "action-buttons" });
- if (isMobile)
- actionButtons.classList.add("kga-mobile");
- if (isPhone)
- actionButtons.classList.add("kga-mobile-phone");
- const ignoreAllButton = actionButtons.createEl("button", { cls: "ignore-all-button" });
- ignoreAllButton.textContent = "\u2715";
- ignoreAllButton.title = "\uC774 \uC624\uB958\uB4E4 \uBAA8\uB450 \uBB34\uC2DC";
- if (isMobile)
- ignoreAllButton.classList.add("kga-mobile");
- if (isPhone)
- ignoreAllButton.classList.add("kga-mobile-phone");
- if (import_obsidian13.Platform.isMobile) {
- ignoreAllButton.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(15);
- }
- }, { passive: false });
- ignoreAllButton.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.ignoreError(mergedError);
- }, { passive: false });
- }
- ignoreAllButton.addEventListener("click", (e) => {
- e.stopPropagation();
- this.ignoreError(mergedError);
- });
- const applyAllButton = actionButtons.createEl("button", {
- text: "\u2713",
- cls: "apply-all-button"
- });
- applyAllButton.title = "\uBAA8\uB4E0 \uC218\uC815 \uC0AC\uD56D \uC801\uC6A9";
- if (isMobile)
- applyAllButton.classList.add("kga-mobile");
- if (isPhone)
- applyAllButton.classList.add("kga-mobile-phone");
- if (isMobile) {
- applyAllButton.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(15);
- }
- }, { passive: false });
- applyAllButton.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (mergedError.originalErrors) {
- mergedError.originalErrors.forEach((originalError) => {
- if (originalError.correction.corrected.length > 0) {
- InlineModeService.applySuggestion(originalError, originalError.correction.corrected[0]);
- }
- });
- }
- this.hide(true);
- }, { passive: false });
- }
- applyAllButton.addEventListener("click", (e) => {
- e.stopPropagation();
- if (mergedError.originalErrors) {
- mergedError.originalErrors.forEach((originalError) => {
- if (originalError.correction.corrected.length > 0) {
- InlineModeService.applySuggestion(originalError, originalError.correction.corrected[0]);
- }
- });
- }
- this.hide(true);
- });
- this.setupHoverEvents(targetElement);
- }
- /**
- * 호버 이벤트 설정 (데스크톱 전용 - 모바일에서는 수동 닫기만)
- */
- setupHoverEvents(targetElement) {
- if (import_obsidian13.Platform.isMobile) {
- Logger.debug("\u{1F4F1} \uBAA8\uBC14\uC77C: \uD638\uBC84 \uC774\uBCA4\uD2B8 \uC124\uC815 \uC0DD\uB7B5 - \uC218\uB3D9 \uB2EB\uAE30\uB9CC \uD5C8\uC6A9");
- return;
- }
- let hideTimeout;
- let isHovering = false;
- const startHideTimer = () => {
- if (hideTimeout)
- clearTimeout(hideTimeout);
- hideTimeout = setTimeout(() => {
- if (!isHovering) {
- Logger.debug("\u{1F50D} \uD234\uD301 \uC790\uB3D9 \uC228\uAE40");
- this.hide(true);
- }
- }, 2e3);
- };
- const cancelHideTimer = () => {
- if (hideTimeout) {
- clearTimeout(hideTimeout);
- hideTimeout = void 0;
- }
- };
- const setHovering = (hovering) => {
- isHovering = hovering;
- if (hovering) {
- cancelHideTimer();
- Logger.debug("\u{1F50D} \uD638\uBC84 \uC2DC\uC791 - \uD0C0\uC774\uBA38 \uCDE8\uC18C");
- } else {
- Logger.debug("\u{1F50D} \uD638\uBC84 \uC885\uB8CC - \uD0C0\uC774\uBA38 \uC2DC\uC791");
- startHideTimer();
- }
- };
- const handleMouseEvent = (e) => {
- if (!this.tooltip)
- return;
- const target = e.target;
- const isOnTarget = targetElement.contains(target) || targetElement === target;
- const isOnTooltip = this.tooltip.contains(target) || this.tooltip === target;
- setHovering(isOnTarget || isOnTooltip);
- };
- const onMouseMove = (e) => handleMouseEvent(e);
- const onMouseOver = (e) => handleMouseEvent(e);
- const onMouseClick = (e) => {
- if (!this.tooltip)
- return;
- const target = e.target;
- const isOnTarget = targetElement.contains(target) || targetElement === target;
- const isOnTooltip = this.tooltip.contains(target) || this.tooltip === target;
- if (!isOnTarget && !isOnTooltip) {
- Logger.debug("\u{1F50D} \uC678\uBD80 \uD074\uB9AD - \uC989\uC2DC \uC228\uAE40");
- this.hide(true);
- }
- };
- document.addEventListener("mousemove", onMouseMove, { passive: true });
- document.addEventListener("mouseover", onMouseOver, { passive: true });
- document.addEventListener("click", onMouseClick);
- startHideTimer();
- this.tooltip._cleanup = () => {
- document.removeEventListener("mousemove", onMouseMove);
- document.removeEventListener("mouseover", onMouseOver);
- document.removeEventListener("click", onMouseClick);
- if (hideTimeout)
- clearTimeout(hideTimeout);
- Logger.debug("\u{1F50D} \uD638\uBC84 \uC774\uBCA4\uD2B8 \uC815\uB9AC \uC644\uB8CC");
- };
- }
- /**
- * 단일 오류용 툴팁 생성 (기존 로직 유지)
- */
- createSingleErrorTooltip(error, targetElement, triggerType) {
- if (!this.tooltip)
- return;
- const latestError = InlineModeService.getErrorWithAIData(error.uniqueId);
- if (latestError) {
- error = latestError;
- Logger.debug(`\u{1F916} \uD234\uD301 \uC624\uB958 \uC815\uBCF4 \uC5C5\uB370\uC774\uD2B8: ${error.correction.original} - AI \uC0C1\uD0DC: ${error.aiStatus || "none"}`);
- } else {
- Logger.debug(`\u{1F916} \uD234\uD301 \uC0DD\uC131: ${error.correction.original} - \uAE30\uC874 AI \uC0C1\uD0DC: ${error.aiStatus || "none"}`);
- }
- const isMobile = import_obsidian13.Platform.isMobile;
- const isPhone = import_obsidian13.Platform.isPhone || window.innerWidth <= 480;
- const header = this.tooltip.createEl("div", { cls: "tooltip-header kga-single" });
- if (isMobile)
- header.classList.add("kga-mobile");
- if (isPhone)
- header.classList.add("kga-mobile-phone");
- const headerText = header.createEl("span", {
- text: "\uB9DE\uCDA4\uBC95 \uC624\uB958",
- cls: "header-text kga-single"
- });
- if (isMobile)
- headerText.classList.add("kga-mobile");
- if (isPhone)
- headerText.classList.add("kga-mobile-phone");
- const headerCloseButton = header.createEl("button", {
- text: "\u2715",
- cls: "header-close-button kga-single"
- });
- if (isMobile)
- headerCloseButton.classList.add("kga-mobile");
- if (isPhone)
- headerCloseButton.classList.add("kga-mobile-phone");
- if (isMobile) {
- headerCloseButton.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(10);
- }
- }, { passive: false });
- headerCloseButton.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.hide(true);
- }, { passive: false });
- }
- headerCloseButton.addEventListener("click", (e) => {
- e.stopPropagation();
- this.hide(true);
- });
- const mainContent = this.tooltip.createEl("div", { cls: "tooltip-main-content" });
- if (isMobile)
- mainContent.classList.add("kga-mobile");
- if (isPhone)
- mainContent.classList.add("kga-mobile-phone");
- const errorWordContainer = mainContent.createEl("div", { cls: "error-word-container" });
- const errorWord = errorWordContainer.createEl("span", {
- text: error.correction.original,
- cls: "error-word"
- });
- if (error.aiStatus) {
- errorWord.classList.add(`kga-ai-${error.aiStatus}`);
- }
- if (isMobile) {
- errorWord.classList.add("kga-mobile");
- }
- if (isPhone) {
- errorWord.classList.add("kga-mobile-phone");
- }
- if (error.morphemeInfo && this.isImportantPos(error.morphemeInfo.mainPos, error.morphemeInfo.tags)) {
- const posInfo = errorWordContainer.createEl("span", {
- text: error.morphemeInfo.mainPos,
- cls: "pos-info"
- });
- if (isMobile) {
- posInfo.classList.add("kga-mobile");
- }
- }
- const arrow = mainContent.createEl("span", { text: "\u2192", cls: "kga-arrow" });
- if (isMobile) {
- arrow.classList.add("kga-mobile");
- }
- if (isPhone) {
- arrow.classList.add("kga-mobile-phone");
- }
- const suggestionsList = mainContent.createEl("div", { cls: "suggestions-list" });
- if (isMobile) {
- suggestionsList.classList.add("kga-mobile");
- }
- if (isPhone) {
- suggestionsList.classList.add("kga-mobile-phone");
- }
- const uniqueSuggestions = [...new Set(error.correction.corrected)];
- Logger.debug(`\u{1F527} \uD234\uD301 \uC81C\uC548 \uC911\uBCF5 \uC81C\uAC70: ${error.correction.corrected.length}\uAC1C \u2192 ${uniqueSuggestions.length}\uAC1C`);
- uniqueSuggestions.forEach((suggestion, index) => {
- const suggestionButton = suggestionsList.createEl("span", {
- text: suggestion,
- cls: "suggestion-button"
- });
- if (isMobile) {
- suggestionButton.classList.add("kga-mobile");
- }
- if (isPhone) {
- suggestionButton.classList.add("kga-mobile-phone");
- }
- if (isMobile) {
- suggestionButton.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(10);
- }
- }, { passive: false });
- suggestionButton.addEventListener("touchend", (e) => {
- e.preventDefault();
- Logger.log(`\u{1F4F1} \uBAA8\uBC14\uC77C \uD130\uCE58\uB85C \uC81C\uC548 \uC801\uC6A9: "${suggestion}"`);
- this.applySuggestion(error, suggestion, targetElement);
- }, { passive: false });
- }
- suggestionButton.addEventListener("click", (e) => {
- e.stopPropagation();
- this.applySuggestion(error, suggestion, targetElement);
- });
- });
- const actionsContainer = mainContent.createEl("div", { cls: "actions-container" });
- if (isMobile) {
- actionsContainer.classList.add("kga-mobile");
- }
- if (isPhone) {
- actionsContainer.classList.add("kga-mobile-phone");
- }
- const exceptionButton = actionsContainer.createEl("button", { cls: "exception-button" });
- exceptionButton.textContent = "\u{1F4DA}";
- exceptionButton.title = "\uC608\uC678 \uB2E8\uC5B4\uB85C \uCD94\uAC00";
- if (isMobile) {
- exceptionButton.classList.add("kga-mobile");
- }
- if (isPhone) {
- exceptionButton.classList.add("kga-mobile-phone");
- }
- if (isMobile) {
- exceptionButton.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(10);
- }
- }, { passive: false });
- exceptionButton.addEventListener("touchend", async (e) => {
- e.preventDefault();
- e.stopPropagation();
- await this.addToExceptionWords(error);
- }, { passive: false });
- }
- exceptionButton.addEventListener("click", async (e) => {
- e.stopPropagation();
- await this.addToExceptionWords(error);
- });
- const ignoreButton = actionsContainer.createEl("button", { cls: "ignore-button" });
- ignoreButton.textContent = "\u274C";
- ignoreButton.title = "\uC774 \uC624\uB958 \uBB34\uC2DC (\uC77C\uC2DC\uC801)";
- if (isMobile) {
- ignoreButton.classList.add("kga-mobile");
- }
- if (isPhone) {
- ignoreButton.classList.add("kga-mobile-phone");
- }
- if (isMobile) {
- ignoreButton.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(10);
- }
- }, { passive: false });
- ignoreButton.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.ignoreError(error);
- }, { passive: false });
- }
- ignoreButton.addEventListener("click", (e) => {
- e.stopPropagation();
- this.ignoreError(error);
- });
- let helpArea = null;
- if (error.correction.help) {
- this.createInlineHelpIcon(error.correction.help, actionsContainer, () => {
- if (!helpArea) {
- helpArea = this.tooltip.createEl("div", { cls: "tooltip-help-area" });
- if (isMobile) {
- helpArea.classList.add("kga-mobile");
- }
- if (isPhone) {
- helpArea.classList.add("kga-mobile-phone");
- }
- helpArea.textContent = error.correction.help;
- } else {
- const isHidden = helpArea.hasClass("kga-hidden");
- helpArea.toggleClass("kga-hidden", !isHidden);
- }
- });
- }
- if (error.aiAnalysis) {
- const aiArea = this.tooltip.createEl("div", { cls: "tooltip-ai-area" });
- if (isMobile) {
- aiArea.classList.add("kga-mobile");
- }
- if (isPhone) {
- aiArea.classList.add("kga-mobile-phone");
- }
- const aiIcon = aiArea.createEl("span", { text: "\u{1F916}", cls: "ai-icon" });
- const reasoningText = aiArea.createEl("span", { cls: "ai-reasoning" });
- if (error.aiAnalysis.reasoning) {
- const shortReason = error.aiAnalysis.reasoning.split(".")[0] + ".";
- reasoningText.textContent = shortReason;
- } else {
- reasoningText.textContent = `\uC2E0\uB8B0\uB3C4 ${error.aiAnalysis.confidence}%`;
- }
- }
- if (triggerType === "hover") {
- this.setupHoverEvents(targetElement);
- } else {
- if (import_obsidian13.Platform.isMobile) {
- Logger.debug("\u{1F4F1} \uBAA8\uBC14\uC77C \uD234\uD301: \uC218\uB3D9 \uB2EB\uAE30 \uBAA8\uB4DC (\uB2EB\uAE30 \uBC84\uD2BC \uB610\uB294 \uC218\uC815 \uC801\uC6A9\uC73C\uB85C\uB9CC \uB2EB\uD798)");
- } else {
- setTimeout(() => {
- document.addEventListener("click", this.handleOutsideClick.bind(this), { once: true });
- }, 0);
- }
- }
- }
- /**
- * 수정 제안 적용 (클릭 후 툴팁 유지)
- */
- applySuggestionKeepOpen(mergedError, suggestion, targetElement) {
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC218\uC815 \uC81C\uC548 \uC801\uC6A9 (\uD074\uB9AD \uD6C4 \uD234\uD301 \uC720\uC9C0) - "${mergedError.correction.original}" \u2192 "${suggestion}"`);
- window.tooltipKeepOpenMode = true;
- try {
- InlineModeService.applySuggestion(mergedError, suggestion);
- Logger.log(`\u2705 \uBCD1\uD569\uB41C \uC624\uB958 \uC218\uC815 \uC801\uC6A9 \uC131\uACF5: "${mergedError.correction.original}" \u2192 "${suggestion}"`);
- } catch (error) {
- Logger.error("\u274C \uC218\uC815 \uC81C\uC548 \uC801\uC6A9 \uC911 \uC624\uB958:", error);
- }
- setTimeout(() => {
- window.tooltipKeepOpenMode = false;
- }, 200);
- Logger.debug("\uD234\uD301 \uC720\uC9C0 \uBAA8\uB4DC\uB85C \uAD50\uC815 \uC801\uC6A9 \uC644\uB8CC");
- }
- /**
- * 수정 제안 적용 (일반 모드)
- */
- applySuggestion(error, suggestion, targetElement) {
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC218\uC815 \uC81C\uC548 \uC801\uC6A9 - "${error.correction.original}" \u2192 "${suggestion}"`);
- try {
- InlineModeService.applySuggestion(error, suggestion);
- Logger.log(`\u2705 \uC77C\uBC18 \uC624\uB958 \uC218\uC815 \uC801\uC6A9 \uC131\uACF5: "${error.correction.original}" \u2192 "${suggestion}"`);
- this.hide(true);
- } catch (error2) {
- Logger.error("\u274C \uC218\uC815 \uC81C\uC548 \uC801\uC6A9 \uC911 \uC624\uB958:", error2);
- }
- }
- /**
- * 📚 예외 단어로 추가 (동일한 단어의 모든 오류 제거)
- */
- async addToExceptionWords(error) {
- const word = error.correction.original;
- try {
- if (window.InlineModeService) {
- const removedCount = await window.InlineModeService.addWordToIgnoreListAndRemoveErrors(word);
- if (removedCount > 0) {
- Logger.log(`\u{1F4DA} \uC608\uC678 \uB2E8\uC5B4 \uCD94\uAC00 \uBC0F ${removedCount}\uAC1C \uC624\uB958 \uC81C\uAC70: "${word}"`);
- new import_obsidian13.Notice(`"${word}"\uB97C \uC608\uC678 \uB2E8\uC5B4\uB85C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4. (${removedCount}\uAC1C \uC624\uB958 \uC81C\uAC70)`);
- } else {
- new import_obsidian13.Notice(`"${word}"\uB294 \uC774\uBBF8 \uC608\uC678 \uB2E8\uC5B4\uB85C \uB4F1\uB85D\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.`);
- }
- this.hide(true);
- } else {
- Logger.error("InlineModeService\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- new import_obsidian13.Notice("\uC608\uC678 \uB2E8\uC5B4 \uCD94\uAC00\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
- }
- } catch (error2) {
- Logger.error("\uC608\uC678 \uB2E8\uC5B4 \uCD94\uAC00 \uC911 \uC624\uB958:", error2);
- new import_obsidian13.Notice("\uC608\uC678 \uB2E8\uC5B4 \uCD94\uAC00\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- /**
- * ❌ 오류 일시적 무시 (해당 오류만 숨김, 예외 단어에는 추가되지 않음)
- */
- ignoreError(error) {
- try {
- Logger.log(`\u274C \uC624\uB958 \uBB34\uC2DC: "${error.correction.original}"`);
- if (window.InlineModeService) {
- window.InlineModeService.removeError(null, error.uniqueId);
- Logger.debug(`\u2705 \uC77C\uC2DC\uC801 \uBB34\uC2DC\uB85C \uC778\uD55C \uC624\uB958 \uC81C\uAC70: ${error.uniqueId}`);
- }
- this.hide(true);
- new import_obsidian13.Notice(`"${error.correction.original}" \uC624\uB958\uB97C \uBB34\uC2DC\uD588\uC2B5\uB2C8\uB2E4.`);
- } catch (err) {
- Logger.error("\uC624\uB958 \uBB34\uC2DC \uC911 \uBB38\uC81C \uBC1C\uC0DD:", err);
- new import_obsidian13.Notice("\uC624\uB958 \uBB34\uC2DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- /**
- * 바깥 클릭 처리
- */
- handleOutsideClick(event) {
- if (this.tooltip && !this.tooltip.contains(event.target)) {
- this.hide(true);
- }
- }
- /**
- * 오류 이유 생성
- */
- generateErrorReason(error) {
- const original = error.correction.original;
- const corrected = error.correction.corrected;
- if (corrected.length === 0) {
- return "\uC218\uC815 \uC81C\uC548\uC774 \uC5C6\uB294 \uC624\uB958\uC785\uB2C8\uB2E4";
- }
- if (original.includes(" ") !== corrected[0].includes(" ")) {
- return "\uB744\uC5B4\uC4F0\uAE30 \uC624\uB958";
- }
- if (Math.abs(original.length - corrected[0].length) <= 2) {
- return "\uB9DE\uCDA4\uBC95 \uC624\uB958";
- }
- if (original.length !== corrected[0].length) {
- return "\uBB38\uBC95 \uC624\uB958";
- }
- return "\uC5B8\uC5B4 \uD45C\uD604 \uAC1C\uC120";
- }
- /**
- * 도움말 아이콘 생성 (Inline 모드용) - 모바일 최적화
- */
- createInlineHelpIcon(helpText, container, onIconClick) {
- const helpIcon = container.createEl("span", { text: "?", cls: "help-icon" });
- const isMobile = import_obsidian13.Platform.isMobile;
- const isPhone = import_obsidian13.Platform.isPhone || window.innerWidth <= 480;
- if (isMobile) {
- helpIcon.classList.add("kga-mobile");
- }
- if (isPhone) {
- helpIcon.classList.add("kga-mobile-phone");
- }
- helpIcon.title = helpText;
- if (isMobile) {
- helpIcon.addEventListener("touchstart", (e) => {
- e.preventDefault();
- if ("vibrate" in navigator) {
- navigator.vibrate(10);
- }
- }, { passive: false });
- helpIcon.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- onIconClick();
- }, { passive: false });
- }
- helpIcon.addEventListener("click", (e) => {
- e.stopPropagation();
- onIconClick();
- });
- }
- /**
- * 툴팁 내용에 따른 적응형 크기 계산
- */
- calculateAdaptiveTooltipSize(viewportWidth, viewportHeight, keyboardHeight, isPhone, editorWidth, editorHeight) {
- if (!this.tooltip) {
- return { width: 250, maxHeight: 200, minWidth: 200, fontSize: "14px" };
- }
- this.tooltip.classList.add("kga-tooltip-measuring");
- const naturalWidth = this.tooltip.scrollWidth;
- const naturalHeight = this.tooltip.scrollHeight;
- Logger.debug(`\u{1F4CF} \uD234\uD301 \uC790\uC5F0 \uD06C\uAE30: ${naturalWidth}x${naturalHeight}`);
- let minWidth, maxWidth, maxHeight, fontSize;
- if (import_obsidian13.Platform.isMobile) {
- fontSize = isPhone ? "13px" : "14px";
- minWidth = isPhone ? 240 : 280;
- maxWidth = Math.min(
- isPhone ? 340 : 400,
- // 최대 너비 증가
- Math.min(viewportWidth, editorWidth) - 24
- // 더 안전한 여백
- );
- const availableHeight = Math.min(viewportHeight, editorHeight) - keyboardHeight - 80;
- maxHeight = Math.min(
- isPhone ? 240 : 280,
- // 최대 높이 증가
- Math.max(160, availableHeight * 0.65)
- // 최소 160px 보장
- );
- } else {
- fontSize = "14px";
- minWidth = 180;
- maxWidth = Math.min(500, Math.min(viewportWidth, editorWidth) - 40);
- maxHeight = Math.min(300, Math.min(viewportHeight, editorHeight) - 40);
- }
- let optimalWidth = naturalWidth + (import_obsidian13.Platform.isMobile ? isPhone ? 32 : 28 : 24);
- optimalWidth = Math.max(minWidth, optimalWidth);
- optimalWidth = Math.min(maxWidth, optimalWidth);
- this.tooltip.classList.remove("kga-tooltip-measuring");
- const result = {
- width: optimalWidth,
- maxHeight,
- minWidth: optimalWidth,
- // 계산된 너비로 고정
- fontSize
- };
- Logger.log(`\u{1F3AF} \uC801\uC751\uD615 \uD234\uD301 \uD06C\uAE30:`, {
- natural: `${naturalWidth}x${naturalHeight}`,
- calculated: `${optimalWidth}x${maxHeight}`,
- range: `${minWidth}-${maxWidth}`,
- platform: import_obsidian13.Platform.isMobile ? isPhone ? "phone" : "tablet" : "desktop"
- });
- return result;
- }
- /**
- * 배경 커서 숨기기
- */
- hideCursorInBackground() {
- const editorElements = document.querySelectorAll(".cm-editor");
- editorElements.forEach((editor) => {
- editor.classList.add("korean-tooltip-cursor-hidden");
- });
- }
- /**
- * 배경 커서 다시 보이기
- */
- showCursorInBackground() {
- const editorElements = document.querySelectorAll(".cm-editor");
- editorElements.forEach((editor) => {
- editor.classList.remove("korean-tooltip-cursor-hidden");
- });
- }
- /**
- * 에디터 포커스만 해제 (키보드는 유지) - 깜빡임 없는 대안
- */
- blurEditorOnly() {
- try {
- const obsidianApp = window.app;
- if (obsidianApp) {
- const activeView = obsidianApp.workspace.getActiveViewOfType(import_obsidian13.MarkdownView);
- if (activeView == null ? void 0 : activeView.editor) {
- const extendedEditor = activeView.editor;
- if (activeView.editor.hasFocus()) {
- Logger.log("\u{1F4F1} \uC5D0\uB514\uD130 \uD3EC\uCEE4\uC2A4\uB9CC \uD574\uC81C (\uD0A4\uBCF4\uB4DC \uC720\uC9C0)");
- activeView.editor.blur();
- const cmEditor = extendedEditor.cm;
- if (cmEditor && cmEditor.dom) {
- cmEditor.dom.blur();
- }
- }
- }
- }
- const focusedElement = document.activeElement;
- if (focusedElement && focusedElement.blur) {
- focusedElement.blur();
- Logger.log("\u{1F4F1} DOM \uD3EC\uCEE4\uC2A4 \uD574\uC81C \uC644\uB8CC (\uD0A4\uBCF4\uB4DC\uB294 \uC720\uC9C0)");
- }
- } catch (error) {
- Logger.warn("\u{1F4F1} \uC5D0\uB514\uD130 \uD3EC\uCEE4\uC2A4 \uD574\uC81C \uC911 \uC624\uB958:", error);
- }
- }
- /**
- * 모바일에서 키보드 숨기기 및 에디터 포커스 해제
- */
- hideKeyboardAndBlurEditor() {
- try {
- const obsidianApp = window.app;
- if (obsidianApp) {
- const activeView = obsidianApp.workspace.getActiveViewOfType(import_obsidian13.MarkdownView);
- if (activeView == null ? void 0 : activeView.editor) {
- const extendedEditor = activeView.editor;
- if (activeView.editor.hasFocus()) {
- Logger.log("\u{1F4F1} \uBAA8\uBC14\uC77C: \uC5D0\uB514\uD130 \uD3EC\uCEE4\uC2A4 \uD574\uC81C \uC2DC\uC791");
- activeView.editor.blur();
- const cmEditor = extendedEditor.cm;
- if (cmEditor && cmEditor.dom) {
- cmEditor.dom.blur();
- }
- }
- }
- }
- const focusedElement = document.activeElement;
- if (focusedElement && focusedElement.blur) {
- focusedElement.blur();
- Logger.log("\u{1F4F1} \uBAA8\uBC14\uC77C: DOM \uD3EC\uCEE4\uC2A4 \uD574\uC81C \uC644\uB8CC");
- }
- const cmEditors = document.querySelectorAll(".cm-editor .cm-content");
- cmEditors.forEach((editor) => {
- if (editor instanceof HTMLElement) {
- editor.blur();
- }
- });
- const hiddenInput = document.createElement("input");
- hiddenInput.className = "kga-hidden-input";
- document.body.appendChild(hiddenInput);
- setTimeout(() => {
- hiddenInput.focus();
- setTimeout(() => {
- hiddenInput.blur();
- setTimeout(() => {
- document.body.removeChild(hiddenInput);
- Logger.log("\u{1F4F1} \uBAA8\uBC14\uC77C: \uD0A4\uBCF4\uB4DC \uC228\uAE40 \uCC98\uB9AC \uC644\uB8CC (\uCD5C\uC801\uD654\uB428)");
- }, 10);
- }, 10);
- }, 50);
- Logger.debug("\u{1F4F1} \uBDF0\uD3EC\uD2B8 \uBCC0\uACBD \uD578\uB4E4\uB7EC \uBE44\uD65C\uC131\uD654 - \uD234\uD301 \uC704\uCE58 \uACE0\uC815 \uC720\uC9C0");
- } catch (error) {
- Logger.warn("\u{1F4F1} \uBAA8\uBC14\uC77C \uD0A4\uBCF4\uB4DC \uC228\uAE40 \uC911 \uC624\uB958:", error);
- }
- }
- /**
- * 중요한 품사인지 확인합니다.
- * 일반명사, 동사, 형용사 등은 숨기고 고유명사, 외국어 등만 표시합니다.
- */
- isImportantPos(mainPos, tags) {
- const importantPos = [
- "\uACE0\uC720\uBA85\uC0AC",
- // 고유명사 (인명, 지명 등)
- "\uC678\uAD6D\uC5B4",
- // 외국어
- "\uD55C\uC790",
- // 한자어
- "\uC22B\uC790",
- // 숫자
- "\uC758\uC874\uBA85\uC0AC"
- // 의존명사 (특별한 용법)
- ];
- if (importantPos.includes(mainPos)) {
- return true;
- }
- if (tags && tags.length > 0) {
- const importantTags = ["NNP", "SL", "SH", "SN", "NNB"];
- return tags.some((tag) => importantTags.includes(tag));
- }
- return false;
- }
- /**
- * 🎨 AI 상태에 따른 오류 단어 스타일 반환
- */
- getErrorWordStyle(error) {
- const aiStatus = error.aiStatus;
- switch (aiStatus) {
- case "corrected":
- return {
- color: "#10b981",
- // 녹색
- backgroundColor: "rgba(16, 185, 129, 0.1)",
- cursor: "pointer"
- };
- case "exception":
- return {
- color: "#3b82f6",
- // 파란색
- backgroundColor: "rgba(59, 130, 246, 0.1)",
- cursor: "pointer"
- };
- case "keep-original":
- return {
- color: "#f59e0b",
- // 주황색
- backgroundColor: "rgba(245, 158, 11, 0.1)",
- cursor: "pointer"
- };
- default:
- return {
- color: "var(--text-error)",
- // 빨간색
- backgroundColor: "rgba(255, 0, 0, 0.1)",
- cursor: "pointer"
- };
- }
- }
- /**
- * 🖱️ 오류 단어 클릭 이벤트 처리
- */
- handleErrorWordClick(error, targetElement) {
- const aiStatus = error.aiStatus;
- Logger.log(`\u{1F5B1}\uFE0F \uC624\uB958 \uB2E8\uC5B4 \uD074\uB9AD: "${error.correction.original}" (AI \uC0C1\uD0DC: ${aiStatus || "none"})`);
- if (import_obsidian13.Platform.isMobile && (aiStatus === "corrected" || aiStatus === "exception" || aiStatus === "keep-original")) {
- Logger.log(`\u{1F4F1} \uBAA8\uBC14\uC77C\uC5D0\uC11C AI \uBD84\uC11D \uD6C4 \uD130\uCE58 - \uD234\uD301\uB9CC \uD45C\uC2DC: "${error.correction.original}"`);
- return;
- }
- switch (aiStatus) {
- case "corrected":
- this.applyAISelectedValue(error, targetElement);
- break;
- case "exception":
- this.addToExceptionWords(error);
- break;
- case "keep-original":
- this.keepOriginalValue(error, targetElement);
- break;
- default:
- this.applyFirstSuggestion(error, targetElement);
- break;
- }
- }
- /**
- * 🟢 AI 선택값 적용
- */
- applyAISelectedValue(error, targetElement) {
- if (!error.aiSelectedValue) {
- Logger.warn("AI \uC120\uD0DD\uAC12\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- new import_obsidian13.Notice("AI \uC120\uD0DD\uAC12\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- Logger.log(`\u{1F7E2} AI \uC120\uD0DD\uAC12 \uC801\uC6A9: "${error.correction.original}" \u2192 "${error.aiSelectedValue}"`);
- try {
- InlineModeService.applySuggestion(error, error.aiSelectedValue);
- new import_obsidian13.Notice(`\u2705 AI \uAD50\uC815 \uC801\uC6A9: "${error.aiSelectedValue}"`);
- this.hide();
- } catch (error2) {
- Logger.error("AI \uC120\uD0DD\uAC12 \uC801\uC6A9 \uC2E4\uD328:", error2);
- new import_obsidian13.Notice("\u274C AI \uAD50\uC815 \uC801\uC6A9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- /**
- * 🟠 원본 유지 (변경 없음)
- */
- keepOriginalValue(error, targetElement) {
- Logger.log(`\u{1F7E0} \uC6D0\uBCF8 \uC720\uC9C0: "${error.correction.original}"`);
- new import_obsidian13.Notice(`\u{1F7E0} \uC6D0\uBCF8 \uC720\uC9C0: "${error.correction.original}"`);
- this.hide();
- }
- /**
- * 🔴 첫 번째 수정 제안 적용 (기존 동작)
- */
- applyFirstSuggestion(error, targetElement) {
- if (!error.correction.corrected || error.correction.corrected.length === 0) {
- Logger.warn("\uC218\uC815 \uC81C\uC548\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- new import_obsidian13.Notice("\uC218\uC815 \uC81C\uC548\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const firstSuggestion = error.correction.corrected[0];
- Logger.log(`\u{1F534} \uCCAB \uBC88\uC9F8 \uC218\uC815 \uC81C\uC548 \uC801\uC6A9: "${error.correction.original}" \u2192 "${firstSuggestion}"`);
- try {
- InlineModeService.applySuggestion(error, firstSuggestion);
- new import_obsidian13.Notice(`\u2705 \uC218\uC815 \uC801\uC6A9: "${firstSuggestion}"`);
- this.hide();
- } catch (error2) {
- Logger.error("\uC218\uC815 \uC81C\uC548 \uC801\uC6A9 \uC2E4\uD328:", error2);
- new import_obsidian13.Notice("\u274C \uC218\uC815 \uC801\uC6A9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
-};
-var globalInlineTooltip = new InlineTooltip();
-window.globalInlineTooltip = globalInlineTooltip;
-
-// src/services/inlineModeService.ts
-var import_obsidian15 = require("obsidian");
-var import_obsidian16 = require("obsidian");
-var import_obsidian17 = require("obsidian");
-
-// src/utils/morphemeUtils.ts
-init_logger();
-var MorphemeUtils = class {
- /**
- * 교정 배열에서 중복된 오류를 제거하고 그룹화합니다.
- *
- * @param corrections 원본 교정 배열
- * @param morphemeData 형태소 분석 결과 (선택사항)
- * @param originalText 원본 텍스트
- * @returns 중복 제거된 교정 배열
- */
- static removeDuplicateCorrections(corrections, morphemeData, originalText) {
- if (corrections.length <= 1) {
- return corrections;
- }
- Logger.debug(`\uC911\uBCF5 \uC81C\uAC70 \uC2DC\uC791: ${corrections.length}\uAC1C \uAD50\uC815`);
- if (morphemeData && originalText) {
- return this.groupCorrectionsByMorphemes(corrections, morphemeData, originalText);
- }
- return this.basicDuplicateRemoval(corrections);
- }
- /**
- * 형태소 정보를 기반으로 교정을 그룹화합니다.
- */
- static groupCorrectionsByMorphemes(corrections, morphemeData, originalText) {
- const tokenMap = /* @__PURE__ */ new Map();
- if (morphemeData.sentences) {
- morphemeData.sentences.forEach((sentence) => {
- var _a;
- (_a = sentence.tokens) == null ? void 0 : _a.forEach((token) => {
- tokenMap.set(token.text.content, token);
- });
- });
- }
- Logger.debug("\uD615\uD0DC\uC18C \uD1A0\uD070 \uB9F5:", Array.from(tokenMap.keys()));
- const groupedCorrections = [];
- const processedRanges = /* @__PURE__ */ new Set();
- corrections.forEach((correction) => {
- const correctionPositions = this.findAllPositions(originalText, correction.original);
- correctionPositions.forEach((position) => {
- const rangeKey = `${position}_${position + correction.original.length}`;
- if (!processedRanges.has(rangeKey)) {
- const overlappingCorrections = this.findOverlappingCorrections(
- corrections,
- originalText,
- position,
- correction.original.length
- );
- if (overlappingCorrections.length > 1) {
- Logger.debug(`\uC704\uCE58 ${position}\uC5D0\uC11C \uACB9\uCE58\uB294 \uAD50\uC815\uB4E4:`, overlappingCorrections.map((c) => c.original));
- const bestCorrection = this.selectBestCorrectionWithTokens(
- overlappingCorrections,
- tokenMap,
- originalText,
- position
- );
- if (bestCorrection) {
- groupedCorrections.push(bestCorrection);
- }
- } else {
- groupedCorrections.push(correction);
- }
- overlappingCorrections.forEach((oc) => {
- const ocPositions = this.findAllPositions(originalText, oc.original);
- ocPositions.forEach((pos) => {
- const key = `${pos}_${pos + oc.original.length}`;
- processedRanges.add(key);
- });
- });
- }
- });
- });
- Logger.debug(`\uD615\uD0DC\uC18C \uAE30\uBC18 \uAD50\uC815 \uADF8\uB8F9\uD654 \uC644\uB8CC: ${corrections.length}\uAC1C \u2192 ${groupedCorrections.length}\uAC1C`);
- return groupedCorrections;
- }
- /**
- * 기본 중복 제거 로직 (형태소 데이터 없을 때)
- */
- static basicDuplicateRemoval(corrections) {
- const seen = /* @__PURE__ */ new Set();
- const unique = [];
- for (const correction of corrections) {
- const key = `${correction.original}:${JSON.stringify(correction.corrected)}`;
- if (!seen.has(key)) {
- seen.add(key);
- unique.push(correction);
- }
- }
- Logger.debug(`\uAE30\uBCF8 \uC911\uBCF5 \uC81C\uAC70 \uC644\uB8CC: ${corrections.length}\uAC1C \u2192 ${unique.length}\uAC1C`);
- return unique;
- }
- /**
- * 텍스트에서 특정 문자열의 모든 위치를 찾습니다.
- */
- static findAllPositions(text, searchText) {
- const positions = [];
- let pos = text.indexOf(searchText);
- while (pos !== -1) {
- positions.push(pos);
- pos = text.indexOf(searchText, pos + 1);
- }
- return positions;
- }
- /**
- * 특정 위치에서 겹치는 교정들을 찾습니다.
- */
- static findOverlappingCorrections(corrections, originalText, position, length) {
- const overlapping = [];
- corrections.forEach((correction) => {
- const positions = this.findAllPositions(originalText, correction.original);
- positions.forEach((pos) => {
- if (pos >= position && pos < position + length || pos + correction.original.length > position && pos + correction.original.length <= position + length || pos <= position && pos + correction.original.length >= position + length) {
- if (!overlapping.find((oc) => oc.original === correction.original)) {
- overlapping.push(correction);
- }
- }
- });
- });
- return overlapping;
- }
- /**
- * 형태소 토큰 정보를 기반으로 최적의 교정을 선택합니다.
- */
- static selectBestCorrectionWithTokens(corrections, tokenMap, originalText, position) {
- var _a, _b;
- if (corrections.length === 0)
- return null;
- if (corrections.length === 1)
- return corrections[0];
- for (const correction of corrections) {
- if (tokenMap.has(correction.original)) {
- const token = tokenMap.get(correction.original);
- Logger.debug(`\uD1A0\uD070 \uAE30\uBC18 \uC120\uD0DD: "${correction.original}" (\uD488\uC0AC: ${((_b = (_a = token.morphemes) == null ? void 0 : _a[0]) == null ? void 0 : _b.tag) || "unknown"})`);
- return correction;
- }
- }
- const longest = corrections.reduce(
- (prev, current) => current.original.length > prev.original.length ? current : prev
- );
- Logger.debug(`\uAE38\uC774 \uAE30\uBC18 \uC120\uD0DD: "${longest.original}"`);
- return longest;
- }
- /**
- * 형태소 분석 결과에서 고유명사/외국어를 감지합니다.
- */
- static isProperNounFromMorphemes(text, morphemeInfo) {
- if (!morphemeInfo || !morphemeInfo.sentences)
- return false;
- for (const sentence of morphemeInfo.sentences) {
- for (const token of sentence.tokens || []) {
- if (token.text.content === text) {
- for (const morpheme of token.morphemes || []) {
- const tag = morpheme.tag;
- if (["NNP", "SL", "SH", "SN"].includes(tag)) {
- Logger.debug(`\uD615\uD0DC\uC18C \uACE0\uC720\uBA85\uC0AC \uAC10\uC9C0: "${text}" - \uD488\uC0AC: ${tag}`);
- return true;
- }
- }
- }
- }
- }
- return false;
- }
- /**
- * 형태소 분석 결과에서 품사 정보를 추출합니다.
- */
- static extractPosInfo(text, morphemeInfo) {
- if (!morphemeInfo || !morphemeInfo.sentences)
- return void 0;
- for (const sentence of morphemeInfo.sentences) {
- for (const token of sentence.tokens || []) {
- if (token.text.content === text) {
- const morphemes = token.morphemes || [];
- if (morphemes.length > 0) {
- const mainMorpheme = morphemes[0];
- return {
- mainPos: this.tagToKorean(mainMorpheme.tag),
- tags: morphemes.map((m) => m.tag),
- confidence: token.confidence || 1
- };
- }
- }
- }
- }
- return void 0;
- }
- /**
- * 형태소 태그를 한국어로 변환합니다.
- */
- static tagToKorean(tag) {
- const tagMap = {
- "NNG": "\uC77C\uBC18\uBA85\uC0AC",
- "NNP": "\uACE0\uC720\uBA85\uC0AC",
- "NNB": "\uC758\uC874\uBA85\uC0AC",
- "VV": "\uB3D9\uC0AC",
- "VA": "\uD615\uC6A9\uC0AC",
- "VX": "\uBCF4\uC870\uC6A9\uC5B8",
- "MM": "\uAD00\uD615\uC0AC",
- "MAG": "\uC77C\uBC18\uBD80\uC0AC",
- "SL": "\uC678\uAD6D\uC5B4",
- "SH": "\uD55C\uC790",
- "SN": "\uC22B\uC790"
- };
- return tagMap[tag] || tag;
- }
-};
-
-// src/utils/notificationUtils.ts
-var import_obsidian14 = require("obsidian");
-init_logger();
-var NotificationUtils = class {
- /**
- * API 분석 시작 알림을 표시합니다.
- */
- static showAnalysisStartNotice(analysisType = "spelling") {
- const messages = {
- spelling: "\u{1F50D} \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911...",
- morpheme: "\u{1F4CB} \uD615\uD0DC\uC18C \uBD84\uC11D \uC911...",
- ai: "\u{1F916} AI \uBD84\uC11D \uC911..."
- };
- const message = messages[analysisType];
- Logger.log(`\uC54C\uB9BC \uD45C\uC2DC: ${message}`);
- return new import_obsidian14.Notice(message, 0);
- }
- /**
- * API 분석 완료 알림을 표시합니다.
- */
- static showAnalysisCompleteNotice(analysisType, resultCount, duration = 3e3) {
- let message;
- switch (analysisType) {
- case "spelling":
- message = resultCount > 0 ? `\u2705 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC644\uB8CC: ${resultCount}\uAC1C \uC624\uB958 \uBC1C\uACAC` : "\u2705 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC644\uB8CC: \uC624\uB958 \uC5C6\uC74C";
- break;
- case "morpheme":
- message = `\u{1F4CB} \uD615\uD0DC\uC18C \uBD84\uC11D \uC644\uB8CC: ${resultCount}\uAC1C \uD1A0\uD070 \uBD84\uC11D`;
- break;
- case "ai":
- message = `\u{1F916} AI \uBD84\uC11D \uC644\uB8CC: ${resultCount}\uAC1C \uC81C\uC548 \uBD84\uC11D`;
- break;
- }
- Logger.log(`\uC54C\uB9BC \uD45C\uC2DC: ${message}`);
- return new import_obsidian14.Notice(message, duration);
- }
- /**
- * API 오류 알림을 표시합니다.
- */
- static showApiErrorNotice(errorType, errorMessage, duration = 5e3) {
- let message;
- switch (errorType) {
- case "api_key":
- message = "\u274C API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC5D0\uC11C Bareun.ai API \uD0A4\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.";
- break;
- case "network":
- message = "\u274C \uB124\uD2B8\uC6CC\uD06C \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694.";
- break;
- case "timeout":
- message = "\u274C API \uC694\uCCAD \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.";
- break;
- case "parse":
- message = "\u274C API \uC751\uB2F5 \uD30C\uC2F1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.";
- break;
- case "general":
- default:
- message = errorMessage ? `\u274C API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: ${errorMessage}` : "\u274C \uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
- break;
- }
- Logger.error(`\uC624\uB958 \uC54C\uB9BC \uD45C\uC2DC: ${message}`);
- return new import_obsidian14.Notice(message, duration);
- }
- /**
- * 중복 제거 결과 알림을 표시합니다.
- */
- static showDuplicateRemovalNotice(originalCount, finalCount, usedMorpheme = false, duration = 2e3) {
- const removedCount = originalCount - finalCount;
- if (removedCount <= 0) {
- return new import_obsidian14.Notice("\u2139\uFE0F \uC911\uBCF5 \uC624\uB958 \uC5C6\uC74C", duration);
- }
- let message;
- if (usedMorpheme) {
- message = `\u{1F504} \uC911\uBCF5 \uC624\uB958 ${removedCount}\uAC1C \uC81C\uAC70\uB428 (\uD615\uD0DC\uC18C \uBD84\uC11D \uD65C\uC6A9)`;
- } else {
- message = `\u{1F504} \uC911\uBCF5 \uC624\uB958 ${removedCount}\uAC1C \uC81C\uAC70\uB428`;
- }
- Logger.log(`\uC911\uBCF5 \uC81C\uAC70 \uC54C\uB9BC: ${originalCount}\uAC1C \u2192 ${finalCount}\uAC1C (\uD615\uD0DC\uC18C: ${usedMorpheme})`);
- return new import_obsidian14.Notice(message, duration);
- }
- /**
- * 캐시 사용 알림을 표시합니다.
- */
- static showCacheUsedNotice(cacheType, duration = 2e3) {
- const messages = {
- spelling: "\u26A1 \uCE90\uC2DC\uB41C \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uACB0\uACFC \uC0AC\uC6A9",
- morpheme: "\u26A1 \uCE90\uC2DC\uB41C \uD615\uD0DC\uC18C \uBD84\uC11D \uACB0\uACFC \uC0AC\uC6A9"
- };
- const message = messages[cacheType];
- Logger.debug(`\uCE90\uC2DC \uC0AC\uC6A9 \uC54C\uB9BC: ${message}`);
- return new import_obsidian14.Notice(message, duration);
- }
- /**
- * 인라인 모드 전용: 오류 적용 알림을 표시합니다.
- */
- static showInlineErrorAppliedNotice(errorText, correctedText, duration = 2e3) {
- const message = `\u2705 "${errorText}" \u2192 "${correctedText}" \uC801\uC6A9\uB428`;
- Logger.log(`\uC778\uB77C\uC778 \uC624\uB958 \uC801\uC6A9: ${message}`);
- return new import_obsidian14.Notice(message, duration);
- }
- /**
- * 인라인 모드 전용: 예외 처리 알림을 표시합니다.
- */
- static showInlineExceptionNotice(errorText, duration = 2e3) {
- const message = `\u{1F535} "${errorText}" \uC608\uC678 \uCC98\uB9AC\uB428`;
- Logger.log(`\uC778\uB77C\uC778 \uC608\uC678 \uCC98\uB9AC: ${message}`);
- return new import_obsidian14.Notice(message, duration);
- }
- /**
- * 기존 Notice를 업데이트합니다 (진행률 표시용).
- */
- static updateNoticeMessage(notice, newMessage) {
- if (notice && notice.noticeEl) {
- const messageEl = notice.noticeEl.querySelector(".notice-message");
- if (messageEl) {
- messageEl.textContent = newMessage;
- }
- }
- }
- /**
- * Notice를 안전하게 숨깁니다.
- */
- static hideNotice(notice) {
- if (notice && notice.hide) {
- try {
- notice.hide();
- } catch (error) {
- Logger.debug("Notice \uC228\uAE40 \uC2E4\uD328 (\uC774\uBBF8 \uC81C\uAC70\uB428):", error);
- }
- }
- }
-};
-
-// src/services/inlineModeService.ts
-var getExtendedWindow = () => window;
-var AITextWidget = class extends import_view.WidgetType {
- constructor(aiText, errorId, originalText) {
- super();
- this.aiText = aiText;
- this.errorId = errorId;
- this.originalText = originalText;
- }
- toDOM() {
- const span = document.createElement("span");
- span.textContent = this.aiText;
- span.className = "korean-grammar-ai-widget";
- span.setAttribute("data-error-id", this.errorId);
- span.setAttribute("data-original", this.originalText);
- span.setAttribute("data-ai-status", "corrected");
- span.setAttribute("data-ai-selected-value", this.aiText);
- span.setAttribute("role", "button");
- span.setAttribute("tabindex", "0");
- span.addEventListener("mouseenter", (e) => {
- const mockError = {
- uniqueId: this.errorId,
- correction: {
- original: this.originalText,
- corrected: [this.aiText],
- // AI가 선택한 텍스트
- help: "AI\uAC00 \uC120\uD0DD\uD55C \uC218\uC815\uC0AC\uD56D"
- // help 필드 추가
- },
- start: 0,
- end: 0,
- line: 0,
- // 필수 필드 추가
- ch: 0,
- // 필수 필드 추가
- isActive: true,
- aiAnalysis: {
- selectedValue: this.aiText,
- confidence: 90,
- // 기본 신뢰도
- reasoning: "AI\uAC00 \uC790\uB3D9\uC73C\uB85C \uC120\uD0DD\uD55C \uC218\uC815\uC0AC\uD56D\uC785\uB2C8\uB2E4.",
- isExceptionProcessed: false
- },
- aiStatus: "corrected",
- aiSelectedValue: this.aiText
- };
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip) {
- const mousePosition = { x: e.clientX, y: e.clientY };
- tooltip.show(mockError, span, "hover", mousePosition);
- }
- });
- span.addEventListener("mouseleave", () => {
- setTimeout(() => {
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip && !tooltip.isHovered) {
- tooltip.hide();
- }
- }, 500);
- });
- span.addEventListener("click", (e) => {
- if (import_obsidian15.Platform.isMobile) {
- Logger.debug("\u{1F7E2} AI Widget: \uBAA8\uBC14\uC77C\uC5D0\uC11C \uD074\uB9AD \uC774\uBCA4\uD2B8 \uBB34\uC2DC (\uD130\uCE58 \uC774\uBCA4\uD2B8 \uC0AC\uC6A9)");
- return;
- }
- e.preventDefault();
- e.stopPropagation();
- Logger.log(`\u{1F7E2} AI Widget \uD074\uB9AD: "${this.originalText}" \u2192 "${this.aiText}" (\uD655\uC815 \uC801\uC6A9)`);
- InlineModeService.applyAIWidgetToEditor(this.errorId, this.aiText, this.originalText);
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip) {
- tooltip.hide();
- }
- });
- span.addEventListener("dblclick", (e) => {
- if (import_obsidian15.Platform.isMobile) {
- Logger.debug("\u{1F7E2} AI Widget: \uBAA8\uBC14\uC77C\uC5D0\uC11C \uB354\uBE14\uD074\uB9AD \uC774\uBCA4\uD2B8 \uBB34\uC2DC");
- return;
- }
- e.preventDefault();
- e.stopPropagation();
- Logger.log(`\u{1F7E2} AI Widget \uB354\uBE14\uD074\uB9AD: "${this.originalText}" \uD3B8\uC9D1 \uBAA8\uB4DC \uC9C4\uC785`);
- this.enterEditMode(span);
- });
- Logger.debug(`\u{1F916} AI Widget \uC0DD\uC131: "${this.originalText}" \u2192 "${this.aiText}"`);
- return span;
- }
- eq(other) {
- return this.aiText === other.aiText && this.errorId === other.errorId;
- }
- /**
- * 🖥️ 편집 모드 진입 (더블클릭 시)
- */
- enterEditMode(span) {
- var _a;
- const input = document.createElement("input");
- input.type = "text";
- input.value = this.aiText;
- input.className = "korean-grammar-ai-widget-edit";
- (_a = span.parentNode) == null ? void 0 : _a.replaceChild(input, span);
- input.focus();
- input.select();
- input.addEventListener("keydown", (e) => {
- var _a2;
- if (e.key === "Enter") {
- e.preventDefault();
- const newValue = input.value.trim();
- if (newValue) {
- Logger.log(`\u{1F7E2} AI Widget \uD3B8\uC9D1 \uC644\uB8CC: "${this.originalText}" \u2192 "${newValue}"`);
- InlineModeService.applyAIWidgetToEditor(this.errorId, newValue, this.originalText);
- } else {
- Logger.log(`\u{1F7E2} AI Widget \uD3B8\uC9D1 \uCDE8\uC18C: \uBE48 \uAC12`);
- }
- } else if (e.key === "Escape") {
- e.preventDefault();
- Logger.log(`\u{1F7E2} AI Widget \uD3B8\uC9D1 \uCDE8\uC18C: Escape`);
- const newSpan = this.createSpanElement();
- (_a2 = input.parentNode) == null ? void 0 : _a2.replaceChild(newSpan, input);
- }
- });
- input.addEventListener("blur", () => {
- var _a2;
- Logger.log(`\u{1F7E2} AI Widget \uD3B8\uC9D1 \uCDE8\uC18C: blur`);
- const newSpan = this.createSpanElement();
- (_a2 = input.parentNode) == null ? void 0 : _a2.replaceChild(newSpan, input);
- });
- }
- /**
- * 🔧 span 요소 재생성 헬퍼
- */
- createSpanElement() {
- const span = document.createElement("span");
- span.textContent = this.aiText;
- span.className = "korean-grammar-ai-widget";
- return span;
- }
-};
-var addErrorDecorations = import_state.StateEffect.define({
- map: (val, change) => val
-});
-var removeErrorDecorations = import_state.StateEffect.define({
- map: (val, change) => val
-});
-var clearAllErrorDecorations = import_state.StateEffect.define({
- map: (val, change) => val
-});
-var setFocusedErrorDecoration = import_state.StateEffect.define({
- map: (val, change) => val
-});
-var setTemporarySuggestionMode = import_state.StateEffect.define({
- map: (val, change) => val
-});
-var temporarySuggestionModeField = import_state.StateField.define({
- create() {
- return false;
- },
- update(isTemporary, tr) {
- for (let effect of tr.effects) {
- if (effect.is(setTemporarySuggestionMode)) {
- return effect.value;
- }
- }
- return isTemporary;
- }
-});
-var errorDecorationField = import_state.StateField.define({
- create() {
- return import_view.Decoration.none;
- },
- update(decorations, tr) {
- decorations = decorations.map(tr.changes);
- const isTemporaryMode = tr.state.field(temporarySuggestionModeField);
- if (tr.docChanged && !isTemporaryMode) {
- const changedRanges = [];
- tr.changes.iterChanges((fromA, toA, fromB, toB) => {
- changedRanges.push({ from: fromA, to: toA });
- });
- if (changedRanges.length > 0) {
- decorations = decorations.update({
- filter: (from, to, decoration) => {
- return !changedRanges.some(
- (range) => from >= range.from && from <= range.to || to >= range.from && to <= range.to || from <= range.from && to >= range.to
- );
- }
- });
- InlineModeService.removeErrorsInRanges(changedRanges);
- Logger.debug(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uD14D\uC2A4\uD2B8 \uBCC0\uACBD\uC73C\uB85C \uC624\uB958 \uC81C\uAC70\uB428 (${changedRanges.length}\uAC1C \uBC94\uC704)`);
- }
- }
- for (let effect of tr.effects) {
- if (effect.is(addErrorDecorations)) {
- const { errors, underlineStyle, underlineColor, preserveAIColors = false } = effect.value;
- const newDecorations = errors.map((error) => {
- var _a;
- const isFocused = false;
- if (error.aiStatus === "corrected" && error.aiSelectedValue) {
- Logger.debug(`\u{1F504} Replace Decoration \uC0AC\uC6A9: "${error.correction.original}" \u2192 "${error.aiSelectedValue}"`);
- const actualText = ((_a = this.currentView) == null ? void 0 : _a.state.doc.sliceString(error.start, error.end)) || "";
- Logger.debug(`\u{1F504} Replace \uBC94\uC704 \uAC80\uC99D: \uC608\uC0C1="${error.correction.original}" (${error.correction.original.length}\uC790), \uC2E4\uC81C="${actualText}" (${actualText.length}\uC790), \uBC94\uC704=${error.start}-${error.end}`);
- return import_view.Decoration.replace({
- widget: new AITextWidget(
- error.aiSelectedValue,
- error.uniqueId,
- error.correction.original
- ),
- inclusive: false,
- block: false
- }).range(error.start, error.end);
- }
- return import_view.Decoration.mark({
- class: `korean-grammar-error-inline ${isFocused ? "korean-grammar-focused" : ""}`,
- attributes: {
- "data-error-id": error.uniqueId,
- "data-original": error.correction.original,
- "data-corrected": JSON.stringify(error.correction.corrected),
- "data-ai-status": error.aiStatus || "none",
- // 🤖 AI 상태 정보 (CSS 선택자용)
- "data-ai-selected-value": error.aiSelectedValue || "",
- // 🤖 AI가 선택한 수정 텍스트 (CSS content용)
- "role": "button",
- "tabindex": "0"
- },
- // CSS에서 오버라이드되지 않도록 inclusive 방식 사용
- inclusive: false,
- // 🔧 클래스가 아닌 attributes에 스타일 적용
- tagName: isFocused ? "mark" : "span"
- }).range(error.start, error.end);
- });
- newDecorations.sort((a, b) => a.from - b.from);
- decorations = decorations.update({
- add: newDecorations,
- sort: true
- });
- } else if (effect.is(removeErrorDecorations)) {
- const errorIds = effect.value;
- decorations = decorations.update({
- filter: (from, to, decoration) => {
- var _a;
- const errorId = (_a = decoration.spec.attributes) == null ? void 0 : _a["data-error-id"];
- return errorId ? !errorIds.includes(errorId) : true;
- }
- });
- } else if (effect.is(clearAllErrorDecorations)) {
- decorations = import_view.Decoration.none;
- } else if (effect.is(setFocusedErrorDecoration)) {
- const focusedErrorId = effect.value;
- Logger.log(`\u{1F3AF} \uD3EC\uCEE4\uC2A4 decoration \uC5C5\uB370\uC774\uD2B8: ${focusedErrorId}`);
- const activeErrorsArray = InlineModeService.getActiveErrors();
- Logger.debug(`\u{1F3AF} \uD3EC\uCEE4\uC2A4 decoration \uC5C5\uB370\uC774\uD2B8 \uC2DC\uC791: ${activeErrorsArray.length}\uAC1C \uC624\uB958, \uD0C0\uAC9F: ${focusedErrorId}`);
- if (activeErrorsArray.length > 0) {
- const newDecorations = activeErrorsArray.map((error) => {
- const isFocused = error.uniqueId === focusedErrorId;
- if (isFocused) {
- Logger.debug(`\u{1F3AF} \uD3EC\uCEE4\uC2A4 \uB9E4\uCE6D: "${error.correction.original}" (${error.uniqueId}) at ${error.start}-${error.end}`);
- }
- return import_view.Decoration.mark({
- class: `korean-grammar-error-inline ${isFocused ? "korean-grammar-focused" : ""}`,
- attributes: {
- "data-error-id": error.uniqueId,
- "data-original": error.correction.original,
- "data-corrected": JSON.stringify(error.correction.corrected),
- "data-ai-status": error.aiStatus || "none",
- // 🤖 AI 상태 정보 (CSS 선택자용)
- "role": "button",
- "tabindex": "0"
- }
- }).range(error.start, error.end);
- });
- newDecorations.sort((a, b) => a.from - b.from);
- decorations = import_view.Decoration.set(newDecorations);
- }
- }
- }
- return decorations;
- },
- provide: (field) => import_view.EditorView.decorations.from(field)
-});
-var InlineModeService = class {
- /**
- * 에디터 뷰 및 설정 초기화
- */
- static setEditorView(view, settings, app) {
- if (this.currentView && this.currentView !== view) {
- Logger.debug("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC774\uC804 \uC5D0\uB514\uD130\uBDF0\uC640 \uB2E4\uB984 - \uC0C1\uD0DC \uC815\uB9AC \uC911");
- this.clearErrors(this.currentView);
- this.activeErrors.clear();
- }
- this.currentView = view;
- if (settings) {
- this.settings = settings;
- }
- if (app) {
- this.app = app;
- }
- this.setupEventListeners(view);
- Logger.debug("\uC778\uB77C\uC778 \uBAA8\uB4DC: Command Palette \uAE30\uBC18 \uD0A4\uBCF4\uB4DC \uB2E8\uCD95\uD0A4 \uC0AC\uC6A9");
- Logger.debug("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC5D0\uB514\uD130 \uBDF0 \uC124\uC815\uB428");
- }
- /**
- * 이벤트 리스너 설정 (겹치는 오류 영역 처리 개선)
- */
- static setupEventListeners(view) {
- const editorDOM = view.dom;
- this.setupCursorMonitoring(view);
- editorDOM.addEventListener("mouseenter", (e) => {
- var _a;
- const target = e.target;
- if (target.classList.contains("korean-grammar-error-inline")) {
- const errorId = target.getAttribute("data-error-id");
- if (errorId && this.activeErrors.has(errorId)) {
- const error = this.activeErrors.get(errorId);
- if (((_a = this.currentHoveredError) == null ? void 0 : _a.uniqueId) === errorId) {
- Logger.debug(`\uC774\uBBF8 \uD638\uBC84 \uC911\uC778 \uC624\uB958: ${error.correction.original}`);
- return;
- }
- this.clearHoverTimeout();
- Logger.debug(`\uC0C8\uB85C\uC6B4 \uC624\uB958 \uD638\uBC84 \uC2DC\uC791: "${error.correction.original}" (ID: ${errorId})`);
- const mousePosition = { x: e.clientX, y: e.clientY };
- this.expandHoverAreaByMorphemes(target, error);
- this.hoverTimeout = setTimeout(() => {
- this.currentHoveredError = error;
- this.handleErrorHover(error, target, mousePosition);
- }, 300);
- }
- }
- }, true);
- editorDOM.addEventListener("mouseleave", (e) => {
- var _a;
- const target = e.target;
- if (target.classList.contains("korean-grammar-error-inline")) {
- const errorId = target.getAttribute("data-error-id");
- if (((_a = this.currentHoveredError) == null ? void 0 : _a.uniqueId) === errorId) {
- Logger.debug(`\uC624\uB958 \uD638\uBC84 \uC885\uB8CC: "${this.currentHoveredError.correction.original}" (ID: ${errorId})`);
- this.clearHoverTimeout();
- setTimeout(() => {
- var _a2;
- if (((_a2 = this.currentHoveredError) == null ? void 0 : _a2.uniqueId) === errorId) {
- this.currentHoveredError = null;
- }
- }, 150);
- }
- }
- }, true);
- editorDOM.addEventListener("click", (e) => {
- try {
- if (import_obsidian15.Platform.isMobile) {
- Logger.debug("\uBAA8\uBC14\uC77C\uC5D0\uC11C \uD074\uB9AD \uC774\uBCA4\uD2B8 \uBB34\uC2DC (\uD130\uCE58 \uC774\uBCA4\uD2B8 \uC0AC\uC6A9)");
- return;
- }
- const target = e.target;
- if (target && target.classList && target.classList.contains("korean-grammar-error-inline")) {
- e.preventDefault();
- e.stopPropagation();
- const errorId = target.getAttribute("data-error-id");
- if (errorId && this.activeErrors.has(errorId)) {
- const error = this.activeErrors.get(errorId);
- if (error) {
- const mousePosition = { x: e.clientX, y: e.clientY };
- this.handleErrorClick(error, target, mousePosition);
- }
- }
- }
- } catch (err) {
- Logger.error("\uD074\uB9AD \uC774\uBCA4\uD2B8 \uCC98\uB9AC \uC911 \uC624\uB958:", err);
- }
- }, true);
- editorDOM.addEventListener("focus", (e) => {
- const target = e.target;
- if (target.classList.contains("korean-grammar-error-inline")) {
- const errorId = target.getAttribute("data-error-id");
- if (errorId && this.activeErrors.has(errorId)) {
- const error = this.activeErrors.get(errorId);
- if (this.currentFocusedError && this.currentFocusedError.uniqueId === error.uniqueId) {
- Logger.debug(`\uC774\uBBF8 \uD3EC\uCEE4\uC2A4\uB41C \uC624\uB958 \uC2A4\uD0B5: ${error.uniqueId}`);
- return;
- }
- this.setFocusedError(error);
- }
- }
- }, true);
- Logger.debug("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC774\uBCA4\uD2B8 \uB9AC\uC2A4\uB108 \uC124\uC815\uB428 (\uC815\uD655\uD55C \uD638\uBC84 \uC694\uC18C\uB9CC \uCC98\uB9AC)");
- this.setupMobileTouchEvents(editorDOM);
- }
- /**
- * 모바일 터치 이벤트 설정
- */
- static setupMobileTouchEvents(editorDOM) {
- if (!import_obsidian15.Platform.isMobile) {
- Logger.debug("\uB370\uC2A4\uD06C\uD1B1 \uD658\uACBD: \uD130\uCE58 \uC774\uBCA4\uD2B8 \uB4F1\uB85D\uD558\uC9C0 \uC54A\uC74C");
- return;
- }
- let touchTimer = null;
- let touchTarget = null;
- let touchStartTime = 0;
- const TOUCH_HOLD_DURATION = 600;
- const MAX_TOUCH_MOVE = 10;
- let touchStartPos = { x: 0, y: 0 };
- Logger.log("\u{1F4F1} \uBAA8\uBC14\uC77C \uD130\uCE58 \uC774\uBCA4\uD2B8 \uB4F1\uB85D");
- editorDOM.addEventListener("touchstart", (e) => {
- const target = e.target;
- if (target.classList.contains("korean-grammar-error-inline")) {
- e.preventDefault();
- const touch = e.touches[0];
- touchStartPos = { x: touch.clientX, y: touch.clientY };
- touchStartTime = Date.now();
- touchTarget = target;
- const errorId = target.getAttribute("data-error-id");
- if (errorId && this.activeErrors.has(errorId)) {
- const error = this.activeErrors.get(errorId);
- touchTimer = setTimeout(() => {
- if (touchTarget === target && this.activeErrors.has(errorId)) {
- Logger.log(`\u{1F4F1} \uB871\uD504\uB808\uC2A4\uB85C \uBC14\uB85C \uC218\uC815: ${error.correction.original}`);
- if ("vibrate" in navigator) {
- navigator.vibrate(50);
- }
- if (error.correction.corrected && error.correction.corrected.length > 0) {
- const firstSuggestion = error.correction.corrected[0];
- this.applySuggestion(error, firstSuggestion);
- Logger.log(`\u{1F4F1} \uB871\uD504\uB808\uC2A4 \uC218\uC815 \uC644\uB8CC: "${error.correction.original}" \u2192 "${firstSuggestion}"`);
- }
- touchTarget = null;
- touchTimer = null;
- }
- }, TOUCH_HOLD_DURATION);
- Logger.debug(`\u{1F4F1} \uD130\uCE58 \uC2DC\uC791: ${error.correction.original}`);
- }
- }
- }, { passive: false });
- editorDOM.addEventListener("touchend", (e) => {
- const wasTouchTimer = touchTimer !== null;
- if (touchTimer) {
- clearTimeout(touchTimer);
- touchTimer = null;
- }
- const touchDuration = Date.now() - touchStartTime;
- if (touchDuration < TOUCH_HOLD_DURATION && touchTarget && wasTouchTimer) {
- const target = touchTarget;
- const errorId = target.getAttribute("data-error-id");
- if (errorId && this.activeErrors.has(errorId)) {
- const error = this.activeErrors.get(errorId);
- Logger.log(`\u{1F4F1} \uC9E7\uC740 \uD130\uCE58\uB85C \uD234\uD301 \uD45C\uC2DC (${touchDuration}ms): ${error.correction.original}`);
- e.preventDefault();
- e.stopPropagation();
- setTimeout(() => {
- const touchPosition = { x: touchStartPos.x, y: touchStartPos.y };
- this.handleErrorTooltip(error, target, touchPosition);
- }, 50);
- }
- }
- touchTarget = null;
- touchStartTime = 0;
- }, { passive: false });
- editorDOM.addEventListener("touchcancel", () => {
- if (touchTimer) {
- clearTimeout(touchTimer);
- touchTimer = null;
- Logger.debug("\u{1F4F1} \uD130\uCE58 \uCDE8\uC18C\uB428");
- }
- touchTarget = null;
- touchStartTime = 0;
- }, { passive: true });
- editorDOM.addEventListener("touchmove", (e) => {
- if (touchTimer && touchTarget) {
- const touch = e.touches[0];
- const moveDistance = Math.sqrt(
- Math.pow(touch.clientX - touchStartPos.x, 2) + Math.pow(touch.clientY - touchStartPos.y, 2)
- );
- if (moveDistance > MAX_TOUCH_MOVE) {
- clearTimeout(touchTimer);
- touchTimer = null;
- touchTarget = null;
- Logger.debug(`\u{1F4F1} \uD130\uCE58 \uC774\uB3D9\uC73C\uB85C \uB871\uD504\uB808\uC2A4 \uCDE8\uC18C (${Math.round(moveDistance)}px)`);
- }
- }
- }, { passive: true });
- Logger.log("\u{1F4F1} \uBAA8\uBC14\uC77C \uD130\uCE58 \uC774\uBCA4\uD2B8 \uC124\uC815 \uC644\uB8CC");
- Logger.log(" \u2022 \uD130\uCE58: \uD234\uD301 \uD45C\uC2DC");
- Logger.log(" \u2022 \uB871\uD504\uB808\uC2A4 (600ms): \uCCAB \uBC88\uC9F8 \uC81C\uC548\uC73C\uB85C \uBC14\uB85C \uC218\uC815");
- Logger.log(" \u2022 \uD585\uD2F1 \uD53C\uB4DC\uBC31 \uC9C0\uC6D0");
- }
- /**
- * 호버 타이머 정리
- */
- static clearHoverTimeout() {
- if (this.hoverTimeout) {
- clearTimeout(this.hoverTimeout);
- this.hoverTimeout = null;
- }
- }
- /**
- * 🎯 커서 위치 변경 모니터링 설정
- */
- static setupCursorMonitoring(view) {
- if (!this.app)
- return;
- setInterval(() => {
- this.checkCursorPosition();
- }, 500);
- Logger.debug("\u{1F3AF} \uCEE4\uC11C \uC704\uCE58 \uBAA8\uB2C8\uD130\uB9C1 \uC124\uC815 \uC644\uB8CC");
- }
- /**
- * 🎯 커서 위치 체크 (주기적 호출)
- */
- static checkCursorPosition() {
- if (!this.currentFocusedError || !this.app)
- return;
- const view = this.app.workspace.getActiveViewOfType(import_obsidian17.MarkdownView);
- if (!view)
- return;
- const editor = view.editor;
- const cursor = editor.getCursor();
- const cursorOffset = editor.posToOffset(cursor);
- if (cursorOffset < this.currentFocusedError.start || cursorOffset > this.currentFocusedError.end) {
- Logger.debug(`\u{1F3AF} \uCEE4\uC11C\uAC00 \uD3EC\uCEE4\uC2A4 \uC601\uC5ED\uC744 \uBC97\uC5B4\uB0A8: ${cursorOffset} (\uBC94\uC704: ${this.currentFocusedError.start}-${this.currentFocusedError.end})`);
- const focusedErrorId = this.currentFocusedError.uniqueId;
- this.clearFocusedError();
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip) {
- tooltip.hide();
- }
- if (this.activeErrors.has(focusedErrorId)) {
- this.activeErrors.delete(focusedErrorId);
- if (this.currentView) {
- this.currentView.dispatch({
- effects: [removeErrorDecorations.of([focusedErrorId])]
- });
- }
- Logger.debug(`\u{1F527} \uC218\uC815 \uB864\uB9C1 \uD6C4 \uC624\uB958 \uC644\uC804 \uC81C\uAC70: ${focusedErrorId}`);
- }
- }
- }
- /**
- * 🎯 현재 커서 위치에 있는 오류 찾기
- */
- static findErrorAtCursor() {
- if (!this.app)
- return null;
- const view = this.app.workspace.getActiveViewOfType(import_obsidian17.MarkdownView);
- if (!view)
- return null;
- const editor = view.editor;
- const cursor = editor.getCursor();
- const cursorOffset = editor.posToOffset(cursor);
- for (const [, error] of this.activeErrors) {
- if (cursorOffset >= error.start && cursorOffset <= error.end) {
- Logger.debug(`\u{1F3AF} \uCEE4\uC11C \uC704\uCE58\uC5D0\uC11C \uC624\uB958 \uBC1C\uACAC: "${error.correction.original}" (${error.start}-${error.end})`);
- return error;
- }
- }
- Logger.debug(`\u{1F3AF} \uCEE4\uC11C \uC704\uCE58\uC5D0 \uC624\uB958 \uC5C6\uC74C: ${cursorOffset}`);
- return null;
- }
- /**
- * 설정 업데이트
- */
- static updateSettings(settings) {
- this.settings = settings;
- Logger.debug("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC124\uC815 \uC5C5\uB370\uC774\uD2B8\uB428");
- }
- /**
- * 오류 표시 (형태소 API 통합)
- */
- static async showErrors(view, corrections, underlineStyle = "wavy", underlineColor = "var(--color-red)", app, morphemeData) {
- if (!view || !corrections.length) {
- Logger.warn("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uBDF0\uB098 \uAD50\uC815 \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const analysisNotice = NotificationUtils.showAnalysisStartNotice("spelling");
- try {
- this.clearErrors(view);
- const doc = view.state.doc;
- const fullText = doc.toString();
- let finalMorphemeData = morphemeData;
- Logger.log(`\u{1F50D} \uD615\uD0DC\uC18C \uBD84\uC11D \uC870\uAC74 \uD655\uC778: morphemeData=${!!morphemeData}, settings=${!!this.settings}`);
- if (!finalMorphemeData && this.settings) {
- try {
- NotificationUtils.updateNoticeMessage(analysisNotice, "\u{1F4CB} \uD615\uD0DC\uC18C \uBD84\uC11D \uC911...");
- Logger.log("\u{1F4CB} \uD615\uD0DC\uC18C \uBD84\uC11D \uC2DC\uC791...");
- const apiService = new SpellCheckApiService();
- finalMorphemeData = await apiService.analyzeMorphemes(fullText, this.settings);
- Logger.log(`\u{1F4CB} \uD615\uD0DC\uC18C \uBD84\uC11D \uC644\uB8CC: ${!!finalMorphemeData ? "\uC131\uACF5" : "\uC2E4\uD328"}`);
- } catch (error) {
- Logger.error("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uD615\uD0DC\uC18C \uBD84\uC11D \uC2E4\uD328, \uAE30\uBCF8 \uB85C\uC9C1 \uC0AC\uC6A9:", error);
- }
- } else {
- Logger.log(`\u{1F4CB} \uD615\uD0DC\uC18C \uBD84\uC11D \uAC74\uB108\uB6F0\uAE30: \uC774\uBBF8 \uC788\uC74C=${!!finalMorphemeData}, \uC124\uC815 \uC5C6\uC74C=${!this.settings}`);
- }
- const originalCount = corrections.length;
- const optimizedCorrections = MorphemeUtils.removeDuplicateCorrections(
- corrections,
- finalMorphemeData,
- fullText
- );
- if (originalCount > optimizedCorrections.length) {
- NotificationUtils.showDuplicateRemovalNotice(
- originalCount,
- optimizedCorrections.length,
- !!finalMorphemeData,
- 1500
- );
- }
- const beforeIgnoreCount = optimizedCorrections.length;
- const filteredCorrections = optimizedCorrections.filter((correction) => {
- const isIgnored = this.settings ? IgnoredWordsService.isWordIgnored(correction.original, this.settings) : false;
- if (isIgnored) {
- Logger.debug(`\u{1F535} \uC608\uC678\uCC98\uB9AC \uC0AC\uC804\uC73C\uB85C \uD544\uD130\uB9C1: "${correction.original}"`);
- }
- return !isIgnored;
- });
- if (beforeIgnoreCount > filteredCorrections.length) {
- const ignoredCount = beforeIgnoreCount - filteredCorrections.length;
- Logger.log(`\u{1F535} \uC608\uC678\uCC98\uB9AC \uC0AC\uC804 \uD544\uD130\uB9C1: ${ignoredCount}\uAC1C \uB2E8\uC5B4 \uC81C\uC678\uB428`);
- }
- const errors = [];
- filteredCorrections.forEach((correction, index) => {
- const searchText = correction.original;
- let searchIndex = 0;
- let occurrence = 0;
- while (true) {
- const foundIndex = fullText.indexOf(searchText, searchIndex);
- if (foundIndex === -1)
- break;
- const beforeChar = foundIndex > 0 ? fullText[foundIndex - 1] : " ";
- const afterChar = foundIndex + searchText.length < fullText.length ? fullText[foundIndex + searchText.length] : " ";
- const isWordBoundary = this.isValidWordBoundary(beforeChar, afterChar, searchText);
- if (isWordBoundary) {
- const uniqueId = `${index}_${occurrence}_${foundIndex}`;
- const lineInfo = doc.lineAt(foundIndex);
- const posInfo = finalMorphemeData ? MorphemeUtils.extractPosInfo(searchText, finalMorphemeData) : void 0;
- const error = {
- correction,
- start: foundIndex,
- end: foundIndex + searchText.length,
- line: lineInfo.number,
- ch: foundIndex - lineInfo.from,
- uniqueId,
- isActive: true,
- morphemeInfo: posInfo
- // 형태소 정보 추가
- };
- const actualText = fullText.slice(foundIndex, foundIndex + searchText.length);
- const positionMatches = actualText === searchText;
- if (!positionMatches) {
- Logger.debug(`\u{1F4CD} \uC778\uB77C\uC778 \uC704\uCE58 \uAC80\uC99D: "${searchText}" at ${foundIndex} \u2192 "${actualText}"`);
- }
- errors.push(error);
- this.activeErrors.set(uniqueId, error);
- Logger.debug(`\u{1F3AF} \uC624\uB958 \uC704\uCE58 \uC124\uC815: "${searchText}" (${uniqueId}) at ${foundIndex}-${foundIndex + searchText.length}${posInfo ? ` [${posInfo.mainPos}]` : ""} ${positionMatches ? "\u2705" : "\u274C"}`);
- Logger.debug(`activeErrors \uD604\uC7AC \uD06C\uAE30: ${this.activeErrors.size}\uAC1C`);
- occurrence++;
- }
- searchIndex = foundIndex + 1;
- }
- });
- const mergedErrors = this.mergeOverlappingErrors(errors);
- Logger.log(`\u{1F527} \uC624\uB958 \uBCD1\uD569: ${errors.length}\uAC1C \u2192 ${mergedErrors.length}\uAC1C`);
- const mergedCount = mergedErrors.filter((err) => err.originalErrors && err.originalErrors.length > 1).length;
- if (mergedCount > 0) {
- Logger.debug(`\u{1F527} \uBCD1\uD569\uB41C \uC624\uB958: ${mergedCount}\uAC1C`);
- }
- view.dispatch({
- effects: addErrorDecorations.of({ errors: mergedErrors, underlineStyle, underlineColor })
- });
- NotificationUtils.hideNotice(analysisNotice);
- NotificationUtils.showAnalysisCompleteNotice("spelling", mergedErrors.length, 2e3);
- } catch (error) {
- Logger.error("\uC778\uB77C\uC778 \uBAA8\uB4DC \uC624\uB958 \uD45C\uC2DC \uC2E4\uD328:", error);
- NotificationUtils.hideNotice(analysisNotice);
- NotificationUtils.showApiErrorNotice("general", error.message);
- }
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uCC98\uB9AC \uC644\uB8CC`);
- }
- /**
- * 겹치는 오류들을 병합하여 분절된 하이라이팅 방지
- */
- static mergeOverlappingErrors(errors) {
- var _a;
- if (errors.length <= 1)
- return errors;
- const sortedErrors = [...errors].sort((a, b) => {
- if (a.start !== b.start)
- return a.start - b.start;
- return a.end - b.end;
- });
- const merged = [];
- let current = sortedErrors[0];
- for (let i = 1; i < sortedErrors.length; i++) {
- const next = sortedErrors[i];
- const isOverlapping = current.end > next.start;
- const isAdjacent = current.end >= next.start - 1;
- if (isOverlapping || isAdjacent) {
- const doc = (_a = this.currentView) == null ? void 0 : _a.state.doc;
- const mergedStart = Math.min(current.start, next.start);
- const mergedEnd = Math.max(current.end, next.end);
- const mergedText = (doc == null ? void 0 : doc.sliceString(mergedStart, mergedEnd)) || "";
- const currentText = (doc == null ? void 0 : doc.sliceString(current.start, current.end)) || current.correction.original;
- const nextText = (doc == null ? void 0 : doc.sliceString(next.start, next.end)) || next.correction.original;
- const originalErrors = [];
- if (current.isMerged && current.originalErrors) {
- originalErrors.push(...current.originalErrors);
- } else {
- originalErrors.push(current);
- }
- if (next.isMerged && next.originalErrors) {
- originalErrors.push(...next.originalErrors);
- } else {
- originalErrors.push(next);
- }
- const mergedCorrected = [.../* @__PURE__ */ new Set([
- ...current.correction.corrected,
- ...next.correction.corrected
- ])];
- const mergedError = {
- correction: {
- original: mergedText,
- corrected: mergedCorrected,
- help: current.correction.help || next.correction.help
- },
- start: mergedStart,
- end: mergedEnd,
- line: current.line,
- ch: current.ch,
- uniqueId: `merged_${current.uniqueId}_${next.uniqueId}`,
- isActive: true,
- isMerged: true,
- originalErrors
- // 원본 오류들 보존
- };
- this.activeErrors.delete(current.uniqueId);
- this.activeErrors.delete(next.uniqueId);
- this.activeErrors.set(mergedError.uniqueId, mergedError);
- current = mergedError;
- Logger.debug(`\u{1F517} \uC624\uB958 \uBCD1\uD569: "${currentText}" (${current.start}-${current.end}) + "${nextText}" (${next.start}-${next.end}) \u2192 "${mergedText}" (${mergedStart}-${mergedEnd}), \uC6D0\uBCF8 \uC624\uB958 ${originalErrors.length}\uAC1C \uBCF4\uC874`);
- } else {
- merged.push(current);
- current = next;
- }
- }
- merged.push(current);
- return merged;
- }
- /**
- * 특정 오류 제거
- */
- static removeError(view, errorId) {
- const targetView = view || this.currentView;
- if (!targetView || !this.activeErrors.has(errorId))
- return;
- this.activeErrors.delete(errorId);
- targetView.dispatch({
- effects: removeErrorDecorations.of([errorId])
- });
- Logger.debug(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC624\uB958 \uC81C\uAC70\uB428 (${errorId})`);
- }
- /**
- * 모든 오류 제거
- */
- static clearErrors(view) {
- if (!view)
- return;
- this.activeErrors.clear();
- view.dispatch({
- effects: clearAllErrorDecorations.of(true)
- });
- Logger.debug("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uBAA8\uB4E0 \uC624\uB958 \uC81C\uAC70\uB428");
- }
- /**
- * 특정 범위의 오류들을 activeErrors에서 제거
- */
- static removeErrorsInRanges(ranges) {
- const errorsToRemove = [];
- this.activeErrors.forEach((error, errorId) => {
- const errorOverlaps = ranges.some(
- (range) => error.start >= range.from && error.start <= range.to || error.end >= range.from && error.end <= range.to || error.start <= range.from && error.end >= range.to
- );
- if (errorOverlaps) {
- errorsToRemove.push(errorId);
- }
- });
- errorsToRemove.forEach((errorId) => {
- this.activeErrors.delete(errorId);
- });
- if (errorsToRemove.length > 0) {
- Logger.debug(`\uC778\uB77C\uC778 \uBAA8\uB4DC: activeErrors\uC5D0\uC11C ${errorsToRemove.length}\uAC1C \uC624\uB958 \uC81C\uAC70\uB428`);
- }
- }
- /**
- * 텍스트 범위의 오류 제거 (사용자 편집 시)
- */
- static removeErrorsInRange(view, from, to) {
- if (!view)
- return;
- const errorsToRemove = [];
- this.activeErrors.forEach((error, errorId) => {
- if (error.start < to && error.end > from) {
- errorsToRemove.push(errorId);
- }
- });
- if (errorsToRemove.length > 0) {
- errorsToRemove.forEach((id) => this.activeErrors.delete(id));
- view.dispatch({
- effects: removeErrorDecorations.of(errorsToRemove)
- });
- Logger.debug(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uBC94\uC704 \uB0B4 ${errorsToRemove.length}\uAC1C \uC624\uB958 \uC81C\uAC70\uB428`);
- }
- }
- /**
- * 텍스트 변경 후 모든 오류 위치 재계산
- * @param changeStart 변경이 시작된 위치
- * @param originalLength 원본 텍스트 길이
- * @param lengthDiff 길이 변화량 (양수: 증가, 음수: 감소)
- */
- static updateErrorPositionsAfterChange(changeStart, originalLength, lengthDiff) {
- var _a, _b, _c, _d;
- const changeEnd = changeStart + originalLength;
- const updatedErrors = [];
- Logger.debug(`\u{1F4CD} \uC704\uCE58 \uC7AC\uACC4\uC0B0 \uC2DC\uC791: ${changeStart}-${changeEnd} \uBC94\uC704, ${lengthDiff > 0 ? "+" : ""}${lengthDiff}\uC790 \uBCC0\uD654`);
- this.activeErrors.forEach((error, errorId) => {
- if (error.start >= changeEnd) {
- const updatedError = {
- ...error,
- start: error.start + lengthDiff,
- end: error.end + lengthDiff
- };
- updatedErrors.push([errorId, updatedError]);
- Logger.debug(` \u{1F4CD} "${error.correction.original}": ${error.start}-${error.end} \u2192 ${updatedError.start}-${updatedError.end}`);
- }
- });
- updatedErrors.forEach(([errorId, updatedError]) => {
- this.activeErrors.set(errorId, updatedError);
- });
- if (this.currentView && updatedErrors.length > 0) {
- const allErrors = Array.from(this.activeErrors.values());
- this.currentView.dispatch({
- effects: [
- clearAllErrorDecorations.of(true),
- // 기존 모든 decoration 제거
- addErrorDecorations.of({
- errors: allErrors,
- underlineStyle: ((_b = (_a = this.settings) == null ? void 0 : _a.inlineMode) == null ? void 0 : _b.underlineStyle) || "wavy",
- underlineColor: ((_d = (_c = this.settings) == null ? void 0 : _c.inlineMode) == null ? void 0 : _d.underlineColor) || "var(--color-red)",
- preserveAIColors: true
- // AI 색상 보존
- })
- ]
- });
- Logger.debug(`\u{1F4CD} decoration \uC7AC\uC0DD\uC131: ${allErrors.length}\uAC1C \uC624\uB958`);
- }
- Logger.debug(`\u{1F4CD} \uC704\uCE58 \uC7AC\uACC4\uC0B0 \uC644\uB8CC: ${updatedErrors.length}\uAC1C \uC624\uB958 \uC5C5\uB370\uC774\uD2B8\uB428`);
- }
- /**
- * 오류 호버 핸들러
- */
- static handleErrorHover(error, hoveredElement, mousePosition) {
- Logger.debug(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC624\uB958 \uD638\uBC84 - ${error.correction.original}`);
- const shouldShowTooltip = this.shouldShowTooltipOnInteraction("hover");
- if (shouldShowTooltip) {
- const targetElement = hoveredElement || this.findErrorElement(error);
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (targetElement && tooltip) {
- tooltip.show(error, targetElement, "hover", mousePosition);
- }
- }
- }
- /**
- * 오류 클릭 핸들러 (AI 상태에 따른 처리)
- */
- static handleErrorClick(error, clickedElement, mousePosition) {
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC624\uB958 \uD074\uB9AD - ${error.correction.original} (AI \uC0C1\uD0DC: ${error.aiStatus || "none"})`);
- try {
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip) {
- tooltip.hide();
- }
- const aiStatus = error.aiStatus;
- switch (aiStatus) {
- case "corrected":
- if (error.aiSelectedValue) {
- this.applySuggestion(error, error.aiSelectedValue);
- Logger.log(`\u{1F7E2} AI \uC120\uD0DD\uAC12 \uC801\uC6A9: "${error.correction.original}" \u2192 "${error.aiSelectedValue}"`);
- } else {
- Logger.warn("AI \uC120\uD0DD\uAC12\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- break;
- case "exception":
- this.addWordToIgnoreListAndRemoveErrors(error.correction.original).then((removedCount) => {
- if (removedCount > 0) {
- Logger.log(`\u{1F535} \uC608\uC678 \uB2E8\uC5B4 \uCD94\uAC00: "${error.correction.original}" (${removedCount}\uAC1C \uC624\uB958 \uC81C\uAC70)`);
- }
- }).catch((err) => {
- Logger.error("\uC608\uC678 \uB2E8\uC5B4 \uCD94\uAC00 \uC2E4\uD328:", err);
- });
- break;
- case "keep-original":
- Logger.log(`\u{1F7E0} \uC6D0\uBCF8 \uC720\uC9C0: "${error.correction.original}"`);
- this.removeError(null, error.uniqueId);
- break;
- default:
- if (error.correction.corrected && error.correction.corrected.length > 0) {
- const firstSuggestion = error.correction.corrected[0];
- this.applySuggestion(error, firstSuggestion);
- Logger.log(`\u{1F534} \uCCAB \uBC88\uC9F8 \uC81C\uC548 \uC790\uB3D9 \uC801\uC6A9: "${error.correction.original}" \u2192 "${firstSuggestion}"`);
- } else {
- Logger.warn(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC218\uC815 \uC81C\uC548\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 - ${error.correction.original}`);
- }
- break;
- }
- } catch (err) {
- Logger.error("\uC624\uB958 \uD074\uB9AD \uCC98\uB9AC \uC911 \uBB38\uC81C \uBC1C\uC0DD:", err);
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip) {
- tooltip.hide();
- }
- }
- }
- /**
- * 오류 툴팁 표시 핸들러 (바로 적용하지 않고 툴팁만 표시)
- */
- static handleErrorTooltip(error, targetElement, touchPosition) {
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC624\uB958 \uD234\uD301 \uD45C\uC2DC - ${error.correction.original}`);
- try {
- const element = targetElement || this.findErrorElement(error);
- if (element) {
- globalInlineTooltip.show(error, element, "click", touchPosition);
- } else {
- Logger.warn(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uD0C0\uAC9F \uC694\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 - ${error.correction.original}`);
- }
- } catch (err) {
- Logger.error("\uC624\uB958 \uD234\uD301 \uD45C\uC2DC \uC911 \uBB38\uC81C \uBC1C\uC0DD:", err);
- }
- }
- /**
- * 현재 활성화된 오류 목록 반환 (위치 기준 정렬)
- */
- static getActiveErrors() {
- const errors = Array.from(this.activeErrors.values());
- return errors.sort((a, b) => {
- if (a.start !== b.start) {
- return a.start - b.start;
- }
- if (a.end !== b.end) {
- return a.end - b.end;
- }
- return a.uniqueId.localeCompare(b.uniqueId);
- });
- }
- /**
- * 특정 위치의 오류 찾기
- */
- static getErrorAtPosition(pos) {
- for (const error of this.activeErrors.values()) {
- if (pos >= error.start && pos <= error.end) {
- return error;
- }
- }
- return null;
- }
- /**
- * 오류에 해당하는 DOM 요소 찾기 (위치 기반 정확한 매칭)
- */
- static findErrorElement(error) {
- const exactElement = document.querySelector(`[data-error-id="${error.uniqueId}"]`);
- if (exactElement) {
- return exactElement;
- }
- const errorElements = document.querySelectorAll(".korean-grammar-error-inline");
- for (let i = 0; i < errorElements.length; i++) {
- const element = errorElements[i];
- if (element.textContent === error.correction.original) {
- Logger.warn(`\uC815\uD655\uD55C ID \uB9E4\uCE6D \uC2E4\uD328, \uD14D\uC2A4\uD2B8 \uAE30\uBC18 \uB9E4\uCE6D \uC0AC\uC6A9: ${error.correction.original}`);
- return element;
- }
- }
- Logger.warn(`\uC624\uB958 \uC694\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C: ${error.correction.original} (ID: ${error.uniqueId})`);
- return null;
- }
- /**
- * 🤖 AI Widget을 실제 에디터에 적용
- */
- static applyAIWidgetToEditor(errorId, newText, originalText) {
- if (!this.app) {
- Logger.error("\uC571 \uC778\uC2A4\uD134\uC2A4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- try {
- const view = this.app.workspace.getActiveViewOfType(import_obsidian17.MarkdownView);
- if (!view || !view.editor) {
- Logger.error("\uD65C\uC131 \uB9C8\uD06C\uB2E4\uC6B4 \uBDF0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const error = this.activeErrors.get(errorId);
- if (!error) {
- Logger.error(`\uC624\uB958 ID\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${errorId}`);
- return;
- }
- Logger.log(`\u{1F916} AI Widget \uC5D0\uB514\uD130 \uC801\uC6A9: "${originalText}" \u2192 "${newText}" (\uC704\uCE58: ${error.start}-${error.end})`);
- if (this.currentView) {
- this.currentView.dispatch({
- effects: removeErrorDecorations.of([errorId])
- });
- this.activeErrors.delete(errorId);
- Logger.debug(`\u{1F527} \uC5F0\uC18D \uD074\uB9AD \uB300\uC751: decoration\uACFC activeErrors \uB3D9\uC2DC \uC81C\uAC70 (${errorId})`);
- const remainingAIErrors = Array.from(this.activeErrors.values()).filter((e) => e.aiStatus === "corrected");
- Logger.log(`\u{1F916} \uB0A8\uC740 AI corrected \uC624\uB958\uB4E4: ${remainingAIErrors.length}\uAC1C (${remainingAIErrors.map((e) => e.correction.original).join(", ")})`);
- this.currentView.requestMeasure();
- requestAnimationFrame(() => {
- try {
- const startPos = view.editor.offsetToPos(error.start);
- const endPos = view.editor.offsetToPos(error.end);
- view.editor.replaceRange(newText, startPos, endPos);
- Logger.debug(`\u2705 AI Widget \uC801\uC6A9 \uC644\uB8CC (\uBE44\uB3D9\uAE30): "${originalText}" \u2192 "${newText}"`);
- const lengthDiff = newText.length - originalText.length;
- if (lengthDiff !== 0) {
- this.updateErrorPositionsAfterChange(error.start, originalText.length, lengthDiff);
- Logger.debug(`\u{1F4CD} \uC704\uCE58 \uC7AC\uACC4\uC0B0: ${lengthDiff > 0 ? "+" : ""}${lengthDiff}\uC790 \uBCC0\uD654, ${error.start} \uC774\uD6C4 \uC624\uB958\uB4E4 \uC5C5\uB370\uC774\uD2B8`);
- }
- if (getExtendedWindow().Notice) {
- new (getExtendedWindow()).Notice(`\u2705 "${newText}" \uC801\uC6A9 \uC644\uB8CC`);
- }
- } catch (replaceError) {
- Logger.error("\uD14D\uC2A4\uD2B8 \uAD50\uCCB4 \uC2E4\uD328:", replaceError);
- if (getExtendedWindow().Notice) {
- new (getExtendedWindow()).Notice("\u274C \uD14D\uC2A4\uD2B8 \uAD50\uCCB4\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- });
- } else {
- this.removeError(null, errorId);
- const startPos = view.editor.offsetToPos(error.start);
- const endPos = view.editor.offsetToPos(error.end);
- view.editor.replaceRange(newText, startPos, endPos);
- if (window.Notice) {
- new window.Notice(`\u2705 "${newText}" \uC801\uC6A9 \uC644\uB8CC`);
- }
- }
- } catch (err) {
- Logger.error("AI Widget \uC5D0\uB514\uD130 \uC801\uC6A9 \uC2E4\uD328:", err);
- if (window.Notice) {
- new window.Notice("\u274C \uD14D\uC2A4\uD2B8 \uC801\uC6A9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- }
- /**
- * 수정 제안 적용
- */
- static applySuggestion(error, suggestion) {
- if (!this.currentView) {
- Logger.error("\uC5D0\uB514\uD130 \uBDF0\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC74C");
- return;
- }
- try {
- if (error.isMerged && error.originalErrors) {
- this.applyIndividualSuggestion(error, suggestion);
- return;
- }
- this.applySingleSuggestion(error, suggestion);
- } catch (err) {
- Logger.error("\uC218\uC815 \uC81C\uC548 \uC801\uC6A9 \uC911 \uC624\uB958:", err);
- }
- }
- /**
- * 개별 교정 제안 적용 (병합된 오류에서 특정 부분만 교체)
- */
- static applyIndividualSuggestion(mergedError, suggestion) {
- if (!mergedError.originalErrors || !this.currentView)
- return;
- const targetError = mergedError.originalErrors.find(
- (originalError) => originalError.correction.corrected.includes(suggestion)
- );
- if (!targetError) {
- Logger.warn(`\uC77C\uCE58\uD558\uB294 \uC6D0\uBCF8 \uC624\uB958\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C: ${suggestion}`);
- return;
- }
- Logger.debug(`\u{1F3AF} \uAC1C\uBCC4 \uAD50\uC815 \uC801\uC6A9: "${targetError.correction.original}" \u2192 "${suggestion}" (${targetError.start}-${targetError.end})`);
- this.applySingleSuggestion(targetError, suggestion);
- mergedError.originalErrors = mergedError.originalErrors.filter((err) => err.uniqueId !== targetError.uniqueId);
- if (mergedError.originalErrors.length === 0) {
- this.removeError(this.currentView, mergedError.uniqueId);
- } else {
- this.updateMergedErrorAfterIndividualApplication(mergedError);
- }
- }
- /**
- * 단일 오류에 대한 교정 제안 적용
- */
- static applySingleSuggestion(error, suggestion) {
- if (!this.currentView)
- return;
- const doc = this.currentView.state.doc;
- const actualText = doc.sliceString(error.start, error.end);
- Logger.debug(`\uD14D\uC2A4\uD2B8 \uAD50\uCCB4 \uC2DC\uB3C4: \uBC94\uC704[${error.start}-${error.end}], \uC608\uC0C1="${error.correction.original}", \uC2E4\uC81C="${actualText}", \uAD50\uCCB4="${suggestion}"`);
- let fromPos = error.start;
- let toPos = error.end;
- if (actualText !== error.correction.original) {
- Logger.warn(`\uD14D\uC2A4\uD2B8 \uBD88\uC77C\uCE58 \uAC10\uC9C0, \uC7AC\uAC80\uC0C9 \uC2DC\uB3C4: "${error.correction.original}"`);
- const fullText = doc.toString();
- const searchIndex = fullText.indexOf(error.correction.original, Math.max(0, error.start - 100));
- if (searchIndex !== -1) {
- fromPos = searchIndex;
- toPos = searchIndex + error.correction.original.length;
- Logger.debug(`\uC7AC\uAC80\uC0C9 \uC131\uACF5: \uC0C8 \uBC94\uC704[${fromPos}-${toPos}]`);
- } else {
- Logger.error(`\uC7AC\uAC80\uC0C9 \uC2E4\uD328: "${error.correction.original}" \uD14D\uC2A4\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C`);
- return;
- }
- }
- const lengthDifference = suggestion.length - (toPos - fromPos);
- this.activeErrors.delete(error.uniqueId);
- if (lengthDifference !== 0) {
- this.activeErrors.forEach((otherError, errorId) => {
- if (otherError.start > toPos) {
- otherError.start += lengthDifference;
- otherError.end += lengthDifference;
- Logger.debug(`\uB2E4\uB978 \uC624\uB958 \uC704\uCE58 \uC5C5\uB370\uC774\uD2B8: ${errorId} [${otherError.start}-${otherError.end}]`);
- }
- });
- }
- this.currentView.dispatch({
- changes: {
- from: fromPos,
- to: toPos,
- insert: suggestion
- },
- effects: [removeErrorDecorations.of([error.uniqueId])]
- });
- const isKeepOpenMode = getExtendedWindow().tooltipKeepOpenMode;
- if (!isKeepOpenMode) {
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip) {
- tooltip.hide();
- }
- this.clearFocusedError();
- } else {
- Logger.debug("\uD234\uD301 \uC720\uC9C0 \uBAA8\uB4DC: \uD234\uD301 \uC228\uAE30\uAE30 \uAC74\uB108\uB700");
- }
- if (this.currentView) {
- this.currentView.dispatch({
- effects: [setTemporarySuggestionMode.of(false)]
- });
- Logger.debug("\u{1F3AF} \uCD5C\uC885 \uC801\uC6A9: \uC784\uC2DC \uC81C\uC548 \uBAA8\uB4DC \uD574\uC81C");
- }
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uAD50\uC815 \uC644\uB8CC - "${error.correction.original}" \u2192 "${suggestion}"`);
- }
- /**
- * 개별 적용 후 병합된 오류 업데이트
- */
- static updateMergedErrorAfterIndividualApplication(mergedError) {
- if (!mergedError.originalErrors || !this.currentView)
- return;
- const remainingErrors = mergedError.originalErrors;
- const newStart = Math.min(...remainingErrors.map((err) => err.start));
- const newEnd = Math.max(...remainingErrors.map((err) => err.end));
- const doc = this.currentView.state.doc;
- const newText = doc.sliceString(newStart, newEnd);
- const remainingCorrected = [...new Set(
- remainingErrors.flatMap((err) => err.correction.corrected)
- )];
- mergedError.start = newStart;
- mergedError.end = newEnd;
- mergedError.correction.original = newText;
- mergedError.correction.corrected = remainingCorrected;
- this.activeErrors.set(mergedError.uniqueId, mergedError);
- const mergedErrors = [mergedError];
- Logger.debug(`\u{1F527} \uBCD1\uD569\uB41C \uC624\uB958 decoration \uC5C5\uB370\uC774\uD2B8: "${mergedError.correction.original}"`);
- const isDarkMode = document.body.classList.contains("theme-dark");
- const debugColor = isDarkMode ? "#fb464c" : "#e93147";
- Logger.debug(`\u{1F3A8} \uB2E4\uD06C\uBAA8\uB4DC \uAC10\uC9C0: ${isDarkMode}, \uC801\uC6A9 \uC0C9\uC0C1: ${debugColor}`);
- this.currentView.dispatch({
- effects: [
- removeErrorDecorations.of([mergedError.uniqueId]),
- // 기존 제거
- addErrorDecorations.of({
- errors: mergedErrors,
- underlineStyle: "wavy",
- underlineColor: isDarkMode ? debugColor : "var(--color-red)"
- // 다크모드에서만 강제 색상
- })
- ]
- });
- if (window.globalInlineTooltip && window.globalInlineTooltip.visible) {
- setTimeout(() => {
- const errorElement = this.findErrorElement(mergedError);
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (errorElement && tooltip) {
- tooltip.hide();
- setTimeout(() => {
- const tooltip2 = getExtendedWindow().globalInlineTooltip;
- if (tooltip2) {
- tooltip2.show(mergedError, errorElement, "click");
- }
- }, 50);
- }
- }, 100);
- }
- Logger.debug(`\u{1F504} \uBCD1\uD569\uB41C \uC624\uB958 \uC5C5\uB370\uC774\uD2B8: ${remainingErrors.length}\uAC1C \uC624\uB958 \uB0A8\uC74C, \uC0C8 \uBC94\uC704[${newStart}-${newEnd}], \uD234\uD301 \uC7AC\uD45C\uC2DC \uC608\uC57D`);
- }
- /**
- * 🔧 레거시 메서드: 키보드 스코프 초기화 (Command Palette 방식으로 대체됨)
- */
- static initializeKeyboardScope() {
- Logger.log("\u{1F527} \uB808\uAC70\uC2DC: initializeKeyboardScope \uD638\uCD9C\uB428 - Command Palette \uBC29\uC2DD\uC73C\uB85C \uBCC0\uACBD\uB428");
- Logger.log('\u{1F4A1} \uC0AC\uC6A9\uBC95: Command Palette (Cmd+P)\uC5D0\uC11C "Korean Grammar Assistant" \uAC80\uC0C9');
- Logger.log("\u{1F4A1} \uB610\uB294 Settings > Hotkeys\uC5D0\uC11C \uC9C1\uC811 \uB2E8\uCD95\uD0A4 \uC124\uC815");
- return;
- }
- /**
- * 포커스된 오류 설정
- */
- static setFocusedError(error) {
- if (this.currentFocusedError) {
- this.removeFocusHighlight(this.currentFocusedError);
- }
- this.currentFocusedError = error;
- this.currentSuggestionIndex = 0;
- this.highlightFocusedError(error);
- this.moveEditorCursorToError(error);
- Logger.debug(`\uC624\uB958 \uD3EC\uCEE4\uC2A4 \uC124\uC815: ${error.correction.original}`);
- }
- /**
- * 포커스된 오류 해제
- */
- static clearFocusedError() {
- if (this.currentFocusedError) {
- this.removeFocusHighlight(this.currentFocusedError);
- }
- this.currentFocusedError = null;
- this.currentSuggestionIndex = 0;
- if (window.globalInlineTooltip) {
- window.globalInlineTooltip.hide();
- }
- Logger.debug("\uC624\uB958 \uD3EC\uCEE4\uC2A4 \uD574\uC81C");
- }
- /**
- * 포커스된 오류에 대한 툴팁 표시
- */
- static showTooltipForFocusedError() {
- if (!this.currentFocusedError)
- return;
- const elements = document.querySelectorAll(`[data-error-id="${this.currentFocusedError.uniqueId}"]`);
- if (elements.length > 0) {
- const targetElement = elements[0];
- globalInlineTooltip.show(this.currentFocusedError, targetElement, "click");
- Logger.debug(`\uD3EC\uCEE4\uC2A4\uB41C \uC624\uB958\uC5D0 \uD234\uD301 \uD45C\uC2DC: ${this.currentFocusedError.correction.original}`);
- }
- }
- /**
- * 포커스된 오류 하이라이트 (CodeMirror 6 decoration 사용)
- */
- static highlightFocusedError(error) {
- if (!this.currentView) {
- Logger.warn("\uC5D0\uB514\uD130 \uBDF0\uAC00 \uC5C6\uC5B4 \uD3EC\uCEE4\uC2A4 \uD558\uC774\uB77C\uC774\uD2B8 \uC2E4\uD328");
- return;
- }
- Logger.log(`\u{1F3AF} CodeMirror decoration\uC73C\uB85C \uD3EC\uCEE4\uC2A4 \uD558\uC774\uB77C\uC774\uD2B8: "${error.correction.original}" (${error.uniqueId}) at ${error.start}-${error.end}`);
- const allErrors = this.getActiveErrors();
- Logger.debug(`\u{1F3AF} \uD604\uC7AC \uD65C\uC131 \uC624\uB958\uB4E4: ${allErrors.map((e) => `"${e.correction.original}"(${e.uniqueId})[${e.start}-${e.end}]`).join(", ")}`);
- this.currentView.dispatch({
- effects: [setFocusedErrorDecoration.of(error.uniqueId)]
- });
- Logger.log(`\u{1F3AF} \uD3EC\uCEE4\uC2A4 decoration dispatch \uC644\uB8CC: ${error.uniqueId}`);
- }
- /**
- * 에디터 커서를 오류 위치로 이동
- */
- static moveEditorCursorToError(error) {
- if (!this.currentView)
- return;
- try {
- const cursorPos = error.start;
- this.currentView.dispatch({
- selection: { anchor: cursorPos, head: cursorPos },
- scrollIntoView: true
- });
- Logger.debug(`\uCEE4\uC11C \uC774\uB3D9: ${error.correction.original} (\uC704\uCE58: ${error.start})`);
- } catch (e) {
- Logger.warn("\uCEE4\uC11C \uC774\uB3D9 \uC2E4\uD328:", e);
- }
- }
- /**
- * 포커스 하이라이트 제거 (CodeMirror 6 decoration 사용)
- */
- static removeFocusHighlight(error) {
- if (!this.currentView) {
- Logger.warn("\uC5D0\uB514\uD130 \uBDF0\uAC00 \uC5C6\uC5B4 \uD3EC\uCEE4\uC2A4 \uD558\uC774\uB77C\uC774\uD2B8 \uC81C\uAC70 \uC2E4\uD328");
- return;
- }
- Logger.debug(`\u{1F504} \uD3EC\uCEE4\uC2A4 decoration \uC81C\uAC70: "${error.correction.original}"`);
- this.currentView.dispatch({
- effects: [setFocusedErrorDecoration.of(null)]
- });
- Logger.debug(`\u{1F504} \uD3EC\uCEE4\uC2A4 decoration \uC81C\uAC70 \uC644\uB8CC`);
- }
- /**
- * 단어 경계 유효성 검사
- */
- static isValidWordBoundary(beforeChar, afterChar, searchText) {
- const punctuation = /[\s.,;:!?'"()[\]{}<>]/;
- return true;
- }
- /**
- * 툴팁의 수정 제안 하이라이트 업데이트 - 제거됨 (사용자 요청)
- */
- static updateTooltipHighlight() {
- return;
- }
- /**
- * 서비스 정리 (메모리 누수 방지)
- */
- static cleanup() {
- var _a;
- this.activeErrors.clear();
- this.currentView = null;
- this.settings = null;
- this.currentFocusedError = null;
- this.currentSuggestionIndex = 0;
- this.currentHoveredError = null;
- this.clearHoverTimeout();
- if ((_a = getExtendedWindow().globalInlineTooltip) == null ? void 0 : _a.visible) {
- window.globalInlineTooltip.hide();
- }
- Logger.debug("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC11C\uBE44\uC2A4 \uC815\uB9AC\uB428 (\uACB9\uCE58\uB294 \uC601\uC5ED \uCC98\uB9AC \uD3EC\uD568)");
- }
- /**
- * 인라인 모드 명령어 등록 (Command Palette 방식)
- */
- static registerCommands(plugin) {
- plugin.addCommand({
- id: "inline-next-error",
- name: "\uB2E4\uC74C \uBB38\uBC95 \uC624\uB958\uB85C \uC774\uB3D9",
- callback: () => {
- if (this.activeErrors.size === 0) {
- new import_obsidian16.Notice("\uD604\uC7AC \uAC10\uC9C0\uB41C \uBB38\uBC95 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uC778\uB77C\uC778 \uBAA8\uB4DC\uB97C \uD65C\uC131\uD654\uD558\uACE0 \uBB38\uBC95 \uAC80\uC0AC\uB97C \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
- return;
- }
- const nextError = this.findNextErrorFromCursor();
- if (nextError) {
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip) {
- tooltip.hide();
- }
- this.moveToError(nextError);
- this.setFocusedError(nextError);
- Logger.log(`\u2705 \uB2E4\uC74C \uC624\uB958\uB85C \uC774\uB3D9: ${nextError.correction.original}`);
- } else {
- new import_obsidian16.Notice("\uB2E4\uC74C \uC624\uB958\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- }
- });
- plugin.addCommand({
- id: "inline-previous-error",
- name: "\uC774\uC804 \uBB38\uBC95 \uC624\uB958\uB85C \uC774\uB3D9",
- callback: () => {
- if (this.activeErrors.size === 0) {
- new import_obsidian16.Notice("\uD604\uC7AC \uAC10\uC9C0\uB41C \uBB38\uBC95 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uC778\uB77C\uC778 \uBAA8\uB4DC\uB97C \uD65C\uC131\uD654\uD558\uACE0 \uBB38\uBC95 \uAC80\uC0AC\uB97C \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
- return;
- }
- const previousError = this.findPreviousErrorFromCursor();
- if (previousError) {
- const tooltip = getExtendedWindow().globalInlineTooltip;
- if (tooltip) {
- tooltip.hide();
- }
- this.moveToError(previousError);
- this.setFocusedError(previousError);
- Logger.log(`\u2705 \uC774\uC804 \uC624\uB958\uB85C \uC774\uB3D9: ${previousError.correction.original}`);
- } else {
- new import_obsidian16.Notice("\uC774\uC804 \uC624\uB958\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- }
- });
- plugin.addCommand({
- id: "inline-next-suggestion",
- name: "\uB2E4\uC74C \uC81C\uC548 \uC120\uD0DD",
- callback: () => {
- if (!this.currentFocusedError) {
- const errorAtCursor = this.findErrorAtCursor();
- if (errorAtCursor) {
- this.setFocusedError(errorAtCursor);
- Logger.log(`\u{1F3AF} \uCEE4\uC11C \uC704\uCE58\uC5D0\uC11C \uC790\uB3D9 \uD3EC\uCEE4\uC2A4: ${errorAtCursor.correction.original}`);
- } else {
- new import_obsidian16.Notice("\uD604\uC7AC \uD3EC\uCEE4\uC2A4\uB41C \uBB38\uBC95 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uCEE4\uC11C\uB97C \uC624\uB958 \uB2E8\uC5B4\uC5D0 \uC704\uCE58\uC2DC\uD0A4\uAC70\uB098 \uBA3C\uC800 \uC624\uB958\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694.");
- return;
- }
- }
- if (!this.currentFocusedError || !this.currentFocusedError.correction) {
- new import_obsidian16.Notice("\uD604\uC7AC \uC624\uB958\uC5D0 \uB300\uD55C \uC81C\uC548\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const suggestions = [this.currentFocusedError.correction.original, ...this.currentFocusedError.correction.corrected];
- if (!suggestions || suggestions.length === 0) {
- new import_obsidian16.Notice("\uD604\uC7AC \uC624\uB958\uC5D0 \uB300\uD55C \uC81C\uC548\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- this.currentSuggestionIndex = (this.currentSuggestionIndex + 1) % suggestions.length;
- this.applyCurrentSuggestionTemporarily();
- Logger.log(`\u2705 \uB2E4\uC74C \uC81C\uC548 \uC801\uC6A9: ${suggestions[this.currentSuggestionIndex]} (${this.currentSuggestionIndex + 1}/${suggestions.length})`);
- }
- });
- plugin.addCommand({
- id: "inline-previous-suggestion",
- name: "\uC774\uC804 \uC81C\uC548 \uC120\uD0DD",
- callback: () => {
- if (!this.currentFocusedError) {
- const errorAtCursor = this.findErrorAtCursor();
- if (errorAtCursor) {
- this.setFocusedError(errorAtCursor);
- Logger.log(`\u{1F3AF} \uCEE4\uC11C \uC704\uCE58\uC5D0\uC11C \uC790\uB3D9 \uD3EC\uCEE4\uC2A4: ${errorAtCursor.correction.original}`);
- } else {
- new import_obsidian16.Notice("\uD604\uC7AC \uD3EC\uCEE4\uC2A4\uB41C \uBB38\uBC95 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uCEE4\uC11C\uB97C \uC624\uB958 \uB2E8\uC5B4\uC5D0 \uC704\uCE58\uC2DC\uD0A4\uAC70\uB098 \uBA3C\uC800 \uC624\uB958\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694.");
- return;
- }
- }
- if (!this.currentFocusedError || !this.currentFocusedError.correction) {
- new import_obsidian16.Notice("\uD604\uC7AC \uC624\uB958\uC5D0 \uB300\uD55C \uC81C\uC548\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const suggestions = [this.currentFocusedError.correction.original, ...this.currentFocusedError.correction.corrected];
- if (!suggestions || suggestions.length === 0) {
- new import_obsidian16.Notice("\uD604\uC7AC \uC624\uB958\uC5D0 \uB300\uD55C \uC81C\uC548\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- this.currentSuggestionIndex = (this.currentSuggestionIndex - 1 + suggestions.length) % suggestions.length;
- this.applyCurrentSuggestionTemporarily();
- Logger.log(`\u2705 \uC774\uC804 \uC81C\uC548 \uC801\uC6A9: ${suggestions[this.currentSuggestionIndex]} (${this.currentSuggestionIndex + 1}/${suggestions.length})`);
- }
- });
- plugin.addCommand({
- id: "inline-apply-suggestion",
- name: "\uC120\uD0DD\uB41C \uC81C\uC548 \uC801\uC6A9",
- callback: () => {
- if (!this.currentFocusedError || !this.currentView || !this.currentFocusedError.correction) {
- new import_obsidian16.Notice("\uD604\uC7AC \uD3EC\uCEE4\uC2A4\uB41C \uBB38\uBC95 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uC624\uB958\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694.");
- return;
- }
- const suggestions = [this.currentFocusedError.correction.original, ...this.currentFocusedError.correction.corrected];
- if (!suggestions || suggestions.length === 0) {
- new import_obsidian16.Notice("\uD604\uC7AC \uC624\uB958\uC5D0 \uB300\uD55C \uC81C\uC548\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const selectedSuggestion = suggestions[this.currentSuggestionIndex];
- const originalText = this.currentFocusedError.correction.original;
- this.applySuggestion(this.currentFocusedError, selectedSuggestion);
- this.clearFocusedError();
- new import_obsidian16.Notice(`\uC81C\uC548 \uC801\uC6A9: "${originalText}" \u2192 "${selectedSuggestion}"`);
- Logger.log(`\u2705 \uC81C\uC548 \uC801\uC6A9: "${originalText}" \u2192 "${selectedSuggestion}"`);
- }
- });
- plugin.addCommand({
- id: "inline-unfocus",
- name: "\uBB38\uBC95 \uC624\uB958 \uD3EC\uCEE4\uC2A4 \uD574\uC81C",
- callback: () => {
- if (!this.currentFocusedError || !this.currentView) {
- new import_obsidian16.Notice("\uD604\uC7AC \uD3EC\uCEE4\uC2A4\uB41C \uBB38\uBC95 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- this.clearFocusedError();
- new import_obsidian16.Notice("\uBB38\uBC95 \uC624\uB958 \uD3EC\uCEE4\uC2A4\uB97C \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4.");
- Logger.log("\u2705 \uD0A4\uBCF4\uB4DC \uB124\uBE44\uAC8C\uC774\uC158 \uD574\uC81C");
- }
- });
- plugin.addCommand({
- id: "toggle-inline-mode",
- name: "\uD55C\uAD6D\uC5B4 \uBB38\uBC95 \uC778\uB77C\uC778 \uBAA8\uB4DC \uD1A0\uAE00",
- callback: () => {
- var _a, _b, _c;
- const currentState = ((_b = (_a = plugin.settings) == null ? void 0 : _a.inlineMode) == null ? void 0 : _b.enabled) || false;
- if ((_c = plugin.settings) == null ? void 0 : _c.inlineMode) {
- plugin.settings.inlineMode.enabled = !currentState;
- plugin.saveSettings();
- if (plugin.settings.inlineMode.enabled) {
- plugin.enableInlineMode();
- Logger.log("\u2705 \uC778\uB77C\uC778 \uBAA8\uB4DC \uD65C\uC131\uD654");
- } else {
- plugin.disableInlineMode();
- Logger.log("\u2705 \uC778\uB77C\uC778 \uBAA8\uB4DC \uBE44\uD65C\uC131\uD654");
- }
- }
- }
- });
- }
- /**
- * 🎯 통합 툴팁 표시 판단: 플랫폼과 설정에 따른 스마트 결정
- */
- static shouldShowTooltipOnInteraction(interactionType) {
- var _a;
- if (!((_a = this.settings) == null ? void 0 : _a.inlineMode))
- return false;
- const { tooltipTrigger } = this.settings.inlineMode;
- if (!tooltipTrigger) {
- return interactionType === "hover" ? this.settings.inlineMode.showTooltipOnHover : this.settings.inlineMode.showTooltipOnClick;
- }
- switch (tooltipTrigger) {
- case "disabled":
- return false;
- case "hover":
- return interactionType === "hover" && !import_obsidian15.Platform.isMobile;
- case "click":
- return interactionType === "click";
- case "auto":
- default:
- if (import_obsidian15.Platform.isMobile) {
- return interactionType === "click";
- } else {
- return true;
- }
- }
- }
- /**
- * 🎯 커서 위치 기반 가장 가까운 다음 오류 찾기
- */
- static findNextErrorFromCursor() {
- if (!this.app || this.activeErrors.size === 0) {
- return null;
- }
- try {
- const view = this.app.workspace.getActiveViewOfType(import_obsidian17.MarkdownView);
- if (!view || !view.editor) {
- Logger.warn("\uD604\uC7AC \uD65C\uC131 Markdown \uC5D0\uB514\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
- return null;
- }
- const cursor = view.editor.getCursor();
- const cursorOffset = view.editor.posToOffset(cursor);
- Logger.debug(`\uCEE4\uC11C \uC704\uCE58: line ${cursor.line}, ch ${cursor.ch}, offset ${cursorOffset}`);
- const sortedErrors = Array.from(this.activeErrors.values()).sort((a, b) => a.start - b.start);
- for (const error of sortedErrors) {
- if (error.start > cursorOffset) {
- Logger.debug(`\uB2E4\uC74C \uC624\uB958 \uBC1C\uACAC: "${error.correction.original}" at offset ${error.start}`);
- return error;
- }
- }
- if (sortedErrors.length > 0) {
- const firstError = sortedErrors[0];
- Logger.debug(`\uB9C8\uC9C0\uB9C9\uAE4C\uC9C0 \uB3C4\uB2EC, \uCCAB \uBC88\uC9F8 \uC624\uB958\uB85C \uC21C\uD658: "${firstError.correction.original}"`);
- return firstError;
- }
- return null;
- } catch (error) {
- Logger.error("\uB2E4\uC74C \uC624\uB958 \uCC3E\uAE30 \uC911 \uC624\uB958:", error);
- return null;
- }
- }
- /**
- * 🎯 커서 위치 기반 가장 가까운 이전 오류 찾기
- */
- static findPreviousErrorFromCursor() {
- if (!this.app || this.activeErrors.size === 0) {
- return null;
- }
- try {
- const view = this.app.workspace.getActiveViewOfType(import_obsidian17.MarkdownView);
- if (!view || !view.editor) {
- Logger.warn("\uD604\uC7AC \uD65C\uC131 Markdown \uC5D0\uB514\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
- return null;
- }
- const cursor = view.editor.getCursor();
- const cursorOffset = view.editor.posToOffset(cursor);
- Logger.debug(`\uCEE4\uC11C \uC704\uCE58: line ${cursor.line}, ch ${cursor.ch}, offset ${cursorOffset}`);
- const sortedErrors = Array.from(this.activeErrors.values()).sort((a, b) => b.start - a.start);
- for (const error of sortedErrors) {
- if (error.end < cursorOffset) {
- Logger.debug(`\uC774\uC804 \uC624\uB958 \uBC1C\uACAC: "${error.correction.original}" at offset ${error.start}-${error.end}`);
- return error;
- }
- }
- if (sortedErrors.length > 0) {
- const lastError = sortedErrors[0];
- Logger.debug(`\uCC98\uC74C\uAE4C\uC9C0 \uB3C4\uB2EC, \uB9C8\uC9C0\uB9C9 \uC624\uB958\uB85C \uC21C\uD658: "${lastError.correction.original}"`);
- return lastError;
- }
- return null;
- } catch (error) {
- Logger.error("\uC774\uC804 \uC624\uB958 \uCC3E\uAE30 \uC911 \uC624\uB958:", error);
- return null;
- }
- }
- /**
- * 🎯 오류 위치로 커서 이동 및 뷰 스크롤
- */
- static moveToError(error) {
- if (!this.app) {
- Logger.warn("App \uC778\uC2A4\uD134\uC2A4\uAC00 \uC5C6\uC5B4 \uC624\uB958 \uC704\uCE58\uB85C \uC774\uB3D9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
- return;
- }
- try {
- const view = this.app.workspace.getActiveViewOfType(import_obsidian17.MarkdownView);
- if (!view || !view.editor) {
- Logger.warn("\uD604\uC7AC \uD65C\uC131 Markdown \uC5D0\uB514\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
- return;
- }
- const editor = view.editor;
- const startPos = editor.offsetToPos(error.start);
- const endPos = editor.offsetToPos(error.end);
- Logger.debug(`\uC624\uB958 \uC704\uCE58\uB85C \uC774\uB3D9: "${error.correction.original}" at line ${startPos.line}, ch ${startPos.ch}`);
- editor.setCursor(startPos);
- const range = { from: startPos, to: endPos };
- editor.scrollIntoView(range, true);
- editor.focus();
- } catch (error2) {
- Logger.error("\uC624\uB958 \uC704\uCE58\uB85C \uC774\uB3D9 \uC911 \uBB38\uC81C \uBC1C\uC0DD:", error2);
- }
- }
- /**
- * 🎯 포커스된 오류에 현재 제안을 임시로 반영
- */
- static applyCurrentSuggestionTemporarily() {
- if (!this.currentFocusedError || !this.app) {
- return;
- }
- try {
- const view = this.app.workspace.getActiveViewOfType(import_obsidian17.MarkdownView);
- if (!view || !view.editor) {
- Logger.warn("\uD604\uC7AC \uD65C\uC131 Markdown \uC5D0\uB514\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
- return;
- }
- const suggestions = [this.currentFocusedError.correction.original, ...this.currentFocusedError.correction.corrected];
- if (!suggestions || suggestions.length === 0) {
- return;
- }
- const currentSuggestion = suggestions[this.currentSuggestionIndex];
- const editor = view.editor;
- const actualCurrentText = editor.getRange(
- editor.offsetToPos(this.currentFocusedError.start),
- editor.offsetToPos(this.currentFocusedError.end)
- );
- Logger.debug(`\u{1F50D} \uC2E4\uC81C \uD14D\uC2A4\uD2B8 \uD655\uC778: "${actualCurrentText}" \u2192 "${currentSuggestion}"`);
- if (this.currentView) {
- this.currentView.dispatch({
- effects: [setTemporarySuggestionMode.of(true)]
- });
- Logger.debug(`\u{1F3AF} \uC784\uC2DC \uC81C\uC548 \uBAA8\uB4DC \uD65C\uC131\uD654\uB428`);
- }
- const startPos = editor.offsetToPos(this.currentFocusedError.start);
- const endPos = editor.offsetToPos(this.currentFocusedError.end);
- editor.replaceRange(currentSuggestion, startPos, endPos);
- const lengthDiff = currentSuggestion.length - actualCurrentText.length;
- this.currentFocusedError.end = this.currentFocusedError.start + currentSuggestion.length;
- for (const [, error] of this.activeErrors) {
- if (error.start > this.currentFocusedError.start) {
- error.start += lengthDiff;
- error.end += lengthDiff;
- }
- }
- const newEndPos = editor.offsetToPos(this.currentFocusedError.start + currentSuggestion.length);
- editor.setCursor(newEndPos);
- if (this.currentView && this.currentFocusedError) {
- requestAnimationFrame(() => {
- if (this.currentView && this.currentFocusedError) {
- this.currentView.dispatch({
- effects: [setFocusedErrorDecoration.of(this.currentFocusedError.uniqueId)]
- });
- Logger.debug(`\u{1F3AF} \uD3EC\uCEE4\uC2A4 decoration \uC7AC\uC801\uC6A9 \uC644\uB8CC: ${this.currentFocusedError.uniqueId} (${this.currentFocusedError.start}-${this.currentFocusedError.end})`);
- }
- });
- Logger.debug(`\u{1F3AF} \uC784\uC2DC \uC81C\uC548 \uBAA8\uB4DC\uC5D0\uC11C \uD3EC\uCEE4\uC2A4 \uC720\uC9C0: ${this.currentFocusedError.uniqueId} (${this.currentFocusedError.start}-${this.currentFocusedError.end})`);
- }
- } catch (error) {
- Logger.error("\uC784\uC2DC \uC81C\uC548 \uC801\uC6A9 \uC911 \uC624\uB958:", error);
- }
- }
- /**
- * 다크모드를 고려한 오류 스타일을 생성합니다.
- */
- static getErrorStyle(underlineStyle, underlineColor, isHover = false, aiStatus, aiColor, aiBackgroundColor) {
- const isDarkMode = document.body.classList.contains("theme-dark");
- let actualColor;
- let actualBgColor;
- if (aiStatus && aiColor && aiBackgroundColor) {
- actualColor = aiColor;
- actualBgColor = isHover ? aiBackgroundColor.replace("0.1", "0.2") : aiBackgroundColor;
- Logger.debug(`\u{1F3A8} AI \uC0C9\uC0C1 \uC801\uC6A9: ${aiStatus} - ${actualColor}`);
- } else {
- if (isDarkMode) {
- actualColor = isHover ? "var(--color-red)" : "var(--color-red)";
- actualBgColor = isHover ? "rgba(var(--color-red-rgb), 0.2)" : "rgba(var(--color-red-rgb), 0.1)";
- } else {
- actualColor = isHover ? "var(--color-red)" : "var(--color-red)";
- actualBgColor = isHover ? "rgba(var(--color-red-rgb), 0.15)" : "rgba(var(--color-red-rgb), 0.08)";
- }
- }
- return `text-decoration-line: underline !important; text-decoration-style: ${underlineStyle} !important; text-decoration-color: ${actualColor} !important; text-decoration-thickness: 2px !important; background-color: ${actualBgColor} !important; cursor: pointer !important;`;
- }
- /**
- * 🎯 컨텍스트 기반 호버 영역 확장
- * 주변 형태소 정보를 활용하여 더 넓은 호버 영역 제공
- */
- static expandHoverAreaByMorphemes(element, error) {
- if (!error.morphemeInfo || !this.currentView) {
- Logger.debug(`\uD615\uD0DC\uC18C \uC815\uBCF4 \uC5C6\uC74C, \uD638\uBC84 \uC601\uC5ED \uD655\uC7A5 \uC0DD\uB7B5: ${error.correction.original}`);
- return;
- }
- try {
- const tokenBoundaries = this.getTokenBoundaries(error);
- if (!tokenBoundaries)
- return;
- const expandedStyle = this.createExpandedHoverStyle(tokenBoundaries);
- this.createExpandedHoverZone(element, expandedStyle, error);
- Logger.debug(`\u{1F3AF} \uD638\uBC84 \uC601\uC5ED \uD655\uC7A5: ${error.correction.original} (\uD1A0\uD070: ${tokenBoundaries.startToken}-${tokenBoundaries.endToken})`);
- } catch (err) {
- Logger.warn("\uD638\uBC84 \uC601\uC5ED \uD655\uC7A5 \uC2E4\uD328:", err);
- }
- }
- /**
- * 형태소 정보 기반 토큰 경계 계산
- */
- static getTokenBoundaries(error) {
- if (!error.morphemeInfo || !this.currentView)
- return null;
- try {
- const doc = this.currentView.state.doc;
- const errorStart = error.start;
- const errorEnd = error.end;
- const contextStart = Math.max(0, errorStart - 30);
- const contextEnd = Math.min(doc.length, errorEnd + 30);
- const contextText = doc.sliceString(contextStart, contextEnd);
- const relativeErrorStart = errorStart - contextStart;
- const relativeErrorEnd = errorEnd - contextStart;
- let expandedStart = relativeErrorStart;
- let expandedEnd = relativeErrorEnd;
- while (expandedStart > 0) {
- const char = contextText[expandedStart - 1];
- if (/[\s.,!?;:\-()[\]{}'"'""''…]/.test(char))
- break;
- if (/[가-힣]/.test(char) && expandedStart <= relativeErrorStart - 10)
- break;
- expandedStart--;
- }
- while (expandedEnd < contextText.length) {
- const char = contextText[expandedEnd];
- if (/[\s.,!?;:\-()[\]{}'"'""''…]/.test(char))
- break;
- if (/[가-힣]/.test(char) && expandedEnd >= relativeErrorEnd + 10)
- break;
- expandedEnd++;
- }
- return {
- startToken: contextStart + expandedStart,
- endToken: contextStart + expandedEnd,
- contextText: contextText.slice(expandedStart, expandedEnd)
- };
- } catch (err) {
- Logger.warn("\uD1A0\uD070 \uACBD\uACC4 \uACC4\uC0B0 \uC2E4\uD328:", err);
- return null;
- }
- }
- /**
- * 확장된 호버 스타일 생성
- */
- static createExpandedHoverStyle(boundaries) {
- const isDarkMode = document.body.classList.contains("theme-dark");
- return `
- position: relative;
- z-index: 2;
- &::before {
- content: '';
- position: absolute;
- left: -5px;
- right: -5px;
- top: -2px;
- bottom: -2px;
- background: ${isDarkMode ? "rgba(var(--color-red-rgb), 0.05)" : "rgba(var(--color-red-rgb), 0.03)"};
- border-radius: 3px;
- pointer-events: none;
- z-index: -1;
- }
- `;
- }
- /**
- * 확장된 호버 감지 영역 생성
- */
- static createExpandedHoverZone(originalElement, style, error) {
- var _a;
- const existingZone = (_a = originalElement.parentElement) == null ? void 0 : _a.querySelector(".korean-grammar-expanded-hover");
- if (existingZone) {
- existingZone.remove();
- }
- const expandedZone = document.createElement("span");
- expandedZone.className = "korean-grammar-expanded-hover korean-grammar-expanded-zone";
- expandedZone.addEventListener("mouseenter", () => {
- Logger.debug(`\u{1F3AF} \uD655\uC7A5 \uD638\uBC84 \uC601\uC5ED \uC9C4\uC785: ${error.correction.original}`);
- if (!this.currentHoveredError || this.currentHoveredError.uniqueId !== error.uniqueId) {
- this.currentHoveredError = error;
- this.handleErrorHover(error, originalElement);
- }
- });
- expandedZone.addEventListener("mouseleave", () => {
- Logger.debug(`\u{1F3AF} \uD655\uC7A5 \uD638\uBC84 \uC601\uC5ED \uC774\uD0C8: ${error.correction.original}`);
- setTimeout(() => {
- var _a2;
- if (((_a2 = this.currentHoveredError) == null ? void 0 : _a2.uniqueId) === error.uniqueId) {
- this.currentHoveredError = null;
- }
- }, 200);
- });
- if (originalElement.parentElement) {
- originalElement.classList.add("korean-grammar-relative");
- originalElement.appendChild(expandedZone);
- }
- }
- /**
- * 🤖 기존 오류가 있는지 확인
- */
- static hasErrors() {
- return this.activeErrors.size > 0;
- }
- /**
- * 🔧 주어진 에디터뷰가 현재 InlineModeService가 관리하는 뷰인지 확인
- */
- static isCurrentView(editorView) {
- return this.currentView === editorView;
- }
- /**
- * 🤖 현재 오류 개수 반환
- */
- static getErrorCount() {
- return this.activeErrors.size;
- }
- /**
- * 🔥 강제 오류 상태 완전 정리 (외부 호출용)
- */
- static forceCleanAllErrors() {
- Logger.log("\u{1F525} InlineModeService: \uAC15\uC81C \uC624\uB958 \uC0C1\uD0DC \uC644\uC804 \uC815\uB9AC");
- this.activeErrors.clear();
- if (this.currentView) {
- this.clearErrors(this.currentView);
- }
- }
- /**
- * 🔥 현재 문서 텍스트에 실제로 존재하는 오류만 유지 (기존 선택 영역 로직 활용)
- */
- static filterErrorsByCurrentDocument(currentDocumentText) {
- const originalCount = this.activeErrors.size;
- Logger.log(`\u{1F525} \uD604\uC7AC \uBB38\uC11C \uAE30\uC900 \uC624\uB958 \uD544\uD130\uB9C1 \uC2DC\uC791 - \uC804\uCCB4 \uC624\uB958: ${originalCount}\uAC1C`);
- const errorsToRemove = [];
- this.activeErrors.forEach((error, errorId) => {
- if (!currentDocumentText.includes(error.correction.original)) {
- errorsToRemove.push(errorId);
- }
- });
- errorsToRemove.forEach((errorId) => this.activeErrors.delete(errorId));
- if (errorsToRemove.length > 0) {
- this.refreshErrorWidgets();
- }
- Logger.log(`\u{1F525} \uC624\uB958 \uD544\uD130\uB9C1 \uC644\uB8CC - \uC81C\uAC70: ${errorsToRemove.length}\uAC1C, \uC720\uC9C0: ${this.activeErrors.size}\uAC1C`);
- }
- /**
- * 🤖 오류 ID로 최신 AI 분석 결과가 포함된 오류 객체 가져오기
- */
- static getErrorWithAIData(errorId) {
- return this.activeErrors.get(errorId);
- }
- /**
- * 🤖 기존 인라인 오류에 대한 AI 분석 실행
- */
- static async runAIAnalysisOnExistingErrors(progressCallback) {
- var _a, _b, _c;
- if (this.activeErrors.size === 0) {
- Logger.warn("AI \uBD84\uC11D\uD560 \uAE30\uC874 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- throw new Error("\uBD84\uC11D\uD560 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC\uB97C \uC2E4\uD589\uD558\uC138\uC694.");
- }
- if (!((_b = (_a = this.settings) == null ? void 0 : _a.ai) == null ? void 0 : _b.enabled)) {
- Logger.warn("AI \uAE30\uB2A5\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
- throw new Error("AI \uAE30\uB2A5\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC124\uC815\uC5D0\uC11C AI \uAE30\uB2A5\uC744 \uD65C\uC131\uD654\uD558\uC138\uC694.");
- }
- Logger.log(`\u{1F916} \uAE30\uC874 \uC624\uB958 ${this.activeErrors.size}\uAC1C\uC5D0 \uB300\uD55C AI \uBD84\uC11D \uC2DC\uC791`);
- try {
- const corrections = [];
- this.activeErrors.forEach((error) => {
- corrections.push({
- original: error.correction.original,
- corrected: error.correction.corrected || []
- });
- });
- const aiService = (_c = getExtendedWindow().koreanGrammarPlugin) == null ? void 0 : _c.instance;
- if (!aiService) {
- throw new Error("AI \uBD84\uC11D \uC11C\uBE44\uC2A4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- const currentStates = {};
- corrections.forEach((_, index) => {
- currentStates[index] = { state: "error", value: "" };
- });
- const aiRequest = {
- corrections,
- morphemeData: null,
- userEdits: [],
- // 인라인 모드에서는 사용자 편집 없음
- currentStates,
- originalText: corrections.map((c) => c.original).join(" "),
- // 원본 텍스트 추가
- onProgress: progressCallback ? (current, total, message) => {
- progressCallback(current, total);
- } : void 0
- };
- let analysisResults = [];
- if (aiService && typeof aiService === "object" && "analyzeCorrections" in aiService) {
- const analyzeMethod = aiService.analyzeCorrections;
- if (typeof analyzeMethod === "function") {
- analysisResults = await analyzeMethod.call(aiService, aiRequest);
- }
- }
- Logger.log(`\u{1F916} AI \uBD84\uC11D \uC644\uB8CC: ${analysisResults.length}\uAC1C \uACB0\uACFC`);
- const totalResults = analysisResults.length;
- for (let i = 0; i < analysisResults.length; i++) {
- const result = analysisResults[i];
- const errorArray = Array.from(this.activeErrors.values());
- const targetError = errorArray[result.correctionIndex];
- if (targetError) {
- targetError.aiAnalysis = {
- selectedValue: result.selectedValue,
- confidence: result.confidence,
- reasoning: result.reasoning,
- isExceptionProcessed: result.isExceptionProcessed
- };
- if (result.isExceptionProcessed) {
- targetError.aiStatus = "exception";
- targetError.aiColor = "#3b82f6";
- targetError.aiBackgroundColor = "rgba(59, 130, 246, 0.1)";
- } else if (result.selectedValue === targetError.correction.original) {
- targetError.aiStatus = "keep-original";
- targetError.aiColor = "#f59e0b";
- targetError.aiBackgroundColor = "rgba(245, 158, 11, 0.1)";
- } else {
- targetError.aiStatus = "corrected";
- targetError.aiColor = "#10b981";
- targetError.aiBackgroundColor = "rgba(16, 185, 129, 0.1)";
- targetError.aiSelectedValue = result.selectedValue;
- }
- this.activeErrors.set(targetError.uniqueId, targetError);
- Logger.debug(`\u{1F3A8} \uC624\uB958 "${targetError.correction.original}"\uC5D0 AI \uBD84\uC11D \uACB0\uACFC \uC801\uC6A9: ${result.selectedValue} (\uC2E0\uB8B0\uB3C4: ${result.confidence}%) - \uC0C9\uC0C1: ${targetError.aiStatus}`);
- }
- }
- if (this.currentView) {
- const correctedErrors = Array.from(this.activeErrors.values()).filter((e) => e.aiStatus === "corrected");
- const exceptionErrors = Array.from(this.activeErrors.values()).filter((e) => e.aiStatus === "exception");
- const keepOriginalErrors = Array.from(this.activeErrors.values()).filter((e) => e.aiStatus === "keep-original");
- Logger.debug(`\u{1F3A8} AI \uACB0\uACFC \uC801\uC6A9 \uD6C4 \uC624\uB958 \uC0C1\uD0DC: \uB179\uC0C9(corrected)=${correctedErrors.length}\uAC1C, \uD30C\uB780\uC0C9(exception)=${exceptionErrors.length}\uAC1C, \uC8FC\uD669\uC0C9(keep-original)=${keepOriginalErrors.length}\uAC1C`);
- Logger.debug(`\u{1F7E2} \uB179\uC0C9 \uC624\uB958\uB4E4: ${correctedErrors.map((e) => `"${e.correction.original}" \u2192 "${e.aiSelectedValue}"`).join(", ")}`);
- this.refreshErrorWidgets();
- }
- Logger.log("\u{1F916} AI \uBD84\uC11D \uACB0\uACFC\uAC00 \uC778\uB77C\uC778 \uC624\uB958\uC5D0 \uC801\uC6A9\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
- } catch (error) {
- Logger.error("AI \uBD84\uC11D \uC2E4\uD589 \uC911 \uC624\uB958:", error);
- throw error;
- }
- }
- /**
- * 텍스트 맞춤법 검사 (기존 로직 재활용)
- */
- static async checkText(text) {
- var _a, _b, _c, _d;
- if (!text.trim()) {
- throw new Error("\uAC80\uC0AC\uD560 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- if (!this.currentView) {
- throw new Error("\uC5D0\uB514\uD130 \uBDF0\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- }
- Logger.log(`\u{1F4DD} \uC778\uB77C\uC778 \uBAA8\uB4DC \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC2DC\uC791: ${text.length}\uC790`);
- try {
- const apiService = new SpellCheckApiService();
- const settings = this.settings;
- if (!settings) {
- Logger.error("\uC124\uC815\uC774 \uCD08\uAE30\uD654\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const result = await apiService.checkSpelling(text, settings);
- if (!result.corrections || result.corrections.length === 0) {
- Logger.log("\uB9DE\uCDA4\uBC95 \uC624\uB958\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- return;
- }
- Logger.debug(`checkText: showErrors \uD638\uCD9C \uC804 - corrections: ${result.corrections.length}\uAC1C`);
- await this.showErrors(
- this.currentView,
- result.corrections,
- ((_b = (_a = this.settings) == null ? void 0 : _a.inlineMode) == null ? void 0 : _b.underlineStyle) || "wavy",
- ((_d = (_c = this.settings) == null ? void 0 : _c.inlineMode) == null ? void 0 : _d.underlineColor) || "var(--color-red)",
- this.app || void 0
- );
- Logger.debug(`checkText: showErrors \uD638\uCD9C \uD6C4 - activeErrors: ${this.activeErrors.size}\uAC1C`);
- Logger.log(`\u{1F4DD} \uC778\uB77C\uC778 \uBAA8\uB4DC \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC644\uB8CC: ${result.corrections.length}\uAC1C \uC624\uB958 \uBC1C\uACAC, ${this.activeErrors.size}\uAC1C activeErrors`);
- } catch (error) {
- Logger.error("\uC778\uB77C\uC778 \uBAA8\uB4DC \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC624\uB958:", error);
- throw error;
- }
- }
- /**
- * 오류 위젯들을 새로고침 (AI 분석 결과 반영)
- */
- static refreshErrorWidgets() {
- var _a, _b, _c, _d;
- if (!this.currentView) {
- Logger.warn("refreshErrorWidgets: \uC5D0\uB514\uD130 \uBDF0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- Logger.debug("\uC778\uB77C\uC778 \uC624\uB958 \uC704\uC82F \uC0C8\uB85C\uACE0\uCE68 \uC2DC\uC791 (AI \uACB0\uACFC \uBC18\uC601)");
- try {
- this.currentView.dispatch({
- effects: [clearAllErrorDecorations.of(true)]
- });
- if (this.activeErrors.size > 0) {
- const allErrors = Array.from(this.activeErrors.values());
- const correctedErrors = allErrors.filter((e) => e.aiStatus === "corrected");
- Logger.debug(`\u{1F504} refreshErrorWidgets: \uC804\uCCB4 ${allErrors.length}\uAC1C \uC624\uB958 \uC911 \uB179\uC0C9(corrected) ${correctedErrors.length}\uAC1C`);
- Logger.debug(`\u{1F504} \uB179\uC0C9 \uC624\uB958 \uC0C1\uC138: ${correctedErrors.map((e) => `"${e.correction.original}" \u2192 "${e.aiSelectedValue}" (${e.start}-${e.end})`).join(", ")}`);
- this.currentView.dispatch({
- effects: addErrorDecorations.of({
- errors: allErrors,
- underlineStyle: ((_b = (_a = this.settings) == null ? void 0 : _a.inlineMode) == null ? void 0 : _b.underlineStyle) || "wavy",
- underlineColor: ((_d = (_c = this.settings) == null ? void 0 : _c.inlineMode) == null ? void 0 : _d.underlineColor) || "var(--color-red)",
- preserveAIColors: true
- // 🎨 AI 색상 보존 플래그 추가
- })
- });
- }
- Logger.debug(`${this.activeErrors.size}\uAC1C \uC624\uB958 \uC704\uC82F \uC0C8\uB85C\uACE0\uCE68 \uC644\uB8CC (AI \uC0C9\uC0C1 \uBC18\uC601)`);
- } catch (error) {
- Logger.error("\uC624\uB958 \uC704\uC82F \uC0C8\uB85C\uACE0\uCE68 \uC2E4\uD328:", error);
- }
- }
- /**
- * 📝 모든 인라인 오류에 대해 현재 상태값을 에디터에 일괄 적용
- * 사용자가 변경한 상태값들을 모두 반영하여 적용 + 예외처리 사전 등록
- */
- static async applyAllCorrections() {
- var _a;
- if (!this.currentView) {
- throw new Error("\uC5D0\uB514\uD130 \uBDF0\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
- }
- if (this.activeErrors.size === 0) {
- throw new Error("\uC801\uC6A9\uD560 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- if (!this.settings) {
- throw new Error("\uD50C\uB7EC\uADF8\uC778 \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- Logger.log(`\u{1F4DD} ${this.activeErrors.size}\uAC1C \uC624\uB958 \uC77C\uAD04 \uC801\uC6A9 \uC2DC\uC791`);
- const doc = this.currentView.state.doc;
- const changes = [];
- const wordsToIgnore = [];
- let appliedCount = 0;
- let skippedCount = 0;
- let ignoredCount = 0;
- const errors = Array.from(this.activeErrors.values()).sort((a, b) => b.start - a.start);
- for (const error of errors) {
- try {
- if (error.aiStatus === "exception") {
- const wordToIgnore = error.correction.original.trim();
- if (wordToIgnore && !wordsToIgnore.includes(wordToIgnore)) {
- wordsToIgnore.push(wordToIgnore);
- ignoredCount++;
- Logger.debug(`\u{1F535} \uC608\uC678\uCC98\uB9AC \uB2E8\uC5B4 \uC218\uC9D1: "${wordToIgnore}"`);
- }
- }
- const replacement = this.determineReplacementText(error);
- if (replacement === null) {
- skippedCount++;
- Logger.debug(`\u23ED\uFE0F \uC608\uC678\uCC98\uB9AC\uB41C \uC624\uB958 \uAC74\uB108\uB6F0\uAE30: "${error.correction.original}"`);
- continue;
- }
- if (error.start < 0 || error.end > doc.length || error.start >= error.end) {
- Logger.warn(`\u26A0\uFE0F \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC94\uC704: ${error.start}-${error.end} (\uBB38\uC11C \uAE38\uC774: ${doc.length})`);
- skippedCount++;
- continue;
- }
- const currentText = doc.sliceString(error.start, error.end);
- if (currentText !== error.correction.original) {
- Logger.warn(`\u26A0\uFE0F \uD14D\uC2A4\uD2B8 \uBD88\uC77C\uCE58: \uC608\uC0C1 "${error.correction.original}", \uC2E4\uC81C "${currentText}"`);
- skippedCount++;
- continue;
- }
- changes.push({
- from: error.start,
- to: error.end,
- insert: replacement
- });
- appliedCount++;
- Logger.debug(`\u2705 \uC801\uC6A9 \uC608\uC815: "${error.correction.original}" \u2192 "${replacement}"`);
- } catch (error_inner) {
- Logger.error(`\u274C \uC624\uB958 \uC801\uC6A9 \uC2E4\uD328:`, error_inner);
- skippedCount++;
- }
- }
- if (wordsToIgnore.length > 0) {
- let updatedSettings = this.settings;
- for (const word of wordsToIgnore) {
- updatedSettings = IgnoredWordsService.addIgnoredWord(word, updatedSettings);
- }
- if ((_a = window.koreanGrammarPlugin) == null ? void 0 : _a.instance) {
- const plugin = window.koreanGrammarPlugin.instance;
- plugin.settings = updatedSettings;
- await plugin.saveSettings();
- Logger.log(`\u{1F535} \uC608\uC678\uCC98\uB9AC \uC0AC\uC804 \uB4F1\uB85D: ${wordsToIgnore.join(", ")}`);
- }
- }
- if (changes.length > 0) {
- this.currentView.dispatch({
- changes,
- userEvent: "korean-grammar.apply-all"
- });
- Logger.log(`\u{1F4DD} \uC77C\uAD04 \uC801\uC6A9 \uC644\uB8CC: ${appliedCount}\uAC1C \uC801\uC6A9, ${skippedCount}\uAC1C \uAC74\uB108\uB6F0\uAE30, ${ignoredCount}\uAC1C \uC608\uC678\uCC98\uB9AC \uB4F1\uB85D`);
- }
- this.clearErrors(this.currentView);
- return appliedCount;
- }
- /**
- * 🎯 개별 오류에 대해 현재 상태에 따른 교체 텍스트 결정
- * AI 분석 후 색상 기반 처리 + 사용자 선택 우선
- */
- static determineReplacementText(error) {
- var _a;
- if (error.aiStatus === "exception") {
- Logger.debug(`\u{1F535} AI \uC608\uC678\uCC98\uB9AC (\uD30C\uB780\uC0C9): "${error.correction.original}" \u2192 \uC608\uC678\uCC98\uB9AC \uC0AC\uC804 \uB4F1\uB85D`);
- return null;
- }
- if (error.aiStatus === "keep-original") {
- Logger.debug(`\u{1F7E0} \uC6D0\uBCF8 \uC720\uC9C0 (\uC8FC\uD669\uC0C9): "${error.correction.original}" \u2192 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC74C`);
- return null;
- }
- if (error.aiStatus === "corrected" && ((_a = error.aiAnalysis) == null ? void 0 : _a.selectedValue)) {
- Logger.debug(`\u{1F7E2} AI \uAD50\uC815 \uC120\uD0DD (\uB179\uC0C9): "${error.correction.original}" \u2192 "${error.aiAnalysis.selectedValue}"`);
- return error.aiAnalysis.selectedValue;
- }
- if (error.aiSelectedValue && !error.aiStatus) {
- Logger.debug(`\u270F\uFE0F \uC0AC\uC6A9\uC790 \uAC1C\uBCC4 \uC120\uD0DD: "${error.correction.original}" \u2192 "${error.aiSelectedValue}"`);
- return error.aiSelectedValue;
- }
- if (error.correction.corrected && error.correction.corrected.length > 0) {
- const firstCorrection = error.correction.corrected[0];
- Logger.debug(`\u{1F534} \uAE30\uBCF8 \uC218\uC815 \uC81C\uC548 \uC801\uC6A9 (\uBE68\uAC04\uC0C9): "${error.correction.original}" \u2192 "${firstCorrection}"`);
- return firstCorrection;
- }
- Logger.debug(`\u23ED\uFE0F \uC218\uC815 \uC81C\uC548 \uC5C6\uC74C, \uC6D0\uBCF8 \uC720\uC9C0: "${error.correction.original}"`);
- return null;
- }
- /**
- * 🔵 특정 단어를 예외처리 사전에 추가하고 동일한 단어의 모든 오류 제거
- * @param word 예외처리할 단어
- * @returns 제거된 오류 개수
- */
- static async addWordToIgnoreListAndRemoveErrors(word) {
- if (!this.settings || !this.currentView) {
- throw new Error("\uC124\uC815 \uB610\uB294 \uC5D0\uB514\uD130 \uBDF0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- const trimmedWord = word.trim();
- if (!trimmedWord) {
- return 0;
- }
- Logger.log(`\u{1F535} \uC608\uC678\uCC98\uB9AC \uC0AC\uC804 \uCD94\uAC00 \uBC0F \uB3D9\uC77C \uB2E8\uC5B4 \uC624\uB958 \uC81C\uAC70: "${trimmedWord}"`);
- const updatedSettings = IgnoredWordsService.addIgnoredWord(trimmedWord, this.settings);
- const pluginWrapper = getExtendedWindow().koreanGrammarPlugin;
- if (pluginWrapper && typeof pluginWrapper === "object" && "instance" in pluginWrapper) {
- const plugin = pluginWrapper.instance;
- if (plugin && typeof plugin === "object" && "saveSettings" in plugin) {
- const saveMethod = plugin.saveSettings;
- if (typeof saveMethod === "function") {
- plugin.settings = updatedSettings;
- await saveMethod.call(plugin);
- }
- }
- this.settings = updatedSettings;
- Logger.debug(`\u{1F535} \uC608\uC678\uCC98\uB9AC \uC0AC\uC804\uC5D0 \uC800\uC7A5\uB428: "${trimmedWord}"`);
- }
- const errorsToRemove = [];
- this.activeErrors.forEach((error, errorId) => {
- if (error.correction.original.trim() === trimmedWord) {
- errorsToRemove.push(errorId);
- }
- });
- if (errorsToRemove.length === 0) {
- Logger.debug(`\u{1F535} \uC81C\uAC70\uD560 "${trimmedWord}" \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`);
- return 0;
- }
- errorsToRemove.forEach((errorId) => {
- this.activeErrors.delete(errorId);
- });
- this.refreshErrorWidgets();
- Logger.log(`\u{1F535} "${trimmedWord}" \uAD00\uB828 ${errorsToRemove.length}\uAC1C \uC624\uB958\uAC00 \uC81C\uAC70\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
- return errorsToRemove.length;
- }
- /**
- * 📍 선택 영역 내 오류 개수 반환
- */
- static getErrorCountInSelection(selectedText) {
- if (!selectedText.trim() || this.activeErrors.size === 0) {
- return 0;
- }
- let count = 0;
- this.activeErrors.forEach((error) => {
- if (selectedText.includes(error.correction.original)) {
- count++;
- }
- });
- Logger.debug(`\uC120\uD0DD \uC601\uC5ED \uB0B4 \uC624\uB958 \uAC1C\uC218: ${count}\uAC1C (\uC804\uCCB4: ${this.activeErrors.size}\uAC1C)`);
- return count;
- }
- /**
- * 📍 선택 영역 내 오류들에 대한 AI 분석 실행
- */
- static async runAIAnalysisOnErrorsInSelection(selectedText, progressCallback) {
- var _a, _b;
- if (!selectedText.trim() || this.activeErrors.size === 0) {
- throw new Error("\uC120\uD0DD \uC601\uC5ED\uC774\uB098 \uBD84\uC11D\uD560 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- if (!((_b = (_a = this.settings) == null ? void 0 : _a.ai) == null ? void 0 : _b.enabled)) {
- throw new Error("AI \uAE30\uB2A5\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
- }
- const selectionErrors = [];
- const selectionErrorIds = [];
- this.activeErrors.forEach((error, errorId) => {
- if (selectedText.includes(error.correction.original)) {
- selectionErrors.push({
- original: error.correction.original,
- corrected: error.correction.corrected || [],
- morphemeInfo: error.morphemeInfo
- });
- selectionErrorIds.push(errorId);
- }
- });
- if (selectionErrors.length === 0) {
- throw new Error("\uC120\uD0DD \uC601\uC5ED\uC5D0 \uBD84\uC11D\uD560 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- Logger.log(`\u{1F916} \uC120\uD0DD \uC601\uC5ED \uB0B4 ${selectionErrors.length}\uAC1C \uC624\uB958\uC5D0 \uB300\uD55C AI \uBD84\uC11D \uC2DC\uC791`);
- try {
- const aiService = new (await Promise.resolve().then(() => (init_aiAnalysisService(), aiAnalysisService_exports))).AIAnalysisService(this.settings.ai);
- const aiRequest = {
- originalText: selectedText,
- corrections: selectionErrors,
- contextWindow: 50,
- currentStates: {},
- enhancedContext: true
- };
- const analysisResults = await aiService.analyzeCorrections(aiRequest);
- for (let i = 0; i < analysisResults.length; i++) {
- const result = analysisResults[i];
- const errorId = selectionErrorIds[i];
- const targetError = this.activeErrors.get(errorId);
- if (targetError) {
- targetError.aiStatus = result.isExceptionProcessed ? "exception" : result.selectedValue === targetError.correction.original ? "keep-original" : "corrected";
- targetError.aiConfidence = result.confidence || 0;
- targetError.aiReasoning = result.reasoning || "";
- targetError.aiSelectedValue = result.selectedValue;
- this.activeErrors.set(errorId, targetError);
- Logger.debug(`\u{1F3A8} \uC120\uD0DD \uC601\uC5ED \uC624\uB958 "${targetError.correction.original}"\uC5D0 AI \uACB0\uACFC \uC801\uC6A9: ${result.selectedValue} (\uC2E0\uB8B0\uB3C4: ${result.confidence}%)`);
- }
- }
- Logger.log(`\u{1F916} \uC120\uD0DD \uC601\uC5ED AI \uBD84\uC11D \uC644\uB8CC: ${selectionErrors.length}\uAC1C \uC624\uB958 \uCC98\uB9AC\uB428`);
- } catch (error) {
- Logger.error("\uC120\uD0DD \uC601\uC5ED AI \uBD84\uC11D \uC2E4\uD328:", error);
- throw error;
- }
- }
- /**
- * 📍 선택 영역에만 오류 표시 (기존 오류 유지)
- */
- static async showErrorsInSelection(view, corrections, selectedText, underlineStyle = "wavy", underlineColor = "var(--color-red)", app, morphemeData) {
- if (!view || !corrections.length || !selectedText.trim()) {
- Logger.warn("\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC120\uD0DD \uC601\uC5ED \uC624\uB958 \uD45C\uC2DC - \uD544\uC218 \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const analysisNotice = NotificationUtils.showAnalysisStartNotice("spelling");
- try {
- const doc = view.state.doc;
- const fullText = doc.toString();
- const selectionStart = fullText.indexOf(selectedText);
- const selectionEnd = selectionStart + selectedText.length;
- if (selectionStart === -1) {
- throw new Error("\uC120\uD0DD\uB41C \uD14D\uC2A4\uD2B8\uB97C \uBB38\uC11C\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- }
- Logger.debug(`\uC120\uD0DD \uC601\uC5ED \uC704\uCE58: ${selectionStart}-${selectionEnd} (${selectedText.length}\uC790)`);
- const errorsToRemove = [];
- this.activeErrors.forEach((error, errorId) => {
- if (error.start >= selectionStart && error.end <= selectionEnd) {
- errorsToRemove.push(errorId);
- }
- });
- errorsToRemove.forEach((errorId) => {
- this.activeErrors.delete(errorId);
- });
- if (errorsToRemove.length > 0) {
- view.dispatch({
- effects: removeErrorDecorations.of(errorsToRemove)
- });
- Logger.debug(`\uC120\uD0DD \uC601\uC5ED \uB0B4 \uAE30\uC874 \uC624\uB958 ${errorsToRemove.length}\uAC1C \uC81C\uAC70\uB428`);
- }
- let finalMorphemeData = morphemeData;
- if (!finalMorphemeData && this.settings) {
- try {
- NotificationUtils.updateNoticeMessage(analysisNotice, "\u{1F4CB} \uD615\uD0DC\uC18C \uBD84\uC11D \uC911...");
- const apiService = new SpellCheckApiService();
- finalMorphemeData = await apiService.analyzeMorphemes(selectedText, this.settings);
- } catch (error) {
- Logger.warn("\uC120\uD0DD \uC601\uC5ED \uD615\uD0DC\uC18C \uBD84\uC11D \uC2E4\uD328, \uAE30\uBCF8 \uB85C\uC9C1 \uC0AC\uC6A9:", error);
- }
- }
- NotificationUtils.updateNoticeMessage(analysisNotice, "\u{1F527} \uC624\uB958 \uC911\uBCF5 \uC81C\uAC70 \uC911...");
- const optimizedCorrections = MorphemeUtils.removeDuplicateCorrections(
- corrections,
- finalMorphemeData,
- selectedText
- );
- const filteredCorrections = optimizedCorrections.filter((correction) => {
- const isIgnored = this.settings ? IgnoredWordsService.isWordIgnored(correction.original, this.settings) : false;
- if (isIgnored) {
- Logger.debug(`\uC608\uC678 \uB2E8\uC5B4\uB85C \uD544\uD130\uB9C1\uB428: "${correction.original}"`);
- }
- return !isIgnored;
- });
- Logger.debug(`\uC120\uD0DD \uC601\uC5ED \uC624\uB958 \uCC98\uB9AC: ${corrections.length} \u2192 ${optimizedCorrections.length} \u2192 ${filteredCorrections.length}\uAC1C`);
- const errors = [];
- filteredCorrections.forEach((correction, index) => {
- const searchText = correction.original;
- let searchStart = 0;
- let occurrence = 1;
- while (searchStart < selectedText.length) {
- const foundIndex = selectedText.indexOf(searchText, searchStart);
- if (foundIndex === -1)
- break;
- const absoluteStart = selectionStart + foundIndex;
- const absoluteEnd = absoluteStart + searchText.length;
- const uniqueId = `${searchText}_${foundIndex}_${occurrence}`;
- const posInfo = finalMorphemeData ? MorphemeUtils.extractPosInfo(correction.original, finalMorphemeData) : null;
- const error = {
- uniqueId,
- start: absoluteStart,
- end: absoluteEnd,
- line: 0,
- // 선택 영역에서는 정확한 라인 계산 생략
- ch: 0,
- // 선택 영역에서는 정확한 문자 위치 계산 생략
- isActive: true,
- correction,
- morphemeInfo: posInfo || void 0
- };
- errors.push(error);
- this.activeErrors.set(uniqueId, error);
- Logger.debug(`\uC120\uD0DD \uC601\uC5ED \uC624\uB958 \uC704\uCE58: "${searchText}" at ${absoluteStart}-${absoluteEnd}`);
- searchStart = foundIndex + 1;
- occurrence++;
- }
- });
- if (errors.length > 0) {
- view.dispatch({
- effects: addErrorDecorations.of({
- errors,
- underlineStyle,
- underlineColor
- })
- });
- }
- NotificationUtils.hideNotice(analysisNotice);
- Logger.log(`\uC120\uD0DD \uC601\uC5ED \uC624\uB958 \uD45C\uC2DC \uC644\uB8CC: ${errors.length}\uAC1C \uC624\uB958 \uCD94\uAC00\uB428`);
- } catch (error) {
- NotificationUtils.hideNotice(analysisNotice);
- Logger.error("\uC120\uD0DD \uC601\uC5ED \uC624\uB958 \uD45C\uC2DC \uC2E4\uD328:", error);
- throw error;
- }
- }
- // 🚧 구현 중인 기능들 - 향후 완성 예정
- // 위의 복잡한 메서드들은 향후 단계별로 구현할 예정입니다.
- // 현재는 기본 Command Palette 명령어와 UI 연동에 집중합니다.
-};
-InlineModeService.activeErrors = /* @__PURE__ */ new Map();
-InlineModeService.currentView = null;
-InlineModeService.settings = null;
-InlineModeService.currentFocusedError = null;
-InlineModeService.currentSuggestionIndex = 0;
-// 🔧 레거시: 기존 키보드 스코프 방식 (Command Palette 방식으로 대체됨)
-// private static keyboardScope: Scope | null = null;
-InlineModeService.app = null;
-InlineModeService.currentHoveredError = null;
-InlineModeService.hoverTimeout = null;
-
-// main.ts
-(0, import_obsidian19.addIcon)(
- "han-spellchecker",
- ``
-);
-var KoreanGrammarPlugin = class extends import_obsidian19.Plugin {
- async onload() {
- if (false) {
- Logger.configureForProduction();
- } else {
- Logger.configureForDevelopment();
- }
- await this.loadSettings();
- this.orchestrator = new SpellCheckOrchestrator(
- this.app,
- this.settings,
- (updatedSettings) => {
- this.settings = updatedSettings;
- this.saveSettings();
- }
- );
- InlineModeService.registerCommands(this);
- this.addRibbonIcon("han-spellchecker", "Check spelling", async () => {
- await this.orchestrator.execute();
- });
- this.addCommand({
- id: "check-korean-spelling",
- name: "\uD55C\uAD6D\uC5B4 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC",
- callback: async () => {
- await this.orchestrator.execute();
- }
- });
- this.addCommand({
- id: "check-current-paragraph",
- name: "\uD604\uC7AC \uBB38\uB2E8 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC",
- callback: async () => {
- await this.orchestrator.executeCurrentParagraph();
- }
- });
- this.addCommand({
- id: "check-current-word",
- name: "\uD604\uC7AC \uB2E8\uC5B4 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC",
- callback: async () => {
- await this.orchestrator.executeCurrentWord();
- }
- });
- this.addCommand({
- id: "check-current-sentence",
- name: "\uD604\uC7AC \uBB38\uC7A5 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC",
- callback: async () => {
- await this.orchestrator.executeCurrentSentence();
- }
- });
- this.addCommand({
- id: "inline-spell-check",
- name: "\uC778\uB77C\uC778 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC (\uBCA0\uD0C0)",
- callback: async () => {
- if (!this.settings.inlineMode.enabled) {
- new import_obsidian19.Notice("\uC778\uB77C\uC778 \uBAA8\uB4DC\uAC00 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC124\uC815\uC5D0\uC11C \uBCA0\uD0C0 \uAE30\uB2A5\uC744 \uD65C\uC131\uD654\uD558\uC138\uC694.");
- return;
- }
- await this.executeInlineSpellCheck();
- }
- });
- this.addCommand({
- id: "inline-ai-analysis",
- name: "\u{1F916} \uC778\uB77C\uC778 AI \uBD84\uC11D (\uBCA0\uD0C0)",
- callback: async () => {
- if (!this.settings.inlineMode.enabled) {
- new import_obsidian19.Notice("\uC778\uB77C\uC778 \uBAA8\uB4DC\uAC00 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC124\uC815\uC5D0\uC11C \uBCA0\uD0C0 \uAE30\uB2A5\uC744 \uD65C\uC131\uD654\uD558\uC138\uC694.");
- return;
- }
- if (!this.settings.ai.enabled) {
- new import_obsidian19.Notice("AI \uAE30\uB2A5\uC774 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC124\uC815\uC5D0\uC11C AI \uAE30\uB2A5\uC744 \uD65C\uC131\uD654\uD558\uC138\uC694.");
- return;
- }
- await this.executeInlineAIAnalysis();
- }
- });
- this.addCommand({
- id: "inline-apply-all",
- name: "\u{1F4DD} \uC778\uB77C\uC778 \uC624\uB958 \uC77C\uAD04 \uC801\uC6A9",
- callback: async () => {
- if (!this.settings.inlineMode.enabled) {
- new import_obsidian19.Notice("\uC778\uB77C\uC778 \uBAA8\uB4DC\uAC00 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC124\uC815\uC5D0\uC11C \uBCA0\uD0C0 \uAE30\uB2A5\uC744 \uD65C\uC131\uD654\uD558\uC138\uC694.");
- return;
- }
- await this.executeInlineApplyAll();
- }
- });
- this.addCommand({
- id: "inline-clear-all",
- name: "\u{1F5D1}\uFE0F \uC778\uB77C\uC778 \uBD84\uC11D \uACB0\uACFC \uD45C\uC2DC \uC77C\uAD04 \uCDE8\uC18C",
- callback: async () => {
- if (!this.settings.inlineMode.enabled) {
- new import_obsidian19.Notice("\uC778\uB77C\uC778 \uBAA8\uB4DC\uAC00 \uBE44\uD65C\uC131\uD654\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC124\uC815\uC5D0\uC11C \uBCA0\uD0C0 \uAE30\uB2A5\uC744 \uD65C\uC131\uD654\uD558\uC138\uC694.");
- return;
- }
- await this.executeInlineClearAll();
- }
- });
- if (this.settings.inlineMode.enabled) {
- this.enableInlineMode();
- }
- this.addSettingTab(new ModernSettingsTab(this.app, this));
- window.koreanGrammarPlugin = {
- settings: this.settings,
- instance: this
- };
- this.setupDocumentChangeListeners();
- }
- onunload() {
- if (this.fileOpenListener) {
- this.app.workspace.offref(this.fileOpenListener);
- }
- if (this.activeLeafChangeListener) {
- this.app.workspace.offref(this.activeLeafChangeListener);
- }
- this.disableInlineMode();
- if (this.orchestrator) {
- this.orchestrator.destroy();
- }
- }
- async loadSettings() {
- const savedData = await this.loadData();
- this.settings = SettingsService.mergeWithDefaults(savedData || {});
- }
- async saveSettings() {
- await this.saveData(this.settings);
- if (this.orchestrator) {
- this.orchestrator.updateSettings(this.settings);
- }
- }
- /**
- * 인라인 맞춤법 검사 실행
- */
- async executeInlineSpellCheck() {
- const activeLeaf = this.app.workspace.activeLeaf;
- if (!activeLeaf) {
- new import_obsidian19.Notice("\uD65C\uC131\uD654\uB41C \uD3B8\uC9D1\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const editor = activeLeaf.view.editor;
- if (!editor) {
- new import_obsidian19.Notice("\uD3B8\uC9D1\uAE30\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const editorView = editor.cm;
- if (!editorView) {
- new import_obsidian19.Notice("CodeMirror \uC5D0\uB514\uD130 \uBDF0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- try {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
- const selectedText = editor.getSelection();
- let targetText;
- let isSelection = false;
- if (selectedText.trim()) {
- targetText = selectedText;
- isSelection = true;
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC120\uD0DD \uC601\uC5ED \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC2DC\uC791 (${targetText.length}\uC790)`);
- } else {
- targetText = editorView.state.doc.toString();
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC804\uCCB4 \uBB38\uC11C \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC2DC\uC791 (${targetText.length}\uC790)`);
- }
- if (!targetText.trim()) {
- new import_obsidian19.Notice("\uAC80\uC0AC\uD560 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const apiService = new SpellCheckApiService();
- const result = await apiService.checkSpelling(targetText, this.settings);
- if (!result.corrections || result.corrections.length === 0) {
- new import_obsidian19.Notice(`${isSelection ? "\uC120\uD0DD \uC601\uC5ED\uC5D0\uC11C " : ""}\uB9DE\uCDA4\uBC95 \uC624\uB958\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
- if (isSelection) {
- return;
- }
- InlineModeService.clearErrors(editorView);
- return;
- }
- if (isSelection) {
- await InlineModeService.showErrorsInSelection(
- editorView,
- result.corrections,
- selectedText,
- this.settings.inlineMode.underlineStyle,
- this.settings.inlineMode.underlineColor,
- this.app
- );
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC120\uD0DD \uC601\uC5ED \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC644\uB8CC (${result.corrections.length}\uAC1C \uC624\uB958)`);
- } else {
- await InlineModeService.showErrors(
- editorView,
- result.corrections,
- this.settings.inlineMode.underlineStyle,
- this.settings.inlineMode.underlineColor,
- this.app
- );
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uC804\uCCB4 \uBB38\uC11C \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC644\uB8CC (${result.corrections.length}\uAC1C \uC624\uB958)`);
- }
- } catch (error) {
- Logger.error("\uC778\uB77C\uC778 \uBAA8\uB4DC \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC624\uB958:", error);
- new import_obsidian19.Notice("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- /**
- * 인라인 모드 활성화
- */
- enableInlineMode() {
- try {
- this.registerEditorExtension([errorDecorationField, temporarySuggestionModeField]);
- const activeLeaf = this.app.workspace.activeLeaf;
- if (activeLeaf && activeLeaf.view && activeLeaf.view.editor) {
- const editorView = activeLeaf.view.editor.cm;
- if (editorView) {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
- Logger.log("\uC778\uB77C\uC778 \uBAA8\uB4DC: InlineModeService \uD0A4\uBCF4\uB4DC \uC2A4\uCF54\uD504 \uCD08\uAE30\uD654\uB428");
- }
- }
- window.InlineModeService = InlineModeService;
- Logger.log("\uC778\uB77C\uC778 \uBAA8\uB4DC \uD65C\uC131\uD654\uB428 (InlineModeService + \uD0A4\uBCF4\uB4DC \uB2E8\uCD95\uD0A4)");
- } catch (error) {
- Logger.error("\uC778\uB77C\uC778 \uBAA8\uB4DC \uD65C\uC131\uD654 \uC2E4\uD328:", error);
- new import_obsidian19.Notice("\uC778\uB77C\uC778 \uBAA8\uB4DC \uD65C\uC131\uD654\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- /**
- * 인라인 모드 비활성화
- */
- disableInlineMode() {
- if (window.InlineModeService) {
- delete window.InlineModeService;
- }
- Logger.log("\uC778\uB77C\uC778 \uBAA8\uB4DC \uBE44\uD65C\uC131\uD654\uB428");
- }
- /**
- * 🤖 인라인 모드 AI 분석 실행
- */
- async executeInlineAIAnalysis() {
- var _a;
- const activeView = this.app.workspace.getActiveViewOfType(import_obsidian19.MarkdownView);
- if (!(activeView == null ? void 0 : activeView.editor)) {
- new import_obsidian19.Notice("\uD65C\uC131\uD654\uB41C \uB9C8\uD06C\uB2E4\uC6B4 \uD3B8\uC9D1\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const editor = activeView.editor;
- try {
- const selectedText = editor.getSelection();
- let targetText = selectedText;
- let isSelection = false;
- if (!targetText.trim()) {
- targetText = editor.getValue();
- if (!targetText.trim()) {
- new import_obsidian19.Notice("\uBD84\uC11D\uD560 \uD14D\uC2A4\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- } else {
- isSelection = true;
- }
- Logger.log(`\uC778\uB77C\uC778 AI \uBD84\uC11D \uC2DC\uC791 - ${isSelection ? "\uC120\uD0DD\uB41C \uC601\uC5ED" : "\uC804\uCCB4 \uBB38\uC11C"}: ${targetText.length}\uC790`);
- editor.focus();
- const currentEditorView = (_a = activeView.editor) == null ? void 0 : _a.cm;
- if (!currentEditorView) {
- Logger.error("\uD604\uC7AC \uC5D0\uB514\uD130\uBDF0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
- new import_obsidian19.Notice("\uC5D0\uB514\uD130\uBDF0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- Logger.log("\u{1F527} CRITICAL: \uD604\uC7AC \uBB38\uC11C\uC758 \uC5D0\uB514\uD130\uBDF0\uB85C InlineModeService \uAC15\uC81C \uC7AC\uC124\uC815");
- Logger.log("\u{1F525} SMART FIX: \uD604\uC7AC \uBB38\uC11C \uAE30\uC900\uC73C\uB85C \uC624\uB958 \uD544\uD130\uB9C1");
- InlineModeService.setEditorView(currentEditorView, this.settings, this.app);
- InlineModeService.filterErrorsByCurrentDocument(targetText);
- const hasCurrentDocumentErrors = InlineModeService.hasErrors();
- Logger.log(`\u{1F50D} \uD604\uC7AC \uBB38\uC11C\uC758 \uC624\uB958 \uC874\uC7AC \uC5EC\uBD80: ${hasCurrentDocumentErrors}`);
- if (hasCurrentDocumentErrors) {
- Logger.log("\u{1F50D} \uCF00\uC774\uC2A4 1: \uD604\uC7AC \uBB38\uC11C\uC5D0 \uC624\uB958\uAC00 \uC788\uC5B4\uC11C AI \uBD84\uC11D \uC2E4\uD589");
- if (isSelection) {
- Logger.log("\u{1F50D} \uC120\uD0DD \uC601\uC5ED AI \uBD84\uC11D \uC2E4\uD589");
- await this.analyzeExistingInlineErrorsInSelection(selectedText);
- } else {
- Logger.log("\u{1F50D} \uC804\uCCB4 \uC624\uB958 AI \uBD84\uC11D \uC2E4\uD589");
- await this.analyzeExistingInlineErrors();
- }
- } else {
- Logger.log("\u{1F50D} \uCF00\uC774\uC2A4 2: \uD604\uC7AC \uBB38\uC11C\uC5D0 \uC624\uB958\uAC00 \uC5C6\uC5B4\uC11C \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uD6C4 AI \uBD84\uC11D \uC2E4\uD589");
- await this.analyzeTextWithSpellCheckAndAI(targetText, isSelection);
- }
- } catch (error) {
- Logger.error("\uC778\uB77C\uC778 AI \uBD84\uC11D \uC624\uB958:", error);
- new import_obsidian19.Notice("AI \uBD84\uC11D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
- }
- }
- /**
- * 선택 영역 내 기존 인라인 오류에 대한 AI 분석
- */
- async analyzeExistingInlineErrorsInSelection(selectedText) {
- const { getCurrentModelInfo: getCurrentModelInfo2 } = await Promise.resolve().then(() => (init_aiModels(), aiModels_exports));
- const modelInfo = getCurrentModelInfo2(this.settings.ai);
- const analysisNotice = new import_obsidian19.Notice(`\u{1F916} \uC120\uD0DD \uC601\uC5ED AI \uBD84\uC11D \uC2DC\uC791 (${modelInfo.displayName})...`, 0);
- try {
- const selectionErrorCount = InlineModeService.getErrorCountInSelection(selectedText);
- if (selectionErrorCount === 0) {
- analysisNotice.hide();
- new import_obsidian19.Notice("\u26A0\uFE0F \uC120\uD0DD \uC601\uC5ED\uC5D0 \uBD84\uC11D\uD560 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", 3e3);
- return;
- }
- analysisNotice.setMessage(`\u{1F522} \uC120\uD0DD \uC601\uC5ED \uB0B4 ${selectionErrorCount}\uAC1C \uC624\uB958 \uBD84\uC11D \uC900\uBE44 \uC911...`);
- await new Promise((resolve) => setTimeout(resolve, 500));
- analysisNotice.setMessage(`\u{1F9E0} \uC120\uD0DD \uC601\uC5ED AI \uBD84\uC11D \uC911 (${modelInfo.model})... \uC218\uC2ED \uCD08 \uC18C\uC694\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4`);
- await InlineModeService.runAIAnalysisOnErrorsInSelection(selectedText, (current, total) => {
- analysisNotice.setMessage(`\u{1F9E0} ${modelInfo.model} \uBD84\uC11D \uC911... (${current}/${total})`);
- });
- InlineModeService.refreshErrorWidgets();
- analysisNotice.hide();
- new import_obsidian19.Notice(`\u2705 \uC120\uD0DD \uC601\uC5ED AI \uBD84\uC11D \uC644\uB8CC! ${selectionErrorCount}\uAC1C \uC624\uB958\uC5D0 \uC0C9\uC0C1\uC774 \uC801\uC6A9\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`, 4e3);
- new import_obsidian19.Notice("\u{1F4A1} \uC624\uB958\uB97C \uD074\uB9AD\uD558\uC5EC AI \uCD94\uCC9C \uC774\uC720\uB97C \uD655\uC778\uD558\uC138\uC694.", 3e3);
- } catch (error) {
- analysisNotice.hide();
- Logger.error("\uC120\uD0DD \uC601\uC5ED AI \uBD84\uC11D \uC2E4\uD328:", error);
- new import_obsidian19.Notice("\u274C \uC120\uD0DD \uC601\uC5ED AI \uBD84\uC11D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.", 4e3);
- }
- }
- /**
- * 기존 인라인 오류에 대한 AI 분석
- */
- async analyzeExistingInlineErrors() {
- const { getCurrentModelInfo: getCurrentModelInfo2 } = await Promise.resolve().then(() => (init_aiModels(), aiModels_exports));
- const modelInfo = getCurrentModelInfo2(this.settings.ai);
- const analysisNotice = new import_obsidian19.Notice(`\u{1F916} AI \uBD84\uC11D \uC2DC\uC791 (${modelInfo.displayName})...`, 0);
- try {
- const errorCount = InlineModeService.getErrorCount();
- analysisNotice.setMessage(`\u{1F522} ${errorCount}\uAC1C \uC624\uB958 \uBD84\uC11D \uC900\uBE44 \uC911...`);
- Logger.log(`\u{1F50D} \uD1A0\uD070 \uACBD\uACE0 \uD655\uC778 \uC2DC\uC791 - \uC624\uB958 \uAC1C\uC218: ${errorCount}`);
- const shouldProceed = await this.checkInlineTokenUsageWarning();
- Logger.log(`\u{1F50D} \uD1A0\uD070 \uACBD\uACE0 \uACB0\uACFC: ${shouldProceed ? "\uC9C4\uD589" : "\uCDE8\uC18C"}`);
- if (!shouldProceed) {
- analysisNotice.hide();
- return;
- }
- await new Promise((resolve) => setTimeout(resolve, 500));
- analysisNotice.setMessage(`\u{1F9E0} AI \uBD84\uC11D \uC911 (${modelInfo.model})... \uC218\uC2ED \uCD08 \uC18C\uC694\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4`);
- await InlineModeService.runAIAnalysisOnExistingErrors((current, total) => {
- analysisNotice.setMessage(`\u{1F9E0} ${modelInfo.model} \uBD84\uC11D \uC911... (${current}/${total})`);
- });
- InlineModeService.refreshErrorWidgets();
- analysisNotice.hide();
- new import_obsidian19.Notice(`\u2705 AI \uBD84\uC11D \uC644\uB8CC! ${errorCount}\uAC1C \uC624\uB958\uC5D0 \uC0C9\uC0C1\uC774 \uC801\uC6A9\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`, 4e3);
- new import_obsidian19.Notice("\u{1F4A1} \uC624\uB958\uB97C \uD074\uB9AD\uD558\uC5EC AI \uCD94\uCC9C \uC774\uC720\uB97C \uD655\uC778\uD558\uC138\uC694.", 3e3);
- } catch (error) {
- analysisNotice.hide();
- Logger.error("\uAE30\uC874 \uC624\uB958 AI \uBD84\uC11D \uC2E4\uD328:", error);
- new import_obsidian19.Notice("\u274C AI \uBD84\uC11D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.", 4e3);
- }
- }
- /**
- * 맞춤법 검사 후 AI 분석 실행
- */
- async analyzeTextWithSpellCheckAndAI(targetText, isSelection) {
- var _a;
- const processNotice = new import_obsidian19.Notice("\u{1F4DD} \uB9DE\uCDA4\uBC95 \uAC80\uC0AC\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4...", 0);
- const activeLeaf = this.app.workspace.activeLeaf;
- if (!activeLeaf) {
- processNotice.hide();
- new import_obsidian19.Notice("\uD65C\uC131\uD654\uB41C \uD3B8\uC9D1\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- try {
- processNotice.setMessage(`\u{1F4DD} ${targetText.length}\uC790 \uD14D\uC2A4\uD2B8 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC911...`);
- const editorView = (_a = activeLeaf.view.editor) == null ? void 0 : _a.cm;
- if (editorView) {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
- }
- await InlineModeService.checkText(targetText);
- await new Promise((resolve) => setTimeout(resolve, 1e3));
- const errorCount = InlineModeService.getErrorCount();
- if (errorCount === 0) {
- processNotice.hide();
- new import_obsidian19.Notice("\u2705 \uB9DE\uCDA4\uBC95 \uC624\uB958\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.", 3e3);
- return;
- }
- processNotice.setMessage(`\u2705 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uC644\uB8CC! ${errorCount}\uAC1C \uC624\uB958 \uBC1C\uACAC (\uBE68\uAC04\uC0C9 \uD45C\uC2DC)`);
- await new Promise((resolve) => setTimeout(resolve, 3e3));
- const { getCurrentModelInfo: getCurrentModelInfo2 } = await Promise.resolve().then(() => (init_aiModels(), aiModels_exports));
- const modelInfo = getCurrentModelInfo2(this.settings.ai);
- processNotice.setMessage(`\u{1F916} ${errorCount}\uAC1C \uC624\uB958 AI \uBD84\uC11D \uC2DC\uC791 (${modelInfo.displayName})...`);
- Logger.log(`\u{1F50D} \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uD6C4 \uD1A0\uD070 \uACBD\uACE0 \uD655\uC778 \uC2DC\uC791 - \uC624\uB958 \uAC1C\uC218: ${errorCount}`);
- Logger.log(`\u{1F50D} AI \uC124\uC815 \uD655\uC778:`, {
- enabled: this.settings.ai.enabled,
- provider: this.settings.ai.provider,
- showTokenWarning: this.settings.ai.showTokenWarning,
- threshold: this.settings.ai.tokenWarningThreshold,
- maxTokens: this.settings.ai.maxTokens
- });
- try {
- const shouldProceed = await this.checkInlineTokenUsageWarning();
- Logger.log(`\u{1F50D} \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uD6C4 \uD1A0\uD070 \uACBD\uACE0 \uACB0\uACFC: ${shouldProceed ? "\uC9C4\uD589" : "\uCDE8\uC18C"}`);
- if (!shouldProceed) {
- processNotice.hide();
- return;
- }
- } catch (error) {
- Logger.error("\u{1F50D} \uD1A0\uD070 \uACBD\uACE0 \uD655\uC778 \uC911 \uC608\uC678 \uBC1C\uC0DD:", error);
- }
- await new Promise((resolve) => setTimeout(resolve, 500));
- processNotice.setMessage(`\u{1F9E0} AI \uBD84\uC11D \uC911 (${modelInfo.model})... \uC218\uC2ED \uCD08 \uC18C\uC694\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4`);
- await InlineModeService.runAIAnalysisOnExistingErrors((current, total) => {
- processNotice.setMessage(`\u{1F9E0} ${modelInfo.model} \uBD84\uC11D \uC911... (${current}/${total})`);
- });
- InlineModeService.refreshErrorWidgets();
- processNotice.hide();
- new import_obsidian19.Notice(`\u2705 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uBC0F AI \uBD84\uC11D \uC644\uB8CC!`, 4e3);
- new import_obsidian19.Notice(`\u{1F3A8} ${errorCount}\uAC1C \uC624\uB958\uC5D0 AI \uC0C9\uC0C1\uC774 \uC801\uC6A9\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`, 3e3);
- new import_obsidian19.Notice("\u{1F4A1} \uC624\uB958\uB97C \uD074\uB9AD\uD558\uC5EC AI \uCD94\uCC9C \uC774\uC720\uB97C \uD655\uC778\uD558\uC138\uC694.", 3e3);
- } catch (error) {
- processNotice.hide();
- Logger.error("\uB9DE\uCDA4\uBC95 \uAC80\uC0AC \uBC0F AI \uBD84\uC11D \uC2E4\uD328:", error);
- new import_obsidian19.Notice("\u274C \uBD84\uC11D \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.", 4e3);
- }
- }
- /**
- * 📝 인라인 모드 일괄 적용 실행
- */
- async executeInlineApplyAll() {
- var _a;
- const activeLeaf = this.app.workspace.activeLeaf;
- if (!activeLeaf || !activeLeaf.view || !activeLeaf.view.editor) {
- new import_obsidian19.Notice("\uD65C\uC131\uD654\uB41C \uD3B8\uC9D1\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const errorCount = InlineModeService.getErrorCount();
- if (errorCount === 0) {
- new import_obsidian19.Notice("\uC801\uC6A9\uD560 \uC778\uB77C\uC778 \uC624\uB958\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 \uB9DE\uCDA4\uBC95 \uAC80\uC0AC\uB97C \uC2E4\uD589\uD558\uC138\uC694.");
- return;
- }
- const processNotice = new import_obsidian19.Notice(`\u{1F4DD} ${errorCount}\uAC1C \uC624\uB958 \uC77C\uAD04 \uC801\uC6A9 \uC911...`, 0);
- try {
- const editorView = (_a = activeLeaf.view.editor) == null ? void 0 : _a.cm;
- if (editorView) {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
- }
- const appliedCount = await InlineModeService.applyAllCorrections();
- processNotice.hide();
- new import_obsidian19.Notice(`\u2705 ${appliedCount}\uAC1C \uC624\uB958\uAC00 \uC5D0\uB514\uD130\uC5D0 \uC801\uC6A9\uB418\uC5C8\uC2B5\uB2C8\uB2E4!`, 4e3);
- if (appliedCount < errorCount) {
- const skippedCount = errorCount - appliedCount;
- new import_obsidian19.Notice(`\u{1F4A1} ${skippedCount}\uAC1C \uC624\uB958\uB294 \uAC74\uB108\uB6F0\uAC70\uB098 \uC608\uC678\uCC98\uB9AC \uC0AC\uC804\uC5D0 \uB4F1\uB85D\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`, 3e3);
- }
- } catch (error) {
- processNotice.hide();
- Logger.error("\uC778\uB77C\uC778 \uC624\uB958 \uC77C\uAD04 \uC801\uC6A9 \uC2E4\uD328:", error);
- new import_obsidian19.Notice("\u274C \uC77C\uAD04 \uC801\uC6A9 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.", 4e3);
- }
- }
- /**
- * 인라인 분석 결과 표시 일괄 취소 실행
- */
- async executeInlineClearAll() {
- var _a;
- const activeLeaf = this.app.workspace.activeLeaf;
- if (!activeLeaf || !activeLeaf.view || !activeLeaf.view.editor) {
- new import_obsidian19.Notice("\uD65C\uC131\uD654\uB41C \uD3B8\uC9D1\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const errorCount = InlineModeService.getErrorCount();
- if (errorCount === 0) {
- new import_obsidian19.Notice("\uCDE8\uC18C\uD560 \uC778\uB77C\uC778 \uBD84\uC11D \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
- return;
- }
- const processNotice = new import_obsidian19.Notice(`\u{1F5D1}\uFE0F ${errorCount}\uAC1C \uBD84\uC11D \uACB0\uACFC \uD45C\uC2DC \uC77C\uAD04 \uCDE8\uC18C \uC911...`, 2e3);
- try {
- const editorView = (_a = activeLeaf.view.editor) == null ? void 0 : _a.cm;
- if (editorView) {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
- InlineModeService.clearErrors(editorView);
- }
- processNotice.hide();
- new import_obsidian19.Notice(`\u2705 ${errorCount}\uAC1C \uBD84\uC11D \uACB0\uACFC \uD45C\uC2DC\uAC00 \uBAA8\uB450 \uC81C\uAC70\uB418\uC5C8\uC2B5\uB2C8\uB2E4!`, 3e3);
- } catch (error) {
- processNotice.hide();
- Logger.error("\uC778\uB77C\uC778 \uBD84\uC11D \uACB0\uACFC \uD45C\uC2DC \uC77C\uAD04 \uCDE8\uC18C \uC2E4\uD328:", error);
- new import_obsidian19.Notice("\u274C \uBD84\uC11D \uACB0\uACFC \uD45C\uC2DC \uC77C\uAD04 \uCDE8\uC18C \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.", 4e3);
- }
- }
- /**
- * 인라인 모드 AI 분석 토큰 경고 확인
- */
- async checkInlineTokenUsageWarning() {
- var _a;
- try {
- Logger.log("\u{1F50D} \uC778\uB77C\uC778 \uD1A0\uD070 \uACBD\uACE0 \uD655\uC778 \uC2DC\uC791");
- const { TokenWarningModal: TokenWarningModal2 } = await Promise.resolve().then(() => (init_tokenWarningModal(), tokenWarningModal_exports));
- Logger.log("\u{1F50D} TokenWarningModal \uC784\uD3EC\uD2B8 \uC644\uB8CC");
- const activeView = this.app.workspace.getActiveViewOfType(import_obsidian19.MarkdownView);
- if (!(activeView == null ? void 0 : activeView.editor)) {
- Logger.warn("\uD65C\uC131 \uB9C8\uD06C\uB2E4\uC6B4 \uC5D0\uB514\uD130\uAC00 \uC5C6\uC5B4\uC11C \uD1A0\uD070 \uACBD\uACE0 \uAC74\uB108\uB700");
- return true;
- }
- const currentEditorView = (_a = activeView.editor) == null ? void 0 : _a.cm;
- if (!currentEditorView || !InlineModeService.isCurrentView(currentEditorView)) {
- Logger.warn("\uD604\uC7AC \uC5D0\uB514\uD130\uBDF0\uAC00 InlineModeService\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC544\uC11C \uD1A0\uD070 \uACBD\uACE0 \uAC74\uB108\uB700");
- return true;
- }
- const errorCount = InlineModeService.getErrorCount();
- Logger.log(`\u{1F50D} \uD604\uC7AC \uBB38\uC11C\uC758 \uC720\uD6A8\uD55C \uC624\uB958 \uAC1C\uC218: ${errorCount}`);
- if (errorCount === 0) {
- Logger.log("\u{1F50D} \uC720\uD6A8\uD55C \uC624\uB958\uAC00 \uC5C6\uC5B4\uC11C \uBC14\uB85C \uC9C4\uD589");
- return true;
- }
- const sampleErrors = Array.from({ length: errorCount }, (_, i) => ({
- original: `\uC0D8\uD50C\uC624\uB958${i + 1}`,
- corrected: [`\uC218\uC815\uC548${i + 1}`],
- help: `\uC0D8\uD50C \uB3C4\uC6C0\uB9D0 ${i + 1}`
- }));
- Logger.log(`\u{1F50D} \uD1A0\uD070 \uCD94\uC815\uC6A9 \uC0D8\uD50C \uC624\uB958 \uC0DD\uC131: ${sampleErrors.length}\uAC1C (\uC2E4\uC81C \uC624\uB958 \uAC1C\uC218: ${errorCount})`);
- const request = {
- originalText: "",
- // 인라인 모드에서는 전체 텍스트 불필요
- corrections: sampleErrors,
- contextWindow: errorCount > 1 ? 30 : 100,
- // 복수 오류 시 최적화
- currentStates: {},
- enhancedContext: false
- // 인라인 모드에서는 단순한 컨텍스트
- };
- const { AIAnalysisService: AIAnalysisService2 } = await Promise.resolve().then(() => (init_aiAnalysisService(), aiAnalysisService_exports));
- const aiService = new AIAnalysisService2(this.settings.ai);
- const tokenWarningSettings = {
- showTokenWarning: this.settings.ai.showTokenWarning,
- tokenWarningThreshold: this.settings.ai.tokenWarningThreshold,
- maxTokens: this.settings.ai.maxTokens
- };
- Logger.log("\u{1F50D} \uD1A0\uD070 \uACBD\uACE0 \uC124\uC815:", {
- showTokenWarning: tokenWarningSettings.showTokenWarning,
- threshold: tokenWarningSettings.tokenWarningThreshold,
- maxTokens: tokenWarningSettings.maxTokens,
- aiEnabled: this.settings.ai.enabled,
- provider: this.settings.ai.provider
- });
- const onSettingsUpdate = (newMaxTokens) => {
- this.settings.ai.maxTokens = newMaxTokens;
- this.saveSettings();
- Logger.log(`\uC778\uB77C\uC778 \uBAA8\uB4DC: \uCD5C\uB300 \uD1A0\uD070\uC744 ${newMaxTokens}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD588\uC2B5\uB2C8\uB2E4.`);
- new import_obsidian19.Notice(`\u2699\uFE0F \uCD5C\uB300 \uD1A0\uD070\uC774 ${newMaxTokens.toLocaleString()}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`, 3e3);
- };
- Logger.log("\u{1F50D} TokenWarningModal.checkTokenUsageWarning \uD638\uCD9C \uC2DC\uC791");
- const result = await TokenWarningModal2.checkTokenUsageWarning(
- request,
- aiService,
- tokenWarningSettings,
- onSettingsUpdate
- );
- Logger.log(`\u{1F50D} TokenWarningModal.checkTokenUsageWarning \uACB0\uACFC: ${result}`);
- return result;
- } catch (error) {
- Logger.error("\uC778\uB77C\uC778 \uD1A0\uD070 \uACBD\uACE0 \uD655\uC778 \uC911 \uC624\uB958:", error);
- return true;
- }
- }
- /**
- * 🔧 문서 전환 감지 이벤트 리스너 설정
- * 파일이나 리프가 변경될 때마다 InlineModeService 상태를 자동으로 정리
- */
- setupDocumentChangeListeners() {
- this.fileOpenListener = this.app.workspace.on("file-open", (file) => {
- var _a, _b, _c, _d, _e;
- if (((_b = (_a = this.settings) == null ? void 0 : _a.inlineMode) == null ? void 0 : _b.enabled) && InlineModeService.hasErrors()) {
- Logger.log("\u{1F527} file-open: \uC774\uC804 \uBB38\uC11C\uC758 \uC778\uB77C\uC778 \uC624\uB958 \uC0C1\uD0DC \uC644\uC804 \uC815\uB9AC");
- InlineModeService.forceCleanAllErrors();
- }
- const activeView = this.app.workspace.getActiveViewOfType(import_obsidian19.MarkdownView);
- if ((activeView == null ? void 0 : activeView.editor) && ((_d = (_c = this.settings) == null ? void 0 : _c.inlineMode) == null ? void 0 : _d.enabled)) {
- const currentEditorView = (_e = activeView.editor) == null ? void 0 : _e.cm;
- if (currentEditorView) {
- InlineModeService.setEditorView(currentEditorView, this.settings, this.app);
- }
- }
- });
- this.activeLeafChangeListener = this.app.workspace.on("active-leaf-change", (leaf) => {
- var _a, _b, _c, _d, _e, _f;
- if (((_b = (_a = this.settings) == null ? void 0 : _a.inlineMode) == null ? void 0 : _b.enabled) && InlineModeService.hasErrors()) {
- Logger.log("\u{1F527} active-leaf-change: \uC774\uC804 \uD0ED\uC758 \uC778\uB77C\uC778 \uC624\uB958 \uC0C1\uD0DC \uC644\uC804 \uC815\uB9AC");
- InlineModeService.forceCleanAllErrors();
- }
- if (((_c = leaf == null ? void 0 : leaf.view) == null ? void 0 : _c.getViewType()) === "markdown" && ((_e = (_d = this.settings) == null ? void 0 : _d.inlineMode) == null ? void 0 : _e.enabled)) {
- const markdownView = leaf.view;
- if (markdownView == null ? void 0 : markdownView.editor) {
- const currentEditorView = (_f = markdownView.editor) == null ? void 0 : _f.cm;
- if (currentEditorView) {
- InlineModeService.setEditorView(currentEditorView, this.settings, this.app);
- }
- }
- }
- });
- }
-};
diff --git a/main.ts b/main.ts
index ddc3605..8ffe13e 100644
--- a/main.ts
+++ b/main.ts
@@ -34,9 +34,7 @@ export default class KoreanGrammarPlugin extends Plugin {
orchestrator: SpellCheckOrchestrator;
// 🤖 InlineModeService는 정적 클래스로 설계되어 인스턴스 불필요
- // 🔧 문서 전환 감지 이벤트 참조
- private fileOpenListener?: any;
- private activeLeafChangeListener?: any;
+ // 🤖 InlineModeService 참조 (전역 변수 대신 인스턴스 속성 사용)
async onload() {
// 디버그/프로덕션 모드 설정
@@ -67,96 +65,92 @@ export default class KoreanGrammarPlugin extends Plugin {
await this.orchestrator.execute();
});
- // 명령어 등록
+ // 명령어 등록 (conditional commands)
this.addCommand({
id: "check-korean-spelling",
name: "한국어 맞춤법 검사",
- callback: async () => {
- await this.orchestrator.execute();
+ checkCallback: (checking: boolean) => {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view) return false;
+ if (!checking) this.orchestrator.execute();
+ return true;
},
});
-
- // 현재 문단 맞춤법 검사 명령어 추가
+
this.addCommand({
id: "check-current-paragraph",
name: "현재 문단 맞춤법 검사",
- callback: async () => {
- await this.orchestrator.executeCurrentParagraph();
+ checkCallback: (checking: boolean) => {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view) return false;
+ if (!checking) this.orchestrator.executeCurrentParagraph();
+ return true;
},
});
- // 현재 단어 맞춤법 검사 명령어 추가
this.addCommand({
id: "check-current-word",
name: "현재 단어 맞춤법 검사",
- callback: async () => {
- await this.orchestrator.executeCurrentWord();
+ checkCallback: (checking: boolean) => {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view) return false;
+ if (!checking) this.orchestrator.executeCurrentWord();
+ return true;
},
});
- // 현재 문장 맞춤법 검사 명령어 추가
this.addCommand({
id: "check-current-sentence",
name: "현재 문장 맞춤법 검사",
- callback: async () => {
- await this.orchestrator.executeCurrentSentence();
+ checkCallback: (checking: boolean) => {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view) return false;
+ if (!checking) this.orchestrator.executeCurrentSentence();
+ return true;
},
});
- // 인라인 모드 명령어 추가 (베타 기능)
this.addCommand({
id: "inline-spell-check",
name: "인라인 맞춤법 검사 (베타)",
- callback: async () => {
- if (!this.settings.inlineMode.enabled) {
- new Notice("인라인 모드가 비활성화되어 있습니다. 설정에서 베타 기능을 활성화하세요.");
- return;
- }
- await this.executeInlineSpellCheck();
+ checkCallback: (checking: boolean) => {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view || !this.settings.inlineMode.enabled) return false;
+ if (!checking) this.executeInlineSpellCheck();
+ return true;
},
});
-
- // 🤖 인라인 모드 AI 분석 명령어 추가
this.addCommand({
id: "inline-ai-analysis",
- name: "🤖 인라인 AI 분석 (베타)",
- callback: async () => {
- if (!this.settings.inlineMode.enabled) {
- new Notice("인라인 모드가 비활성화되어 있습니다. 설정에서 베타 기능을 활성화하세요.");
- return;
- }
- if (!this.settings.ai.enabled) {
- new Notice("AI 기능이 비활성화되어 있습니다. 설정에서 AI 기능을 활성화하세요.");
- return;
- }
- await this.executeInlineAIAnalysis();
+ name: "인라인 AI 분석 (베타)",
+ checkCallback: (checking: boolean) => {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view || !this.settings.inlineMode.enabled || !this.settings.ai.enabled) return false;
+ if (!checking) this.executeInlineAIAnalysis();
+ return true;
},
});
- // 📝 인라인 모드 일괄 적용 명령어 추가
this.addCommand({
id: "inline-apply-all",
- name: "📝 인라인 오류 일괄 적용",
- callback: async () => {
- if (!this.settings.inlineMode.enabled) {
- new Notice("인라인 모드가 비활성화되어 있습니다. 설정에서 베타 기능을 활성화하세요.");
- return;
- }
- await this.executeInlineApplyAll();
+ name: "인라인 오류 일괄 적용",
+ checkCallback: (checking: boolean) => {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view || !this.settings.inlineMode.enabled) return false;
+ if (!checking) this.executeInlineApplyAll();
+ return true;
},
});
- // 인라인 분석 결과 표시 일괄 취소 명령어 추가
this.addCommand({
id: "inline-clear-all",
- name: "🗑️ 인라인 분석 결과 표시 일괄 취소",
- callback: async () => {
- if (!this.settings.inlineMode.enabled) {
- new Notice("인라인 모드가 비활성화되어 있습니다. 설정에서 베타 기능을 활성화하세요.");
- return;
- }
- await this.executeInlineClearAll();
+ name: "인라인 분석 결과 표시 일괄 취소",
+ checkCallback: (checking: boolean) => {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view || !this.settings.inlineMode.enabled) return false;
+ if (!checking) this.executeInlineClearAll();
+ return true;
},
});
@@ -168,28 +162,14 @@ export default class KoreanGrammarPlugin extends Plugin {
// 설정 탭 추가
this.addSettingTab(new ModernSettingsTab(this.app, this));
- // 🤖 전역 설정 등록 (인라인 모드 AI 분석용)
- (window as any).koreanGrammarPlugin = {
- settings: this.settings,
- instance: this
- };
-
// 🔧 문서 전환 감지 이벤트 리스너 등록
this.setupDocumentChangeListeners();
}
onunload() {
- // 🔧 문서 전환 감지 이벤트 리스너 정리
- if (this.fileOpenListener) {
- this.app.workspace.offref(this.fileOpenListener);
- }
- if (this.activeLeafChangeListener) {
- this.app.workspace.offref(this.activeLeafChangeListener);
- }
-
// 인라인 모드 정리
this.disableInlineMode();
-
+
// 오케스트레이터 정리
if (this.orchestrator) {
this.orchestrator.destroy();
@@ -213,21 +193,20 @@ export default class KoreanGrammarPlugin extends Plugin {
* 인라인 맞춤법 검사 실행
*/
async executeInlineSpellCheck(): Promise {
- const activeLeaf = this.app.workspace.activeLeaf;
- if (!activeLeaf) {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view) {
new Notice("활성화된 편집기가 없습니다.");
return;
}
- // @ts-ignore - Obsidian 내부 API 사용
- const editor = activeLeaf.view.editor;
+ const editor = view.editor;
if (!editor) {
new Notice("편집기를 찾을 수 없습니다.");
return;
}
// @ts-ignore - CodeMirror 6 에디터 뷰 접근
- const editorView = editor.cm;
+ const editorView = (editor as any).cm;
if (!editorView) {
new Notice("CodeMirror 에디터 뷰를 찾을 수 없습니다.");
return;
@@ -235,7 +214,7 @@ export default class KoreanGrammarPlugin extends Plugin {
try {
// 에디터 뷰 및 설정 초기화
- InlineModeService.setEditorView(editorView, this.settings, this.app);
+ InlineModeService.setEditorView(editorView, this.settings, this.app, async (s) => { this.settings = s; await this.saveSettings(); });
// 선택된 텍스트 확인
const selectedText = editor.getSelection();
@@ -312,19 +291,15 @@ export default class KoreanGrammarPlugin extends Plugin {
this.registerEditorExtension([errorDecorationField, temporarySuggestionModeField]);
// InlineModeService 초기화 (키보드 단축키 지원을 위해 필요)
- const activeLeaf = this.app.workspace.activeLeaf;
- if (activeLeaf && activeLeaf.view && (activeLeaf.view as any).editor) {
- // @ts-ignore - Obsidian 내부 API 사용
- const editorView = (activeLeaf.view as any).editor.cm;
+ const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (activeView?.editor) {
+ const editorView = (activeView.editor as any).cm;
if (editorView) {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
+ InlineModeService.setEditorView(editorView, this.settings, this.app, async (s) => { this.settings = s; await this.saveSettings(); });
Logger.log('인라인 모드: InlineModeService 키보드 스코프 초기화됨');
}
}
- // 전역 접근을 위한 참조 설정
- (window as any).InlineModeService = InlineModeService;
-
Logger.log('인라인 모드 활성화됨 (InlineModeService + 키보드 단축키)');
} catch (error) {
@@ -337,11 +312,6 @@ export default class KoreanGrammarPlugin extends Plugin {
* 인라인 모드 비활성화
*/
disableInlineMode(): void {
- // 전역 객체 정리
- if ((window as any).InlineModeService) {
- delete (window as any).InlineModeService;
- }
-
Logger.log('인라인 모드 비활성화됨');
}
@@ -393,7 +363,7 @@ export default class KoreanGrammarPlugin extends Plugin {
// 🔥 SMART FIX: 현재 문서 텍스트에 실제로 존재하는 오류만 유지
Logger.log('🔥 SMART FIX: 현재 문서 기준으로 오류 필터링');
- InlineModeService.setEditorView(currentEditorView, this.settings, this.app);
+ InlineModeService.setEditorView(currentEditorView, this.settings, this.app, async (s) => { this.settings = s; await this.saveSettings(); });
InlineModeService.filterErrorsByCurrentDocument(targetText);
// 🔧 이제 정리된 상태에서 현재 문서의 오류 상태 확인
@@ -531,8 +501,8 @@ export default class KoreanGrammarPlugin extends Plugin {
const processNotice = new Notice('📝 맞춤법 검사를 시작합니다...', 0);
// 에디터 정보 가져오기
- const activeLeaf = this.app.workspace.activeLeaf;
- if (!activeLeaf) {
+ const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!activeView) {
processNotice.hide();
new Notice('활성화된 편집기가 없습니다.');
return;
@@ -541,12 +511,11 @@ export default class KoreanGrammarPlugin extends Plugin {
try {
// 1단계: 맞춤법 검사 실행
processNotice.setMessage(`📝 ${targetText.length}자 텍스트 맞춤법 검사 중...`);
-
+
// 🔧 단일 InlineModeService 시스템 사용
- // @ts-ignore - Obsidian 내부 API 사용
- const editorView = (activeLeaf.view as any).editor?.cm;
+ const editorView = (activeView.editor as any)?.cm;
if (editorView) {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
+ InlineModeService.setEditorView(editorView, this.settings, this.app, async (s) => { this.settings = s; await this.saveSettings(); });
}
// InlineModeService를 통한 맞춤법 검사 실행
@@ -627,8 +596,8 @@ export default class KoreanGrammarPlugin extends Plugin {
* 📝 인라인 모드 일괄 적용 실행
*/
async executeInlineApplyAll(): Promise {
- const activeLeaf = this.app.workspace.activeLeaf;
- if (!activeLeaf || !activeLeaf.view || !(activeLeaf.view as any).editor) {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view?.editor) {
new Notice('활성화된 편집기가 없습니다.');
return;
}
@@ -644,10 +613,9 @@ export default class KoreanGrammarPlugin extends Plugin {
try {
// 에디터 뷰 설정
- // @ts-ignore - Obsidian 내부 API 사용
- const editorView = (activeLeaf.view as any).editor?.cm;
+ const editorView = (view.editor as any)?.cm;
if (editorView) {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
+ InlineModeService.setEditorView(editorView, this.settings, this.app, async (s) => { this.settings = s; await this.saveSettings(); });
}
// 일괄 적용 실행
@@ -673,8 +641,8 @@ export default class KoreanGrammarPlugin extends Plugin {
* 인라인 분석 결과 표시 일괄 취소 실행
*/
async executeInlineClearAll(): Promise {
- const activeLeaf = this.app.workspace.activeLeaf;
- if (!activeLeaf || !activeLeaf.view || !(activeLeaf.view as any).editor) {
+ const view = this.app.workspace.getActiveViewOfType(MarkdownView);
+ if (!view?.editor) {
new Notice('활성화된 편집기가 없습니다.');
return;
}
@@ -690,10 +658,9 @@ export default class KoreanGrammarPlugin extends Plugin {
try {
// 에디터 뷰 설정
- // @ts-ignore - Obsidian 내부 API 사용
- const editorView = (activeLeaf.view as any).editor?.cm;
+ const editorView = (view.editor as any)?.cm;
if (editorView) {
- InlineModeService.setEditorView(editorView, this.settings, this.app);
+ InlineModeService.setEditorView(editorView, this.settings, this.app, async (s) => { this.settings = s; await this.saveSettings(); });
// 모든 오류 제거
InlineModeService.clearErrors(editorView);
@@ -812,34 +779,34 @@ export default class KoreanGrammarPlugin extends Plugin {
private setupDocumentChangeListeners(): void {
// 파일 변경 감지 - 다른 파일로 이동할 때 트리거
- this.fileOpenListener = this.app.workspace.on('file-open', (file) => {
-
+ this.registerEvent(this.app.workspace.on('file-open', () => {
+
// 인라인 모드가 활성화되어 있고 오류가 있으면 상태 완전 정리
if (this.settings?.inlineMode?.enabled && InlineModeService.hasErrors()) {
Logger.log('🔧 file-open: 이전 문서의 인라인 오류 상태 완전 정리');
InlineModeService.forceCleanAllErrors();
}
-
+
// 새로운 문서에 인라인 모드 설정 (오류 상태는 깨끗한 상태에서 시작)
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView?.editor && this.settings?.inlineMode?.enabled) {
// @ts-ignore - Obsidian 내부 API 사용
const currentEditorView = (activeView as any).editor?.cm;
if (currentEditorView) {
- InlineModeService.setEditorView(currentEditorView, this.settings, this.app);
+ InlineModeService.setEditorView(currentEditorView, this.settings, this.app, async (s) => { this.settings = s; await this.saveSettings(); });
}
}
- });
+ }));
// 리프 변경 감지 - 탭 변경, 패널 변경 등을 포함한 더 광범위한 변경 감지
- this.activeLeafChangeListener = this.app.workspace.on('active-leaf-change', (leaf) => {
-
+ this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => {
+
// 인라인 모드가 활성화되어 있고 오류가 있으면 먼저 상태 정리
if (this.settings?.inlineMode?.enabled && InlineModeService.hasErrors()) {
Logger.log('🔧 active-leaf-change: 이전 탭의 인라인 오류 상태 완전 정리');
InlineModeService.forceCleanAllErrors();
}
-
+
// 마크다운 뷰로 변경되었을 때만 새로운 뷰 설정
if (leaf?.view?.getViewType() === 'markdown' && this.settings?.inlineMode?.enabled) {
const markdownView = leaf.view as MarkdownView;
@@ -847,11 +814,11 @@ export default class KoreanGrammarPlugin extends Plugin {
// @ts-ignore - Obsidian 내부 API 사용
const currentEditorView = (markdownView as any).editor?.cm;
if (currentEditorView) {
- InlineModeService.setEditorView(currentEditorView, this.settings, this.app);
+ InlineModeService.setEditorView(currentEditorView, this.settings, this.app, async (s) => { this.settings = s; await this.saveSettings(); });
}
}
}
- });
+ }));
}
}
diff --git a/src/popup/ai/TokenCalculator.ts b/src/popup/ai/TokenCalculator.ts
index 182a079..a8893a1 100644
--- a/src/popup/ai/TokenCalculator.ts
+++ b/src/popup/ai/TokenCalculator.ts
@@ -170,22 +170,22 @@ export class TokenCalculator implements IPopupServiceManager {
return new Promise((resolve) => {
// 모달 컨테이너 생성
const modal = document.createElement('div');
- modal.className = 'modal-container token-warning-modal';
+ modal.className = 'modal-container kga-token-warning-modal';
// 모달 콘텐츠
const content = document.createElement('div');
- content.className = 'modal token-warning-content';
+ content.className = 'modal kga-token-warning-content';
// DOM API를 사용한 안전한 모달 생성
- const title = content.createDiv({ cls: 'modal-title token-warning-title' });
+ const title = content.createDiv({ cls: 'modal-title kga-token-warning-title' });
title.textContent = '🚨 토큰 사용량 경고';
- const modalContent = content.createDiv({ cls: 'modal-content token-warning-body' });
+ const modalContent = content.createDiv({ cls: 'modal-content kga-token-warning-body' });
const p1 = modalContent.createEl('p');
p1.textContent = 'AI 분석에 많은 토큰이 사용될 예정입니다:';
- const infoBox = modalContent.createDiv({ cls: 'token-warning-info-box' });
+ const infoBox = modalContent.createDiv({ cls: 'kga-token-warning-info-box' });
const tokenInfo = infoBox.createDiv();
const tokenStrong = tokenInfo.createEl('strong');
@@ -198,7 +198,7 @@ export class TokenCalculator implements IPopupServiceManager {
costInfo.appendChild(document.createTextNode(` ${tokenUsage.estimatedCost}`));
if (tokenUsage.morphemeOptimized) {
- const optimizedInfo = infoBox.createDiv({ cls: 'token-warning-optimized' });
+ const optimizedInfo = infoBox.createDiv({ cls: 'kga-token-warning-optimized' });
const optimizedStrong = optimizedInfo.createEl('strong');
optimizedStrong.textContent = '✓ 형태소 최적화 적용됨';
}
@@ -206,12 +206,12 @@ export class TokenCalculator implements IPopupServiceManager {
const p2 = modalContent.createEl('p');
p2.textContent = '계속 진행하시겠습니까?';
- const buttonContainer = content.createDiv({ cls: 'modal-button-container token-warning-buttons' });
+ const buttonContainer = content.createDiv({ cls: 'modal-button-container kga-token-warning-buttons' });
- const proceedBtn = buttonContainer.createEl('button', { cls: 'mod-cta token-warning-proceed' });
+ const proceedBtn = buttonContainer.createEl('button', { cls: 'mod-cta kga-token-warning-proceed' });
proceedBtn.textContent = '진행';
- const cancelBtn = buttonContainer.createEl('button', { cls: 'token-warning-cancel' });
+ const cancelBtn = buttonContainer.createEl('button', { cls: 'kga-token-warning-cancel' });
cancelBtn.textContent = '취소';
modal.appendChild(content);
document.body.appendChild(modal);
diff --git a/src/popup/events/ClickHandler.ts b/src/popup/events/ClickHandler.ts
index 56a4b4c..704d9f3 100644
--- a/src/popup/events/ClickHandler.ts
+++ b/src/popup/events/ClickHandler.ts
@@ -138,8 +138,8 @@ export class ClickHandler {
// 네비게이션 버튼 클릭
if (target.classList.contains('nav-button') ||
- target.classList.contains('pagination-btn') ||
- target.closest('.nav-button, .pagination-btn')) {
+ target.classList.contains('kga-pagination-btn') ||
+ target.closest('.nav-button, .kga-pagination-btn')) {
return 'navigation';
}
@@ -420,7 +420,7 @@ export class ClickHandler {
// 클릭 가능한 클래스들
const clickableClasses = [
'error-text', 'error-highlight', 'suggestion-item',
- 'nav-button', 'pagination-btn', 'btn', 'toggle-btn',
+ 'nav-button', 'kga-pagination-btn', 'btn', 'toggle-btn',
'edit-btn', 'error-edit-btn'
];
diff --git a/src/popup/events/HoverHandler.ts b/src/popup/events/HoverHandler.ts
index 6bbc644..03b50dc 100644
--- a/src/popup/events/HoverHandler.ts
+++ b/src/popup/events/HoverHandler.ts
@@ -105,7 +105,7 @@ export class HoverHandler {
*/
private createTooltipContainer(): void {
this.tooltipContainer = document.createElement('div');
- this.tooltipContainer.className = 'popup-tooltip-container';
+ this.tooltipContainer.className = 'kga-popup-tooltip-container';
document.body.appendChild(this.tooltipContainer);
}
@@ -240,8 +240,8 @@ export class HoverHandler {
// 네비게이션 버튼 호버
if (target.classList.contains('nav-button') ||
- target.classList.contains('pagination-btn') ||
- target.closest('.nav-button, .pagination-btn')) {
+ target.classList.contains('kga-pagination-btn') ||
+ target.closest('.nav-button, .kga-pagination-btn')) {
return 'navigation-hint';
}
@@ -300,37 +300,37 @@ export class HoverHandler {
): Promise {
let content = '';
- let className = 'hover-tooltip';
+ let className = 'kga-hover-tooltip';
switch (actionType) {
case 'error-preview':
content = await this.createErrorPreviewContent(context);
- className = 'error-preview-tooltip';
+ className = 'kga-error-preview-tooltip';
break;
case 'suggestion-tooltip':
content = await this.createSuggestionTooltipContent(context);
- className = 'suggestion-tooltip';
+ className = 'kga-suggestion-tooltip';
break;
case 'ai-info':
content = await this.createAIInfoContent(context);
- className = 'ai-info-tooltip';
+ className = 'kga-ai-info-tooltip';
break;
case 'help-tooltip':
content = await this.createHelpTooltipContent(context);
- className = 'help-tooltip';
+ className = 'kga-help-tooltip';
break;
case 'button-hint':
content = await this.createButtonHintContent(context);
- className = 'button-hint-tooltip';
+ className = 'kga-button-hint-tooltip';
break;
case 'navigation-hint':
content = await this.createNavigationHintContent(context);
- className = 'navigation-hint-tooltip';
+ className = 'kga-navigation-hint-tooltip';
break;
default:
@@ -361,17 +361,17 @@ export class HoverHandler {
if (correctionIndex === undefined) return '';
// 기본 오류 정보
- let content = ``;
- content += `
맞춤법 오류 #${correctionIndex + 1}
`;
+ let content = `
`;
+ content += `
맞춤법 오류 #${correctionIndex + 1}
`;
// 오류 텍스트
const errorText = target.textContent || '';
if (errorText) {
- content += `
원본: "${errorText}"
`;
+ content += `
원본: "${errorText}"
`;
}
// 추가 정보 (Phase 7에서 상태 관리자와 연결 예정)
- content += `
클릭하여 수정 제안 확인
`;
+ content += `
클릭하여 수정 제안 확인
`;
content += `
`;
return content;
@@ -386,9 +386,9 @@ export class HoverHandler {
const suggestionText = target.textContent || '';
if (!suggestionText) return '';
- let content = `
`;
- content += `
"${suggestionText}"
`;
- content += `
클릭하여 이 제안 적용
`;
+ let content = `
`;
+ content += `
"${suggestionText}"
`;
+ content += `
클릭하여 이 제안 적용
`;
content += `
`;
return content;
@@ -404,15 +404,15 @@ export class HoverHandler {
const confidence = target.dataset.confidence || '';
const reasoning = target.dataset.reasoning || '';
- let content = `
`;
- content += `
🤖 AI 분석 정보
`;
+ let content = `
`;
+ content += `
🤖 AI 분석 정보
`;
if (confidence) {
- content += `
신뢰도: ${confidence}%
`;
+ content += `
신뢰도: ${confidence}%
`;
}
if (reasoning) {
- content += `
${reasoning}
`;
+ content += `
${reasoning}
`;
}
content += `
`;
@@ -429,8 +429,8 @@ export class HoverHandler {
const helpText = target.dataset.help || target.title || '';
if (!helpText) return '';
- let content = `
`;
- content += `
${helpText}
`;
+ let content = `
`;
+ content += `
${helpText}
`;
content += `
`;
return content;
@@ -445,14 +445,14 @@ export class HoverHandler {
const buttonText = target.textContent || '';
const shortcut = target.dataset.shortcut || '';
- let content = `
`;
+ let content = `
`;
if (buttonText) {
- content += `
${buttonText}
`;
+ content += `
${buttonText}
`;
}
if (shortcut) {
- content += `
단축키: ${shortcut}
`;
+ content += `
단축키: ${shortcut}
`;
}
content += `
`;
@@ -480,8 +480,8 @@ export class HoverHandler {
if (!hintText) return '';
- let content = `
`;
- content += `
${hintText}
`;
+ let content = `
`;
+ content += `
${hintText}
`;
content += `
`;
return content;
@@ -496,7 +496,7 @@ export class HoverHandler {
*/
private createTooltipElement(config: TooltipConfig): HTMLElement {
const tooltip = document.createElement('div');
- tooltip.className = `popup-tooltip ${config.className || ''} kga-dynamic-position kga-tooltip-enter`;
+ tooltip.className = `kga-popup-tooltip ${config.className || ''} kga-dynamic-position kga-tooltip-enter`;
// Set position using CSS variables
tooltip.style.setProperty('--kga-pos-left', `${config.position.x}px`);
diff --git a/src/popup/events/MobileEventHandler.ts b/src/popup/events/MobileEventHandler.ts
index d08d4e8..b658fbb 100644
--- a/src/popup/events/MobileEventHandler.ts
+++ b/src/popup/events/MobileEventHandler.ts
@@ -525,7 +525,7 @@ export class MobileEventHandler {
const input = document.createElement('input');
input.type = 'text';
input.value = target.textContent || '';
- input.className = 'error-original-input mobile-edit-input';
+ input.className = 'kga-error-original-input kga-mobile-edit-input';
// data 속성 복사
if (context.correctionIndex !== undefined) {
@@ -538,7 +538,7 @@ export class MobileEventHandler {
// 편집 컨테이너 생성
const editContainer = document.createElement('div');
- editContainer.className = 'mobile-edit-container';
+ editContainer.className = 'kga-mobile-edit-container';
editContainer.appendChild(input);
editContainer.appendChild(buttonContainer);
@@ -567,17 +567,17 @@ export class MobileEventHandler {
): HTMLElement {
const container = document.createElement('div');
- container.className = 'mobile-edit-buttons';
+ container.className = 'kga-mobile-edit-buttons';
// 완료 버튼
const confirmBtn = document.createElement('button');
confirmBtn.textContent = '✓';
- confirmBtn.className = 'mobile-edit-confirm';
+ confirmBtn.className = 'kga-mobile-edit-confirm';
// 취소 버튼
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '✕';
- cancelBtn.className = 'mobile-edit-cancel';
+ cancelBtn.className = 'kga-mobile-edit-cancel';
// 이벤트 리스너
confirmBtn.addEventListener('click', (e) => {
@@ -658,7 +658,7 @@ export class MobileEventHandler {
if (!input) return;
// 편집 컨테이너 제거
- const editContainer = input.closest('.mobile-edit-container');
+ const editContainer = input.closest('.kga-mobile-edit-container');
if (editContainer) {
editContainer.remove();
}
@@ -683,8 +683,8 @@ export class MobileEventHandler {
// 편집 중인 요소나 특정 UI 요소에서는 스크롤 방지
return target.classList.contains('error-text') ||
target.classList.contains('suggestion-item') ||
- target.classList.contains('mobile-edit-input') ||
- !!target.closest('.error-text, .suggestion-item, .mobile-edit-container');
+ target.classList.contains('kga-mobile-edit-input') ||
+ !!target.closest('.error-text, .suggestion-item, .kga-mobile-edit-container');
}
/**
@@ -695,7 +695,7 @@ export class MobileEventHandler {
const touchableClasses = [
'error-text', 'error-highlight', 'suggestion-item',
- 'nav-button', 'pagination-btn', 'btn', 'toggle-btn',
+ 'nav-button', 'kga-pagination-btn', 'btn', 'toggle-btn',
'edit-btn'
];
diff --git a/src/popup/events/PopupEventManager.ts b/src/popup/events/PopupEventManager.ts
index ef30d65..9f599d4 100644
--- a/src/popup/events/PopupEventManager.ts
+++ b/src/popup/events/PopupEventManager.ts
@@ -298,7 +298,7 @@ export class PopupEventManager implements IPopupServiceManager {
}
// 네비게이션 버튼 클릭
- this.addEventRule('.nav-button, .pagination-btn', 'click', async (event, context) => {
+ this.addEventRule('.nav-button, .kga-pagination-btn', 'click', async (event, context) => {
Logger.debug('네비게이션 버튼 클릭됨');
return await this.handleNavigationClick(event, context);
});
diff --git a/src/popup/keyboard/FocusManager.ts b/src/popup/keyboard/FocusManager.ts
index 08825eb..2637565 100644
--- a/src/popup/keyboard/FocusManager.ts
+++ b/src/popup/keyboard/FocusManager.ts
@@ -30,9 +30,9 @@ export class FocusManager implements IPopupComponent {
private focusObserver?: MutationObserver;
// CSS 클래스 상수
- private readonly FOCUS_CLASS = 'keyboard-focused';
- private readonly FOCUS_HIGHLIGHT_CLASS = 'focus-highlight';
- private readonly EDIT_MODE_CLASS = 'edit-mode-active';
+ private readonly FOCUS_CLASS = 'kga-keyboard-focused';
+ private readonly FOCUS_HIGHLIGHT_CLASS = 'kga-focus-highlight';
+ private readonly EDIT_MODE_CLASS = 'kga-edit-mode-active';
constructor() {
this.focusState = this.createInitialFocusState();
@@ -55,7 +55,7 @@ export class FocusManager implements IPopupComponent {
}
render(): HTMLElement {
- const container = createEl('div', { cls: 'focus-manager' });
+ const container = createEl('div', { cls: 'kga-focus-manager' });
this.containerElement = container;
// 포커스 하이라이트용 스타일 추가
@@ -409,7 +409,7 @@ export class FocusManager implements IPopupComponent {
100% { opacity: 1; }
}
- .${this.FOCUS_CLASS}.focus-pulse {
+ .${this.FOCUS_CLASS}.kga-focus-pulse {
animation: focusPulse 1s ease-in-out infinite;
}
`;
diff --git a/src/popup/keyboard/KeyboardManager.ts b/src/popup/keyboard/KeyboardManager.ts
index ef360a1..22e8312 100644
--- a/src/popup/keyboard/KeyboardManager.ts
+++ b/src/popup/keyboard/KeyboardManager.ts
@@ -64,7 +64,7 @@ export class KeyboardManager implements IPopupComponent {
}
render(): HTMLElement {
- const container = createEl('div', { cls: 'keyboard-manager' });
+ const container = createEl('div', { cls: 'kga-keyboard-manager' });
this.containerElement = container;
// 키보드 힌트 표시 (데스크톱에서만)
@@ -337,7 +337,7 @@ export class KeyboardManager implements IPopupComponent {
}
private createKeyboardHints(): HTMLElement {
- const hintsContainer = createEl('div', { cls: 'keyboard-hints' });
+ const hintsContainer = createEl('div', { cls: 'kga-keyboard-hints' });
const hints = [
{ key: 'Tab', desc: '다음 오류' },
@@ -349,12 +349,12 @@ export class KeyboardManager implements IPopupComponent {
];
hints.forEach(hint => {
- const hintElement = createEl('div', { cls: 'keyboard-hint' });
+ const hintElement = createEl('div', { cls: 'kga-keyboard-hint' });
- const keyElement = createEl('kbd', { cls: 'keyboard-hint-key' });
+ const keyElement = createEl('kbd', { cls: 'kga-keyboard-hint-key' });
keyElement.textContent = hint.key;
- const descElement = createEl('span', { cls: 'keyboard-hint-desc' });
+ const descElement = createEl('span', { cls: 'kga-keyboard-hint-desc' });
descElement.textContent = hint.desc;
hintElement.appendChild(keyElement);
@@ -370,7 +370,7 @@ export class KeyboardManager implements IPopupComponent {
const container = this.containerElement;
if (!container) return;
- const hintsContainer = container.querySelector('.keyboard-hints');
+ const hintsContainer = container.querySelector('.kga-keyboard-hints');
if (hintsContainer && !this.shouldShowKeyboardHints()) {
hintsContainer.remove();
} else if (!hintsContainer && this.shouldShowKeyboardHints()) {
diff --git a/src/popup/layout/HeaderRenderer.ts b/src/popup/layout/HeaderRenderer.ts
index b842087..2661203 100644
--- a/src/popup/layout/HeaderRenderer.ts
+++ b/src/popup/layout/HeaderRenderer.ts
@@ -354,9 +354,9 @@ export class HeaderRenderer implements IPopupComponent {
// 활성화/비활성화 상태 클래스 추가
if (options.disabled) {
- button.classList.add('header-button-disabled');
+ button.classList.add('kga-header-button-disabled');
} else {
- button.classList.add('header-button-enabled');
+ button.classList.add('kga-header-button-enabled');
}
// 아이콘 추가
@@ -443,12 +443,12 @@ export class HeaderRenderer implements IPopupComponent {
// 버튼 상태 업데이트
if (this.isAiAnalyzing) {
this.aiButtonElement.setAttribute('disabled', 'true');
- this.aiButtonElement.classList.add('header-button-disabled');
- this.aiButtonElement.classList.remove('header-button-enabled');
+ this.aiButtonElement.classList.add('kga-header-button-disabled');
+ this.aiButtonElement.classList.remove('kga-header-button-enabled');
} else {
this.aiButtonElement.removeAttribute('disabled');
- this.aiButtonElement.classList.remove('header-button-disabled');
- this.aiButtonElement.classList.add('header-button-enabled');
+ this.aiButtonElement.classList.remove('kga-header-button-disabled');
+ this.aiButtonElement.classList.add('kga-header-button-enabled');
}
// 아이콘 애니메이션 (분석 중일 때)
@@ -593,12 +593,12 @@ export class HeaderRenderer implements IPopupComponent {
if (enabled) {
button.removeAttribute('disabled');
- button.classList.remove('header-button-disabled');
- button.classList.add('header-button-enabled');
+ button.classList.remove('kga-header-button-disabled');
+ button.classList.add('kga-header-button-enabled');
} else {
button.setAttribute('disabled', 'true');
- button.classList.add('header-button-disabled');
- button.classList.remove('header-button-enabled');
+ button.classList.add('kga-header-button-disabled');
+ button.classList.remove('kga-header-button-enabled');
}
Logger.debug('HeaderRenderer: 버튼 활성화 상태 변경', { type, enabled });
diff --git a/src/popup/layout/PreviewRenderer.ts b/src/popup/layout/PreviewRenderer.ts
index fb00be0..cf9f346 100644
--- a/src/popup/layout/PreviewRenderer.ts
+++ b/src/popup/layout/PreviewRenderer.ts
@@ -853,7 +853,7 @@ export class PreviewRenderer implements IPopupComponent {
span.textContent = segment.text;
if (segment.correctionIndex !== undefined) {
- span.className = `clickable-error ${segment.className || ''}`;
+ span.className = `kga-clickable-error ${segment.className || ''}`;
span.dataset.correctionIndex = segment.correctionIndex.toString();
span.dataset.uniqueId = segment.uniqueId || '';
span.setAttribute('tabindex', '0');
diff --git a/src/popup/ui/ComponentManager.ts b/src/popup/ui/ComponentManager.ts
index 039a0b8..401fe05 100644
--- a/src/popup/ui/ComponentManager.ts
+++ b/src/popup/ui/ComponentManager.ts
@@ -286,7 +286,7 @@ export class ComponentManager {
correction: PageCorrection,
renderContext: RenderContext
): Promise
{
- const template = this.templates.get('error-card');
+ const template = this.templates.get('kga-error-card');
if (!template) {
return this.createBasicSummaryComponent(correction, renderContext);
}
@@ -302,7 +302,7 @@ export class ComponentManager {
renderContext: RenderContext
): HTMLElement {
const card = createEl('div', {
- cls: 'error-card',
+ cls: 'kga-error-card',
attr: {
'data-correction-index': correction.originalIndex.toString(),
'role': 'button',
@@ -313,21 +313,21 @@ export class ComponentManager {
// 상태 표시기
const stateIndicator = createEl('div', {
- cls: 'error-state-indicator',
+ cls: 'kga-error-state-indicator',
text: this.getStateDisplayText(renderContext.state)
});
card.appendChild(stateIndicator);
// 원본 텍스트
const originalText = createEl('div', {
- cls: 'error-original-text',
+ cls: 'kga-error-original-text',
text: correction.correction.original
});
card.appendChild(originalText);
// 수정 제안
if (correction.correction.suggestions && correction.correction.suggestions.length > 0) {
- const suggestions = createEl('div', { cls: 'error-suggestions' });
+ const suggestions = createEl('div', { cls: 'kga-error-suggestions' });
correction.correction.suggestions.forEach((suggestion, index) => {
const suggestionSpan = createEl('span', {
cls: 'suggestion-item',
@@ -511,13 +511,13 @@ export class ComponentManager {
*/
private initializeTemplates(): void {
// 오류 카드 템플릿
- this.templates.set('error-card', {
- name: 'error-card',
+ this.templates.set('kga-error-card', {
+ name: 'kga-error-card',
html: `
-
-
{{stateText}}
-
{{originalText}}
-
+
+
{{stateText}}
+
{{originalText}}
+
`,
bindings: {}
diff --git a/src/popup/ui/InteractionHandler.ts b/src/popup/ui/InteractionHandler.ts
index 2fce9b5..68da46c 100644
--- a/src/popup/ui/InteractionHandler.ts
+++ b/src/popup/ui/InteractionHandler.ts
@@ -170,9 +170,9 @@ export class InteractionHandler {
}
// 상태 표시 업데이트
- const stateIndicator = summaryElement.querySelector('.error-state-indicator');
+ const stateIndicator = summaryElement.querySelector('.kga-error-state-indicator');
if (stateIndicator) {
- stateIndicator.className = `error-state-indicator state-${newState}`;
+ stateIndicator.className = `kga-error-state-indicator state-${newState}`;
stateIndicator.textContent = this.getStateDisplayText(newState);
}
diff --git a/src/services/inlineModeService.ts b/src/services/inlineModeService.ts
index 7345a9d..3a0af36 100644
--- a/src/services/inlineModeService.ts
+++ b/src/services/inlineModeService.ts
@@ -1,6 +1,6 @@
import { EditorView, WidgetType, Decoration, DecorationSet } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
-import { Correction, InlineError, ExtendedWindow, PluginInstance, PluginSettings } from '../types/interfaces';
+import { Correction, InlineError, ExtendedWindow, PluginSettings } from '../types/interfaces';
import { Logger } from '../utils/logger';
import { globalInlineTooltip } from '../ui/inlineTooltip';
import { Scope, App, Platform } from 'obsidian';
@@ -500,13 +500,14 @@ export class InlineModeService {
// 🔧 레거시: 기존 키보드 스코프 방식 (Command Palette 방식으로 대체됨)
// private static keyboardScope: Scope | null = null;
private static app: App | null = null;
+ private static saveSettingsCallback: ((settings: PluginSettings) => Promise
) | null = null;
private static currentHoveredError: InlineError | null = null;
private static hoverTimeout: NodeJS.Timeout | null = null;
/**
* 에디터 뷰 및 설정 초기화
*/
- static setEditorView(view: EditorView, settings?: PluginSettings, app?: App): void {
+ static setEditorView(view: EditorView, settings?: PluginSettings, app?: App, saveSettingsCallback?: (settings: PluginSettings) => Promise): void {
// 🔧 새로운 에디터뷰가 이전과 다르면 이전 상태 완전 정리
if (this.currentView && this.currentView !== view) {
Logger.debug('인라인 모드: 이전 에디터뷰와 다름 - 상태 정리 중');
@@ -521,7 +522,10 @@ export class InlineModeService {
if (app) {
this.app = app;
}
-
+ if (saveSettingsCallback) {
+ this.saveSettingsCallback = saveSettingsCallback;
+ }
+
// 이벤트 리스너 추가
this.setupEventListeners(view);
@@ -2188,7 +2192,6 @@ export class InlineModeService {
* 인라인 모드 명령어 등록 (Command Palette 방식)
*/
static registerCommands(plugin: any): void {
- Logger.log('🎹 인라인 모드: 명령어 등록 시작');
// 다음 오류로 이동
plugin.addCommand({
@@ -2402,17 +2405,6 @@ export class InlineModeService {
}
}
});
-
- Logger.log('🎹 인라인 모드: 명령어 등록 완료!');
- Logger.log('📋 등록된 명령어:');
- Logger.log(' • Korean Grammar Assistant: 다음 문법 오류로 이동');
- Logger.log(' • Korean Grammar Assistant: 이전 문법 오류로 이동');
- Logger.log(' • Korean Grammar Assistant: 다음 제안 선택');
- Logger.log(' • Korean Grammar Assistant: 이전 제안 선택');
- Logger.log(' • Korean Grammar Assistant: 선택된 제안 적용');
- Logger.log(' • Korean Grammar Assistant: 문법 오류 포커스 해제');
- Logger.log(' • Korean Grammar Assistant: 한국어 문법 인라인 모드 토글');
- Logger.log('💡 Command Palette (Cmd+P)에서 검색하거나 Hotkeys에서 단축키를 설정하세요!');
}
/**
@@ -2945,9 +2937,7 @@ export class InlineModeService {
});
// AI 분석 서비스가 있는지 확인
- const aiService = getExtendedWindow().koreanGrammarPlugin?.instance;
-
- if (!aiService) {
+ if (!this.settings) {
throw new Error('AI 분석 서비스를 찾을 수 없습니다.');
}
@@ -3219,11 +3209,10 @@ export class InlineModeService {
updatedSettings = IgnoredWordsService.addIgnoredWord(word, updatedSettings);
}
- // 설정 업데이트 (플러그인 인스턴스를 통해)
- if ((window as any).koreanGrammarPlugin?.instance) {
- const plugin = (window as any).koreanGrammarPlugin.instance;
- plugin.settings = updatedSettings;
- await plugin.saveSettings();
+ // 설정 업데이트 (saveSettingsCallback을 통해)
+ if (this.saveSettingsCallback) {
+ this.settings = updatedSettings;
+ await this.saveSettingsCallback(updatedSettings);
Logger.log(`🔵 예외처리 사전 등록: ${wordsToIgnore.join(', ')}`);
}
}
@@ -3305,18 +3294,10 @@ export class InlineModeService {
// 1. 예외처리 사전에 단어 추가
const updatedSettings = IgnoredWordsService.addIgnoredWord(trimmedWord, this.settings);
- // 2. 설정 저장
- const pluginWrapper = getExtendedWindow().koreanGrammarPlugin;
- if (pluginWrapper && typeof pluginWrapper === 'object' && 'instance' in pluginWrapper) {
- const plugin = pluginWrapper.instance;
- if (plugin && typeof plugin === 'object' && 'saveSettings' in plugin) {
- const saveMethod = (plugin as any).saveSettings;
- if (typeof saveMethod === 'function') {
- (plugin as any).settings = updatedSettings;
- await saveMethod.call(plugin);
- }
- }
- this.settings = updatedSettings; // 로컬 설정도 업데이트
+ // 2. 설정 저장 (saveSettingsCallback을 통해)
+ if (this.saveSettingsCallback) {
+ this.settings = updatedSettings;
+ await this.saveSettingsCallback(updatedSettings);
Logger.debug(`🔵 예외처리 사전에 저장됨: "${trimmedWord}"`);
}
diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts
index 9419bb9..003a24e 100644
--- a/src/types/interfaces.ts
+++ b/src/types/interfaces.ts
@@ -308,9 +308,6 @@ export interface ExtendedWindow extends Window {
isHovered?: boolean;
};
tooltipKeepOpenMode?: boolean;
- koreanGrammarPlugin?: {
- instance?: unknown;
- };
Notice?: new (message: string, timeout?: number) => void;
sanitizeHTMLToDom?: (html: string) => DocumentFragment;
getEventListeners?: (element: Element) => Record;
@@ -351,11 +348,3 @@ export interface EventContext {
platform?: 'desktop' | 'mobile';
}
-/**
- * Obsidian Plugin 인터페이스 확장
- */
-export interface PluginInstance {
- settings?: PluginSettings;
- orchestrator?: unknown;
- instance?: unknown;
-}
\ No newline at end of file
diff --git a/src/ui/correctionPopup.ts b/src/ui/correctionPopup.ts
index aabc2f7..9fb0662 100644
--- a/src/ui/correctionPopup.ts
+++ b/src/ui/correctionPopup.ts
@@ -85,7 +85,7 @@ export class CorrectionPopup extends BaseComponent {
// Enter: 현재 선택된 수정사항 적용 (편집 중이 아닐 때만)
this.keyboardScope.register([], 'Enter', (evt: KeyboardEvent) => {
const target = evt.target as HTMLElement;
- if (target && (target.dataset?.editMode === 'true' || target.classList.contains('error-original-input'))) {
+ if (target && (target.dataset?.editMode === 'true' || target.classList.contains('kga-error-original-input'))) {
// 편집 중인 input 요소에서는 기본 동작 허용
Logger.debug('Enter key in edit mode - allowing default behavior');
return true;
@@ -98,7 +98,7 @@ export class CorrectionPopup extends BaseComponent {
// Escape: 팝업 닫기 (편집 중이 아닐 때만)
this.keyboardScope.register([], 'Escape', (evt: KeyboardEvent) => {
const target = evt.target as HTMLElement;
- if (target && (target.dataset?.editMode === 'true' || target.classList.contains('error-original-input'))) {
+ if (target && (target.dataset?.editMode === 'true' || target.classList.contains('kga-error-original-input'))) {
// 편집 중인 input 요소에서는 기본 동작 허용
Logger.debug('Escape key in edit mode - allowing default behavior');
return true;
@@ -469,9 +469,9 @@ export class CorrectionPopup extends BaseComponent {
Logger.debug(`currentFocusIndex: ${this.currentFocusIndex}`);
// 기존 포커스 하이라이트 제거
- const prevFocused = this.element.querySelectorAll('.keyboard-focused');
+ const prevFocused = this.element.querySelectorAll('.kga-keyboard-focused');
Logger.debug(`기존 포커스 요소 ${prevFocused.length}개 제거`);
- prevFocused.forEach(el => el.removeClass('keyboard-focused'));
+ prevFocused.forEach(el => el.removeClass('kga-keyboard-focused'));
// 현재 포커스 항목 하이라이트
if (this.currentCorrections.length > 0 &&
@@ -500,7 +500,7 @@ export class CorrectionPopup extends BaseComponent {
}
if (errorItem) {
- errorItem.addClass('keyboard-focused');
+ errorItem.addClass('kga-keyboard-focused');
// 스크롤하여 보이게 하기
errorItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
Logger.debug(`포커스 하이라이트 적용 성공: 고유 ID ${uniqueId}, 원본 인덱스 ${actualIndex}, 절대 위치 ${pageCorrection.absolutePosition}`);
@@ -588,40 +588,40 @@ export class CorrectionPopup extends BaseComponent {
this.element.empty();
// Popup overlay
- const overlay = this.element.createDiv('popup-overlay');
+ const overlay = this.element.createDiv('kga-popup-overlay');
// Popup content
- const content = this.element.createDiv('popup-content');
+ const content = this.element.createDiv('kga-popup-content');
// Header
- const header = content.createDiv('header');
+ const header = content.createDiv('kga-header');
new Setting(header).setName('한국어 맞춤법 검사').setHeading();
- const headerTop = header.createDiv('preview-header-top');
+ const headerTop = header.createDiv('kga-preview-header-top');
// AI 분석 버튼 (항상 표시, 상태에 따라 활성화/비활성화)
const aiBtn = headerTop.createEl('button', {
- cls: 'ai-analyze-btn',
+ cls: 'kga-ai-analyze-btn',
attr: { id: 'aiAnalyzeBtn' }
});
// AI 서비스 상태에 따른 버튼 설정
this.updateAiButtonState(aiBtn);
- headerTop.createEl('button', { cls: 'close-btn-header', text: '×' });
+ headerTop.createEl('button', { cls: 'kga-close-btn-header', text: '×' });
// Main content
- const mainContent = content.createDiv('content');
+ const mainContent = content.createDiv('kga-content');
// Preview section
- const previewSection = mainContent.createDiv('preview-section');
- const previewHeader = previewSection.createDiv('preview-header');
+ const previewSection = mainContent.createDiv('kga-preview-section');
+ const previewHeader = previewSection.createDiv('kga-preview-header');
- const previewLabel = previewHeader.createDiv('preview-label');
+ const previewLabel = previewHeader.createDiv('kga-preview-label');
previewLabel.createSpan({ text: '미리보기' });
- previewLabel.createSpan({ cls: 'preview-hint', text: '클릭하여 수정사항 적용' });
+ previewLabel.createSpan({ cls: 'kga-preview-hint', text: '클릭하여 수정사항 적용' });
// Color legend
- const colorLegend = previewHeader.createDiv('color-legend');
+ const colorLegend = previewHeader.createDiv('kga-color-legend');
const legendItems = [
{ cls: 'error', text: '오류' },
{ cls: 'corrected', text: '수정' },
@@ -631,8 +631,8 @@ export class CorrectionPopup extends BaseComponent {
];
legendItems.forEach(item => {
- const legendItem = colorLegend.createDiv('color-legend-item');
- legendItem.createDiv(`color-legend-dot ${item.cls}`);
+ const legendItem = colorLegend.createDiv('kga-color-legend-item');
+ legendItem.createDiv(`kga-color-legend-dot ${item.cls}`);
legendItem.createSpan({ text: item.text });
});
@@ -641,35 +641,35 @@ export class CorrectionPopup extends BaseComponent {
this.createPaginationElement(paginationDiv);
// Preview content
- const previewContent = previewSection.createDiv('preview-text');
+ const previewContent = previewSection.createDiv('kga-preview-text');
previewContent.id = 'resultPreview';
previewContent.createEl('span', { text: this.config.selectedText.trim() });
// Error summary
- const errorSummary = mainContent.createDiv('error-summary collapsed');
+ const errorSummary = mainContent.createDiv('kga-error-summary collapsed');
errorSummary.id = 'errorSummary';
- const errorToggle = errorSummary.createDiv('error-summary-toggle');
- const leftSection = errorToggle.createDiv('left-section');
- leftSection.createSpan({ cls: 'error-summary-label', text: '오류 상세' });
+ const errorToggle = errorSummary.createDiv('kga-error-summary-toggle');
+ const leftSection = errorToggle.createDiv('kga-left-section');
+ leftSection.createSpan({ cls: 'kga-error-summary-label', text: '오류 상세' });
const badge = leftSection.createSpan({
- cls: 'error-count-badge',
+ cls: 'kga-error-count-badge',
text: this.getErrorStateCount().toString(),
attr: { id: 'errorCountBadge' }
});
- errorToggle.createSpan({ cls: 'toggle-icon', text: '▼' });
+ errorToggle.createSpan({ cls: 'kga-toggle-icon', text: '▼' });
- const errorContent = errorSummary.createDiv('error-summary-content');
+ const errorContent = errorSummary.createDiv('kga-error-summary-content');
errorContent.id = 'errorSummaryContent';
// Error summary content
this.createErrorSummaryElement(errorContent);
// Button area
- const buttonArea = content.createDiv('button-area');
- buttonArea.createEl('button', { cls: 'cancel-btn', text: '취소' });
- buttonArea.createEl('button', {
- cls: 'apply-btn',
+ const buttonArea = content.createDiv('kga-button-area');
+ buttonArea.createEl('button', { cls: 'kga-cancel-btn', text: '취소' });
+ buttonArea.createEl('button', {
+ cls: 'kga-apply-btn',
text: '적용',
attr: { id: 'applyCorrectionsButton' }
});
@@ -683,17 +683,17 @@ export class CorrectionPopup extends BaseComponent {
container.empty();
if (!this.isLongText || this.totalPreviewPages <= 1) {
- const hiddenContainer = container.createDiv('pagination-container-hidden');
+ const hiddenContainer = container.createDiv('kga-pagination-container-hidden');
hiddenContainer.id = 'paginationContainer';
return;
}
- const paginationControls = container.createDiv('pagination-controls');
+ const paginationControls = container.createDiv('kga-pagination-controls');
paginationControls.id = 'paginationContainer';
// Previous button
const prevBtn = paginationControls.createEl('button', {
- cls: 'pagination-btn',
+ cls: 'kga-pagination-btn',
text: '이전'
});
prevBtn.id = 'prevPreviewPage';
@@ -702,13 +702,13 @@ export class CorrectionPopup extends BaseComponent {
}
// Page info
- const pageInfo = paginationControls.createSpan('page-info');
+ const pageInfo = paginationControls.createSpan('kga-page-info');
pageInfo.id = 'previewPageInfo';
pageInfo.textContent = `${this.currentPreviewPage + 1} / ${this.totalPreviewPages}`;
// Next button
const nextBtn = paginationControls.createEl('button', {
- cls: 'pagination-btn',
+ cls: 'kga-pagination-btn',
text: '다음'
});
nextBtn.id = 'nextPreviewPage';
@@ -717,7 +717,7 @@ export class CorrectionPopup extends BaseComponent {
}
// Page chars info
- const pageCharsInfo = paginationControls.createSpan('page-chars-info');
+ const pageCharsInfo = paginationControls.createSpan('kga-page-chars-info');
pageCharsInfo.id = 'pageCharsInfo';
pageCharsInfo.textContent = `${this.charsPerPage}자`;
}
@@ -728,15 +728,15 @@ export class CorrectionPopup extends BaseComponent {
*/
private createPaginationHTML(): string {
if (!this.isLongText || this.totalPreviewPages <= 1) {
- return '';
+ return '';
}
return `
-