@impakers/debug 1.4.6 → 1.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/next.d.mts CHANGED
@@ -10,6 +10,10 @@ interface ImpakersDebugOptions {
10
10
  * false로 설정하면 소스코드가 .map에 그대로 노출됨
11
11
  */
12
12
  stripSourceContent?: boolean;
13
+ /**
14
+ * Next App Router route -> file manifest 생성 여부 (기본: true)
15
+ */
16
+ emitRouteManifest?: boolean;
13
17
  }
14
18
  /**
15
19
  * Next.js config wrapper — 소스맵 설정 자동 구성
package/dist/next.d.ts CHANGED
@@ -10,6 +10,10 @@ interface ImpakersDebugOptions {
10
10
  * false로 설정하면 소스코드가 .map에 그대로 노출됨
11
11
  */
12
12
  stripSourceContent?: boolean;
13
+ /**
14
+ * Next App Router route -> file manifest 생성 여부 (기본: true)
15
+ */
16
+ emitRouteManifest?: boolean;
13
17
  }
14
18
  /**
15
19
  * Next.js config wrapper — 소스맵 설정 자동 구성
package/dist/next.js CHANGED
@@ -89,8 +89,127 @@ var StripSourceContentPlugin = class {
89
89
  );
90
90
  }
91
91
  };
92
+ var RouteManifestPlugin = class {
93
+ constructor(projectDir) {
94
+ this.projectDir = projectDir;
95
+ }
96
+ apply(compiler) {
97
+ compiler.hooks.afterEmit.tapAsync(
98
+ "ImpakersDebugRouteManifest",
99
+ (compilation, callback) => {
100
+ const fs = require("fs");
101
+ const path = require("path");
102
+ const outputPath = compilation.outputOptions?.path;
103
+ if (!outputPath) {
104
+ callback();
105
+ return;
106
+ }
107
+ const appDir = findAppDirectory(fs, path, this.projectDir);
108
+ if (!appDir) {
109
+ callback();
110
+ return;
111
+ }
112
+ const entries = scanRouteManifestEntries(fs, path, this.projectDir, appDir);
113
+ if (entries.length === 0) {
114
+ callback();
115
+ return;
116
+ }
117
+ const manifestPath = path.resolve(
118
+ outputPath,
119
+ "../static/chunks/impakers-debug-route-manifest.json"
120
+ );
121
+ try {
122
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
123
+ fs.writeFileSync(
124
+ manifestPath,
125
+ JSON.stringify({ version: 1, entries }, null, 2),
126
+ "utf-8"
127
+ );
128
+ console.log(
129
+ `[@impakers/debug] Emitted route manifest with ${entries.length} route entries`
130
+ );
131
+ } catch {
132
+ }
133
+ callback();
134
+ }
135
+ );
136
+ }
137
+ };
138
+ function findAppDirectory(fs, path, projectDir) {
139
+ const candidates = [
140
+ path.join(projectDir, "src", "app"),
141
+ path.join(projectDir, "app")
142
+ ];
143
+ for (const candidate of candidates) {
144
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
145
+ return candidate;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ function toPosixPath(path) {
151
+ return path.replace(/\\/g, "/");
152
+ }
153
+ function findRouteFile(fs, path, dir, baseName) {
154
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
155
+ for (const ext of extensions) {
156
+ const file = path.join(dir, `${baseName}${ext}`);
157
+ if (fs.existsSync(file) && fs.statSync(file).isFile()) {
158
+ return file;
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+ function getUrlSegment(segmentName) {
164
+ if (!segmentName) return null;
165
+ if (segmentName.startsWith("(") && segmentName.endsWith(")")) return null;
166
+ return segmentName;
167
+ }
168
+ function routeFromSegments(segments) {
169
+ if (segments.length === 0) return "/";
170
+ return `/${segments.join("/")}`;
171
+ }
172
+ function scanRouteManifestEntries(fs, path, projectDir, appDir) {
173
+ const entries = [];
174
+ const walk = (currentDir, routeSegments, layoutChain) => {
175
+ const currentLayout = findRouteFile(fs, path, currentDir, "layout");
176
+ const nextLayoutChain = currentLayout ? [
177
+ ...layoutChain,
178
+ {
179
+ kind: "layout",
180
+ file: toPosixPath(path.relative(projectDir, currentLayout))
181
+ }
182
+ ] : layoutChain;
183
+ const currentPage = findRouteFile(fs, path, currentDir, "page");
184
+ if (currentPage) {
185
+ entries.push({
186
+ route: routeFromSegments(routeSegments),
187
+ segments: [...routeSegments],
188
+ files: [
189
+ {
190
+ kind: "page",
191
+ file: toPosixPath(path.relative(projectDir, currentPage))
192
+ },
193
+ ...nextLayoutChain
194
+ ]
195
+ });
196
+ }
197
+ const children = fs.readdirSync(currentDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
198
+ for (const child of children) {
199
+ if (child.name.startsWith("@")) continue;
200
+ const nextSegment = getUrlSegment(child.name);
201
+ walk(
202
+ path.join(currentDir, child.name),
203
+ nextSegment ? [...routeSegments, nextSegment] : routeSegments,
204
+ nextLayoutChain
205
+ );
206
+ }
207
+ };
208
+ walk(appDir, [], []);
209
+ return entries;
210
+ }
92
211
  function withImpakersDebug(nextConfig, options = {}) {
93
- const { stripSourceContent = true } = options;
212
+ const { stripSourceContent = true, emitRouteManifest = true } = options;
94
213
  return {
95
214
  ...nextConfig,
96
215
  // 소스맵 생성 활성화
@@ -99,9 +218,14 @@ function withImpakersDebug(nextConfig, options = {}) {
99
218
  if (typeof nextConfig.webpack === "function") {
100
219
  config = nextConfig.webpack(config, context);
101
220
  }
102
- if (!context.isServer && stripSourceContent) {
221
+ if (!context.isServer) {
103
222
  config.plugins = config.plugins || [];
104
- config.plugins.push(new StripSourceContentPlugin());
223
+ if (stripSourceContent) {
224
+ config.plugins.push(new StripSourceContentPlugin());
225
+ }
226
+ if (emitRouteManifest && context.dir) {
227
+ config.plugins.push(new RouteManifestPlugin(context.dir));
228
+ }
105
229
  }
106
230
  return config;
107
231
  }
package/dist/next.mjs CHANGED
@@ -72,8 +72,127 @@ var StripSourceContentPlugin = class {
72
72
  );
73
73
  }
74
74
  };
75
+ var RouteManifestPlugin = class {
76
+ constructor(projectDir) {
77
+ this.projectDir = projectDir;
78
+ }
79
+ apply(compiler) {
80
+ compiler.hooks.afterEmit.tapAsync(
81
+ "ImpakersDebugRouteManifest",
82
+ (compilation, callback) => {
83
+ const fs = __require("fs");
84
+ const path = __require("path");
85
+ const outputPath = compilation.outputOptions?.path;
86
+ if (!outputPath) {
87
+ callback();
88
+ return;
89
+ }
90
+ const appDir = findAppDirectory(fs, path, this.projectDir);
91
+ if (!appDir) {
92
+ callback();
93
+ return;
94
+ }
95
+ const entries = scanRouteManifestEntries(fs, path, this.projectDir, appDir);
96
+ if (entries.length === 0) {
97
+ callback();
98
+ return;
99
+ }
100
+ const manifestPath = path.resolve(
101
+ outputPath,
102
+ "../static/chunks/impakers-debug-route-manifest.json"
103
+ );
104
+ try {
105
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
106
+ fs.writeFileSync(
107
+ manifestPath,
108
+ JSON.stringify({ version: 1, entries }, null, 2),
109
+ "utf-8"
110
+ );
111
+ console.log(
112
+ `[@impakers/debug] Emitted route manifest with ${entries.length} route entries`
113
+ );
114
+ } catch {
115
+ }
116
+ callback();
117
+ }
118
+ );
119
+ }
120
+ };
121
+ function findAppDirectory(fs, path, projectDir) {
122
+ const candidates = [
123
+ path.join(projectDir, "src", "app"),
124
+ path.join(projectDir, "app")
125
+ ];
126
+ for (const candidate of candidates) {
127
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
128
+ return candidate;
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ function toPosixPath(path) {
134
+ return path.replace(/\\/g, "/");
135
+ }
136
+ function findRouteFile(fs, path, dir, baseName) {
137
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
138
+ for (const ext of extensions) {
139
+ const file = path.join(dir, `${baseName}${ext}`);
140
+ if (fs.existsSync(file) && fs.statSync(file).isFile()) {
141
+ return file;
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+ function getUrlSegment(segmentName) {
147
+ if (!segmentName) return null;
148
+ if (segmentName.startsWith("(") && segmentName.endsWith(")")) return null;
149
+ return segmentName;
150
+ }
151
+ function routeFromSegments(segments) {
152
+ if (segments.length === 0) return "/";
153
+ return `/${segments.join("/")}`;
154
+ }
155
+ function scanRouteManifestEntries(fs, path, projectDir, appDir) {
156
+ const entries = [];
157
+ const walk = (currentDir, routeSegments, layoutChain) => {
158
+ const currentLayout = findRouteFile(fs, path, currentDir, "layout");
159
+ const nextLayoutChain = currentLayout ? [
160
+ ...layoutChain,
161
+ {
162
+ kind: "layout",
163
+ file: toPosixPath(path.relative(projectDir, currentLayout))
164
+ }
165
+ ] : layoutChain;
166
+ const currentPage = findRouteFile(fs, path, currentDir, "page");
167
+ if (currentPage) {
168
+ entries.push({
169
+ route: routeFromSegments(routeSegments),
170
+ segments: [...routeSegments],
171
+ files: [
172
+ {
173
+ kind: "page",
174
+ file: toPosixPath(path.relative(projectDir, currentPage))
175
+ },
176
+ ...nextLayoutChain
177
+ ]
178
+ });
179
+ }
180
+ const children = fs.readdirSync(currentDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
181
+ for (const child of children) {
182
+ if (child.name.startsWith("@")) continue;
183
+ const nextSegment = getUrlSegment(child.name);
184
+ walk(
185
+ path.join(currentDir, child.name),
186
+ nextSegment ? [...routeSegments, nextSegment] : routeSegments,
187
+ nextLayoutChain
188
+ );
189
+ }
190
+ };
191
+ walk(appDir, [], []);
192
+ return entries;
193
+ }
75
194
  function withImpakersDebug(nextConfig, options = {}) {
76
- const { stripSourceContent = true } = options;
195
+ const { stripSourceContent = true, emitRouteManifest = true } = options;
77
196
  return {
78
197
  ...nextConfig,
79
198
  // 소스맵 생성 활성화
@@ -82,9 +201,14 @@ function withImpakersDebug(nextConfig, options = {}) {
82
201
  if (typeof nextConfig.webpack === "function") {
83
202
  config = nextConfig.webpack(config, context);
84
203
  }
85
- if (!context.isServer && stripSourceContent) {
204
+ if (!context.isServer) {
86
205
  config.plugins = config.plugins || [];
87
- config.plugins.push(new StripSourceContentPlugin());
206
+ if (stripSourceContent) {
207
+ config.plugins.push(new StripSourceContentPlugin());
208
+ }
209
+ if (emitRouteManifest && context.dir) {
210
+ config.plugins.push(new RouteManifestPlugin(context.dir));
211
+ }
88
212
  }
89
213
  return config;
90
214
  }
package/dist/react.js CHANGED
@@ -1569,23 +1569,59 @@ function findNearestComponentSource(element, maxAncestors = 10) {
1569
1569
  }
1570
1570
 
1571
1571
  // src/utils/capture-element.ts
1572
- var UNSUPPORTED_COLOR_RE = /oklab|oklch|color-mix/i;
1573
- var COLOR_DECLARATION_RE = /[^{};\n]+:\s*[^;{}]*(?:oklab|oklch|color-mix|lch|lab)\([^)]*(?:\([^)]*\))*[^)]*\)[^;{}]*/gi;
1572
+ var UNSUPPORTED_RE = /oklab|oklch|color-mix/i;
1574
1573
  function sanitizeUnsupportedColors(clonedDoc) {
1574
+ try {
1575
+ for (let i = 0; i < clonedDoc.styleSheets.length; i++) {
1576
+ try {
1577
+ const sheet = clonedDoc.styleSheets[i];
1578
+ const rules = sheet.cssRules || sheet.rules;
1579
+ if (!rules) continue;
1580
+ sanitizeCSSRules(sheet, rules);
1581
+ } catch {
1582
+ }
1583
+ }
1584
+ } catch {
1585
+ }
1575
1586
  clonedDoc.querySelectorAll("style").forEach((styleEl) => {
1576
1587
  const text = styleEl.textContent || "";
1577
- if (UNSUPPORTED_COLOR_RE.test(text)) {
1578
- styleEl.textContent = text.replace(COLOR_DECLARATION_RE, "");
1588
+ if (UNSUPPORTED_RE.test(text)) {
1589
+ styleEl.textContent = text.replace(
1590
+ /[^{};\n]+:\s*[^;{}]*(?:oklab|oklch|color-mix|lch|lab)\([^)]*(?:\([^)]*\))*[^)]*\)[^;{}]*/gi,
1591
+ ""
1592
+ );
1579
1593
  }
1580
1594
  });
