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 ` -
- - ${this.currentPreviewPage + 1} / ${this.totalPreviewPages} - - ${this.charsPerPage}\uC790 -
- `; - } - /** - * 현재 페이지의 교정 목록을 가져옵니다. (절대 위치 기반 정확한 순서) - */ - 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 = `