@impakers/debug 1.4.6 → 1.4.8

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,22 +1569,60 @@ 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 = /\b(?:oklab|oklch|color-mix|(?<![\w-])lab|(?<![\w-])lch)\s*\(/i;
1573
+ var COLOR_PROPS = [
1574
+ "color",
1575
+ "background-color",
1576
+ "background-image",
1577
+ "background",
1578
+ "border-color",
1579
+ "border-top-color",
1580
+ "border-right-color",
1581
+ "border-bottom-color",
1582
+ "border-left-color",
1583
+ "outline-color",
1584
+ "text-decoration-color",
1585
+ "box-shadow",
1586
+ "text-shadow",
1587
+ "caret-color",
1588
+ "column-rule-color",
1589
+ "fill",
1590
+ "stroke",
1591
+ "stop-color",
1592
+ "flood-color",
1593
+ "lighting-color"
1594
+ ];
1574
1595
  function sanitizeUnsupportedColors(clonedDoc) {
1575
- clonedDoc.querySelectorAll("style").forEach((styleEl) => {
1576
- const text = styleEl.textContent || "";
1577
- if (UNSUPPORTED_COLOR_RE.test(text)) {
1578
- styleEl.textContent = text.replace(COLOR_DECLARATION_RE, "");
1579
- }
1580
- });
1581
- clonedDoc.querySelectorAll("*").forEach((el) => {
1582
- const s = el.style;
1583
- if (!s?.cssText) return;
1584
- if (UNSUPPORTED_COLOR_RE.test(s.cssText)) {
1585
- s.cssText = s.cssText.replace(COLOR_DECLARATION_RE, "");
1596
+ const allEls = clonedDoc.querySelectorAll("*");
1597
+ for (let i = 0; i < allEls.length; i++) {
1598
+ const el = allEls[i];
1599
+ if (!el.style) continue;
1600
+ let cs = null;
1601
+ for (const prop of COLOR_PROPS) {
1602
+ const inlineVal = el.style.getPropertyValue(prop);
1603
+ if (inlineVal && UNSUPPORTED_RE.test(inlineVal)) {
1604
+ el.style.setProperty(prop, getFallback(prop));
1605
+ continue;
1606
+ }
1607
+ if (!cs) {
1608
+ try {
1609
+ cs = clonedDoc.defaultView.getComputedStyle(el);
1610
+ } catch {
1611
+ break;
1612
+ }
1613
+ }
1614
+ const val = cs.getPropertyValue(prop);
1615
+ if (val && UNSUPPORTED_RE.test(val)) {
1616
+ el.style.setProperty(prop, getFallback(prop));
1617
+ }
1586
1618
  }
1587
- });
1619
+ }
1620
+ }
1621
+ function getFallback(prop) {
1622
+ if (prop === "background-image" || prop === "background") return "none";
1623
+ if (prop === "box-shadow" || prop === "text-shadow") return "none";
1624
+ if (prop === "color") return "inherit";
1625
+ return "transparent";
1588
1626
  }
1589
1627
  async function captureElement(el, options = {}) {
1590
1628
  const { quality = 0.7, maxScale = 2 } = options;
@@ -1838,6 +1876,151 @@ function collectMetadata(getUser) {
1838
1876
  return metadata;
1839
1877
  }
1840
1878
 
1879
+ // src/core/debug-targets.ts
1880
+ var NEXT_ROUTE_MANIFEST_URL = "/_next/static/chunks/impakers-debug-route-manifest.json";
1881
+ var manifestPromise = null;
1882
+ var manifestFailed = false;
1883
+ function normalizePathname(pathname) {
1884
+ if (!pathname || pathname === "/") return "/";
1885
+ return pathname.endsWith("/") ? pathname.slice(0, -1) || "/" : pathname;
1886
+ }
1887
+ function splitPathname(pathname) {
1888
+ const normalized = normalizePathname(pathname);
1889
+ if (normalized === "/") return [];
1890
+ return normalized.slice(1).split("/").map((segment) => decodeURIComponent(segment)).filter(Boolean);
1891
+ }
1892
+ function isDynamicSegment(segment) {
1893
+ return /^\[[^\]]+\]$/.test(segment);
1894
+ }
1895
+ function isCatchAllSegment(segment) {
1896
+ return /^\[\.\.\.[^\]]+\]$/.test(segment);
1897
+ }
1898
+ function isOptionalCatchAllSegment(segment) {
1899
+ return /^\[\[\.\.\.[^\]]+\]\]$/.test(segment);
1900
+ }
1901
+ function scoreRouteMatch(routeSegments, pathnameSegments) {
1902
+ let routeIndex = 0;
1903
+ let pathIndex = 0;
1904
+ let score = 0;
1905
+ while (routeIndex < routeSegments.length) {
1906
+ const routeSegment = routeSegments[routeIndex];
1907
+ if (isOptionalCatchAllSegment(routeSegment)) {
1908
+ score += 1;
1909
+ pathIndex = pathnameSegments.length;
1910
+ routeIndex += 1;
1911
+ break;
1912
+ }
1913
+ if (isCatchAllSegment(routeSegment)) {
1914
+ if (pathIndex >= pathnameSegments.length) return null;
1915
+ score += 2;
1916
+ pathIndex = pathnameSegments.length;
1917
+ routeIndex += 1;
1918
+ break;
1919
+ }
1920
+ const pathSegment = pathnameSegments[pathIndex];
1921
+ if (pathSegment === void 0) return null;
1922
+ if (isDynamicSegment(routeSegment)) {
1923
+ score += 5;
1924
+ routeIndex += 1;
1925
+ pathIndex += 1;
1926
+ continue;
1927
+ }
1928
+ if (routeSegment !== pathSegment) return null;
1929
+ score += 20;
1930
+ routeIndex += 1;
1931
+ pathIndex += 1;
1932
+ }
1933
+ if (routeIndex !== routeSegments.length) return null;
1934
+ if (pathIndex !== pathnameSegments.length) return null;
1935
+ return score;
1936
+ }
1937
+ async function loadNextRouteManifest() {
1938
+ if (manifestFailed) return null;
1939
+ if (manifestPromise) return manifestPromise;
1940
+ manifestPromise = (async () => {
1941
+ try {
1942
+ const res = await fetch(NEXT_ROUTE_MANIFEST_URL, { cache: "force-cache" });
1943
+ if (!res.ok) {
1944
+ manifestFailed = true;
1945
+ return null;
1946
+ }
1947
+ const json = await res.json();
1948
+ if (!json || typeof json !== "object" || !Array.isArray(json.entries)) {
1949
+ manifestFailed = true;
1950
+ return null;
1951
+ }
1952
+ return json;
1953
+ } catch {
1954
+ manifestFailed = true;
1955
+ return null;
1956
+ }
1957
+ })();
1958
+ return manifestPromise;
1959
+ }
1960
+ function buildRouteTargets(entry) {
1961
+ const pageTarget = entry.files.find((file) => file.kind === "page");
1962
+ const layoutTargets = entry.files.filter((file) => file.kind === "layout");
1963
+ const targets = [];
1964
+ if (pageTarget) {
1965
+ targets.push({
1966
+ kind: "route-page",
1967
+ file: pageTarget.file,
1968
+ label: "Current page route",
1969
+ confidence: 1,
1970
+ reason: "matched-current-url"
1971
+ });
1972
+ }
1973
+ layoutTargets.forEach((layout, index) => {
1974
+ targets.push({
1975
+ kind: "route-layout",
1976
+ file: layout.file,
1977
+ label: index === layoutTargets.length - 1 ? "Nearest layout" : "Ancestor layout",
1978
+ confidence: Math.max(0.7, 0.95 - index * 0.08),
1979
+ reason: "matched-layout-chain"
1980
+ });
1981
+ });
1982
+ return targets;
1983
+ }
1984
+ async function getRouteDebugTargets(pathname) {
1985
+ if (typeof window === "undefined") return null;
1986
+ const currentPathname = pathname ?? window.location.pathname;
1987
+ const manifest = await loadNextRouteManifest();
1988
+ if (!manifest) return null;
1989
+ const pathnameSegments = splitPathname(currentPathname);
1990
+ let bestEntry = null;
1991
+ let bestScore = -1;
1992
+ for (const entry of manifest.entries) {
1993
+ const score = scoreRouteMatch(entry.segments, pathnameSegments);
1994
+ if (score === null) continue;
1995
+ if (score > bestScore) {
1996
+ bestScore = score;
1997
+ bestEntry = entry;
1998
+ }
1999
+ }
2000
+ if (!bestEntry) return null;
2001
+ return {
2002
+ targets: buildRouteTargets(bestEntry),
2003
+ context: {
2004
+ pathname: normalizePathname(currentPathname),
2005
+ matchedRoute: bestEntry.route,
2006
+ source: "next-route-manifest"
2007
+ }
2008
+ };
2009
+ }
2010
+ function mergeDebugTargets(...groups) {
2011
+ const merged = [];
2012
+ const seen = /* @__PURE__ */ new Set();
2013
+ groups.forEach((group) => {
2014
+ group?.forEach((target) => {
2015
+ const key = `${target.kind}:${target.file}:${target.line ?? ""}:${target.column ?? ""}`;
2016
+ if (seen.has(key)) return;
2017
+ seen.add(key);
2018
+ merged.push(target);
2019
+ });
2020
+ });
2021
+ return merged.sort((a, b) => b.confidence - a.confidence);
2022
+ }
2023
+
1841
2024
  // src/core/api.ts