1581
1595
  clonedDoc.querySelectorAll("*").forEach((el) => {
1582
1596
  const s = el.style;
1583
1597
  if (!s?.cssText) return;
1584
- if (UNSUPPORTED_COLOR_RE.test(s.cssText)) {
1585
- s.cssText = s.cssText.replace(COLOR_DECLARATION_RE, "");
1598
+ if (UNSUPPORTED_RE.test(s.cssText)) {
1599
+ s.cssText = s.cssText.replace(
1600
+ /[^;]*(?:oklab|oklch|color-mix|lch|lab)\([^)]*(?:\([^)]*\))*[^)]*\)[^;]*/gi,
1601
+ ""
1602
+ );
1586
1603
  }
1587
1604
  });
1588
1605
  }
1606
+ function sanitizeCSSRules(sheet, rules) {
1607
+ for (let j = rules.length - 1; j >= 0; j--) {
1608
+ const rule = rules[j];
1609
+ if ("cssRules" in rule && rule.cssRules) {
1610
+ sanitizeCSSRules(sheet, rule.cssRules);
1611
+ continue;
1612
+ }
1613
+ if (rule instanceof CSSStyleRule) {
1614
+ const style = rule.style;
1615
+ for (let k = style.length - 1; k >= 0; k--) {
1616
+ const prop = style[k];
1617
+ const val = style.getPropertyValue(prop);
1618
+ if (UNSUPPORTED_RE.test(val)) {
1619
+ style.setProperty(prop, "transparent");
1620
+ }
1621
+ }
1622
+ }
1623
+ }
1624
+ }
1589
1625
  async function captureElement(el, options = {}) {
1590
1626
  const { quality = 0.7, maxScale = 2 } = options;
1591
1627
  const html2canvas = (await import("html2canvas")).default;
@@ -1838,6 +1874,151 @@ function collectMetadata(getUser) {
1838
1874
  return metadata;
1839
1875
  }
1840
1876
 
1877
+ // src/core/debug-targets.ts
1878
+ var NEXT_ROUTE_MANIFEST_URL = "/_next/static/chunks/impakers-debug-route-manifest.json";
1879
+ var manifestPromise = null;
1880
+ var manifestFailed = false;
1881
+ function normalizePathname(pathname) {
1882
+ if (!pathname || pathname === "/") return "/";
1883
+ return pathname.endsWith("/") ? pathname.slice(0, -1) || "/" : pathname;
1884
+ }
1885
+ function splitPathname(pathname) {
1886
+ const normalized = normalizePathname(pathname);
1887
+ if (normalized === "/") return [];
1888
+ return normalized.slice(1).split("/").map((segment) => decodeURIComponent(segment)).filter(Boolean);
1889
+ }
1890
+ function isDynamicSegment(segment) {
1891
+ return /^\[[^\]]+\]$/.test(segment);
1892
+ }
1893
+ function isCatchAllSegment(segment) {
1894
+ return /^\[\.\.\.[^\]]+\]$/.test(segment);
1895
+ }
1896
+ function isOptionalCatchAllSegment(segment) {
1897
+ return /^\[\[\.\.\.[^\]]+\]\]$/.test(segment);
1898
+ }
1899
+ function scoreRouteMatch(routeSegments, pathnameSegments) {
1900
+ let routeIndex = 0;
1901
+ let pathIndex = 0;
1902
+ let score = 0;
1903
+ while (routeIndex < routeSegments.length) {
1904
+ const routeSegment = routeSegments[routeIndex];
1905
+ if (isOptionalCatchAllSegment(routeSegment)) {
1906
+ score += 1;
1907
+ pathIndex = pathnameSegments.length;
1908
+ routeIndex += 1;
1909
+ break;
1910
+ }
1911
+ if (isCatchAllSegment(routeSegment)) {
1912
+ if (pathIndex >= pathnameSegments.length) return null;
1913
+ score += 2;
1914
+ pathIndex = pathnameSegments.length;
1915
+ routeIndex += 1;
1916
+ break;
1917
+ }
1918
+ const pathSegment = pathnameSegments[pathIndex];
1919
+ if (pathSegment === void 0) return null;
1920
+ if (isDynamicSegment(routeSegment)) {
1921
+ score += 5;
1922
+ routeIndex += 1;
1923
+ pathIndex += 1;
1924
+ continue;
1925
+ }
1926
+ if (routeSegment !== pathSegment) return null;
1927
+ score += 20;
1928
+ routeIndex += 1;
1929
+ pathIndex += 1;
1930
+ }
1931
+ if (routeIndex !== routeSegments.length) return null;
1932
+ if (pathIndex !== pathnameSegments.length) return null;
1933
+ return score;
1934
+ }
1935
+ async function loadNextRouteManifest() {
1936
+ if (manifestFailed) return null;
1937
+ if (manifestPromise) return manifestPromise;
1938
+ manifestPromise = (async () => {
1939
+ try {
1940
+ const res = await fetch(NEXT_ROUTE_MANIFEST_URL, { cache: "force-cache" });
1941
+ if (!res.ok) {
1942
+ manifestFailed = true;
1943
+ return null;
1944
+ }
1945
+ const json = await res.json();
1946
+ if (!json || typeof json !== "object" || !Array.isArray(json.entries)) {
1947
+ manifestFailed = true;
1948
+ return null;
1949
+ }
1950
+ return json;
1951
+ } catch {
1952
+ manifestFailed = true;
1953
+ return null;
1954
+ }
1955
+ })();
1956
+ return manifestPromise;
1957
+ }
1958
+ function buildRouteTargets(entry) {
1959
+ const pageTarget = entry.files.find((file) => file.kind === "page");
1960
+ const layoutTargets = entry.files.filter((file) => file.kind === "layout");
1961
+ const targets = [];
1962
+ if (pageTarget) {
1963
+ targets.push({
1964
+ kind: "route-page",
1965
+ file: pageTarget.file,
1966
+ label: "Current page route",
1967
+ confidence: 1,
1968
+ reason: "matched-current-url"
1969
+ });
1970
+ }
1971
+ layoutTargets.forEach((layout, index) => {
1972
+ targets.push({
1973
+ kind: "route-layout",
1974
+ file: layout.file,
1975
+ label: index === layoutTargets.length - 1 ? "Nearest layout" : "Ancestor layout",
1976
+ confidence: Math.max(0.7, 0.95 - index * 0.08),
1977
+ reason: "matched-layout-chain"
1978
+ });
1979
+ });
1980
+ return targets;
1981
+ }
1982
+ async function getRouteDebugTargets(pathname) {
1983
+ if (typeof window === "undefined") return null;
1984
+ const currentPathname = pathname ?? window.location.pathname;
1985
+ const manifest = await loadNextRouteManifest();
1986
+ if (!manifest) return null;
1987
+ const pathnameSegments = splitPathname(currentPathname);
1988
+ let bestEntry = null;
1989
+ let bestScore = -1;
1990
+ for (const entry of manifest.entries) {
1991
+ const score = scoreRouteMatch(entry.segments, pathnameSegments);
1992
+ if (score === null) continue;
1993
+ if (score > bestScore) {
1994
+ bestScore = score;
1995
+ bestEntry = entry;
1996
+ }
1997
+ }
1998
+ if (!bestEntry) return null;
1999
+ return {
2000
+ targets: buildRouteTargets(bestEntry),
2001
+ context: {
2002
+ pathname: normalizePathname(currentPathname),
2003
+ matchedRoute: bestEntry.route,
2004
+ source: "next-route-manifest"
2005
+ }
2006
+ };
2007
+ }
2008
+ function mergeDebugTargets(...groups) {
2009
+ const merged = [];
2010
+ const seen = /* @__PURE__ */ new Set();
2011
+ groups.forEach((group) => {
2012
+ group?.forEach((target) => {
2013
+ const key = `${target.kind}:${target.file}:${target.line ?? ""}:${target.column ?? ""}`;
2014
+ if (seen.has(key)) return;
2015
+ seen.add(key);
2016
+ merged.push(target);
2017
+ });
2018
+ });
2019
+ return merged.sort((a, b) => b.confidence - a.confidence);
2020
+ }
2021
+
1841
2022
  // src/core/api.ts