1842
2025
  function getToken() {
1843
2026
  const token = loadToken();
@@ -3405,6 +3588,23 @@ function detectSourceFileCandidates(element) {
3405
3588
  }
3406
3589
  return [];
3407
3590
  }
3591
+ function buildComponentDebugTarget(resolvedSource, componentName) {
3592
+ const match = resolvedSource.match(/^(.+):(\d+)(?::(\d+))?$/);
3593
+ if (!match) return null;
3594
+ const [, file, lineStr, columnStr] = match;
3595
+ const line = Number.parseInt(lineStr, 10);
3596
+ const column = columnStr ? Number.parseInt(columnStr, 10) : void 0;
3597
+ if (!file || Number.isNaN(line)) return null;
3598
+ return {
3599
+ kind: "component",
3600
+ file,
3601
+ line,
3602
+ column,
3603
+ label: componentName || "Resolved component source",
3604
+ confidence: 0.98,
3605
+ reason: "resolved-from-source-map"
3606
+ };
3607
+ }
3408
3608
  var STORAGE_PREFIX = "impakers-debug-markers-";
3409
3609
  function getRouteKey() {
3410
3610
  return STORAGE_PREFIX + window.location.pathname;
@@ -3726,7 +3926,28 @@ function DebugWidget({ endpoint, getUser, onHide }) {
3726
3926
  } catch {
3727
3927
  }
3728
3928
  }
3929
+ const routeMatch = await getRouteDebugTargets(window.location.pathname);
3930
+ const componentTarget = resolvedSource && !resolvedSource.includes("/chunks/") ? buildComponentDebugTarget(resolvedSource, resolvedName) : null;
3931
+ const debugTargets = mergeDebugTargets(
3932
+ componentTarget ? [componentTarget] : void 0,
3933
+ routeMatch?.targets
3934
+ );
3935
+ if (debugTargets.length > 0) {
3936
+ metadata.debugTargets = debugTargets;
3937
+ }
3938
+ if (routeMatch?.context) {
3939
+ metadata.routeDebug = routeMatch.context;
3940
+ }
3729
3941
  const descriptionParts = [comment];
3942
+ if (debugTargets.length > 0) {
3943
+ descriptionParts.push(
3944
+ "\n\n---\n**\uB514\uBC84\uAE45 \uD0C0\uAC9F \uD30C\uC77C**:",
3945
+ ...debugTargets.map((target) => {
3946
+ const suffix = target.line ? `:${target.line}${typeof target.column === "number" ? `:${target.column}` : ""}` : "";
3947
+ return `- [${target.kind}] \`${target.file}${suffix}\` (${target.reason})`;
3948
+ })
3949
+ );
3950
+ }
3730
3951
  const elementInfo = [];
3731
3952
  if (pendingAnnotation.element) {
3732
3953
  elementInfo.push(`**\uC120\uD0DD\uB41C \uC694\uC18C**: ${pendingAnnotation.element}`);