1842
2023
  function getToken() {
1843
2024
  const token = loadToken();
@@ -3405,6 +3586,23 @@ function detectSourceFileCandidates(element) {
3405
3586
  }
3406
3587
  return [];
3407
3588
  }
3589
+ function buildComponentDebugTarget(resolvedSource, componentName) {
3590
+ const match = resolvedSource.match(/^(.+):(\d+)(?::(\d+))?$/);
3591
+ if (!match) return null;
3592
+ const [, file, lineStr, columnStr] = match;
3593
+ const line = Number.parseInt(lineStr, 10);
3594
+ const column = columnStr ? Number.parseInt(columnStr, 10) : void 0;
3595
+ if (!file || Number.isNaN(line)) return null;
3596
+ return {
3597
+ kind: "component",
3598
+ file,
3599
+ line,
3600
+ column,
3601
+ label: componentName || "Resolved component source",
3602
+ confidence: 0.98,
3603
+ reason: "resolved-from-source-map"
3604
+ };
3605
+ }
3408
3606
  var STORAGE_PREFIX = "impakers-debug-markers-";
3409
3607
  function getRouteKey() {
3410
3608
  return STORAGE_PREFIX + window.location.pathname;
@@ -3726,7 +3924,28 @@ function DebugWidget({ endpoint, getUser, onHide }) {
3726
3924
  } catch {
3727
3925
  }
3728
3926
  }
3927
+ const routeMatch = await getRouteDebugTargets(window.location.pathname);
3928
+ const componentTarget = resolvedSource && !resolvedSource.includes("/chunks/") ? buildComponentDebugTarget(resolvedSource, resolvedName) : null;
3929
+ const debugTargets = mergeDebugTargets(
3930
+ componentTarget ? [componentTarget] : void 0,
3931
+ routeMatch?.targets
3932
+ );
3933
+ if (debugTargets.length > 0) {
3934
+ metadata.debugTargets = debugTargets;
3935
+ }
3936
+ if (routeMatch?.context) {
3937
+ metadata.routeDebug = routeMatch.context;
3938
+ }
3729
3939
  const descriptionParts = [comment];
3940
+ if (debugTargets.length > 0) {
3941
+ descriptionParts.push(
3942
+ "\n\n---\n**\uB514\uBC84\uAE45 \uD0C0\uAC9F \uD30C\uC77C**:",
3943
+ ...debugTargets.map((target) => {
3944
+ const suffix = target.line ? `:${target.line}${typeof target.column === "number" ? `:${target.column}` : ""}` : "";
3945
+ return `- [${target.kind}] \`${target.file}${suffix}\` (${target.reason})`;
3946
+ })
3947
+ );
3948
+ }
3730
3949
  const elementInfo = [];
3731
3950
  if (pendingAnnotation.element) {
3732
3951
  elementInfo.push(`**\uC120\uD0DD\uB41C \uC694\uC18C**: ${pendingAnnotation.element}`);