@things-factory/ai-assistant 10.1.6

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.
Files changed (164) hide show
  1. package/client/components/board-ai-chat.test.ts +149 -0
  2. package/client/components/board-ai-chat.ts +3228 -0
  3. package/client/components/chat-defaults.ts +27 -0
  4. package/client/components/chat-echo-dedup.test.ts +230 -0
  5. package/client/components/chat-echo-dedup.ts +155 -0
  6. package/client/components/chat-input-builder.ts +69 -0
  7. package/client/components/markdown.test.ts +518 -0
  8. package/client/components/markdown.ts +508 -0
  9. package/client/components/mention-popup-helpers.ts +433 -0
  10. package/client/components/mention-popup.test.ts +588 -0
  11. package/client/components/mention-popup.ts +238 -0
  12. package/client/index.ts +14 -0
  13. package/client/tsconfig.json +13 -0
  14. package/client/utils/board-edit-patch.ts +374 -0
  15. package/dist-client/components/board-ai-chat.d.ts +423 -0
  16. package/dist-client/components/board-ai-chat.js +3034 -0
  17. package/dist-client/components/board-ai-chat.js.map +1 -0
  18. package/dist-client/components/board-ai-chat.test.d.ts +1 -0
  19. package/dist-client/components/board-ai-chat.test.js +138 -0
  20. package/dist-client/components/board-ai-chat.test.js.map +1 -0
  21. package/dist-client/components/chat-defaults.d.ts +8 -0
  22. package/dist-client/components/chat-defaults.js +22 -0
  23. package/dist-client/components/chat-defaults.js.map +1 -0
  24. package/dist-client/components/chat-echo-dedup.d.ts +54 -0
  25. package/dist-client/components/chat-echo-dedup.js +115 -0
  26. package/dist-client/components/chat-echo-dedup.js.map +1 -0
  27. package/dist-client/components/chat-echo-dedup.test.d.ts +1 -0
  28. package/dist-client/components/chat-echo-dedup.test.js +203 -0
  29. package/dist-client/components/chat-echo-dedup.test.js.map +1 -0
  30. package/dist-client/components/chat-input-builder.d.ts +55 -0
  31. package/dist-client/components/chat-input-builder.js +32 -0
  32. package/dist-client/components/chat-input-builder.js.map +1 -0
  33. package/dist-client/components/markdown.d.ts +60 -0
  34. package/dist-client/components/markdown.js +422 -0
  35. package/dist-client/components/markdown.js.map +1 -0
  36. package/dist-client/components/markdown.test.d.ts +1 -0
  37. package/dist-client/components/markdown.test.js +418 -0
  38. package/dist-client/components/markdown.test.js.map +1 -0
  39. package/dist-client/components/mention-popup-helpers.d.ts +165 -0
  40. package/dist-client/components/mention-popup-helpers.js +330 -0
  41. package/dist-client/components/mention-popup-helpers.js.map +1 -0
  42. package/dist-client/components/mention-popup.d.ts +35 -0
  43. package/dist-client/components/mention-popup.js +223 -0
  44. package/dist-client/components/mention-popup.js.map +1 -0
  45. package/dist-client/components/mention-popup.test.d.ts +1 -0
  46. package/dist-client/components/mention-popup.test.js +474 -0
  47. package/dist-client/components/mention-popup.test.js.map +1 -0
  48. package/dist-client/index.d.ts +14 -0
  49. package/dist-client/index.js +15 -0
  50. package/dist-client/index.js.map +1 -0
  51. package/dist-client/tsconfig.tsbuildinfo +1 -0
  52. package/dist-client/utils/board-edit-patch.d.ts +157 -0
  53. package/dist-client/utils/board-edit-patch.js +284 -0
  54. package/dist-client/utils/board-edit-patch.js.map +1 -0
  55. package/dist-server/index.d.ts +28 -0
  56. package/dist-server/index.js +32 -0
  57. package/dist-server/index.js.map +1 -0
  58. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
  59. package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
  60. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
  61. package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
  62. package/dist-server/service/chat-message/chat-message-publish.js +27 -0
  63. package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
  64. package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
  65. package/dist-server/service/chat-message/chat-message-subscription.js +57 -0
  66. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
  67. package/dist-server/service/chat-message/chat-message.d.ts +68 -0
  68. package/dist-server/service/chat-message/chat-message.js +172 -0
  69. package/dist-server/service/chat-message/chat-message.js.map +1 -0
  70. package/dist-server/service/chat-message/fold-history.d.ts +43 -0
  71. package/dist-server/service/chat-message/fold-history.js +33 -0
  72. package/dist-server/service/chat-message/fold-history.js.map +1 -0
  73. package/dist-server/service/chat-message/history-summary.d.ts +43 -0
  74. package/dist-server/service/chat-message/history-summary.js +77 -0
  75. package/dist-server/service/chat-message/history-summary.js.map +1 -0
  76. package/dist-server/service/chat-message/index.d.ts +5 -0
  77. package/dist-server/service/chat-message/index.js +12 -0
  78. package/dist-server/service/chat-message/index.js.map +1 -0
  79. package/dist-server/service/chat-message/llm-history.d.ts +46 -0
  80. package/dist-server/service/chat-message/llm-history.js +87 -0
  81. package/dist-server/service/chat-message/llm-history.js.map +1 -0
  82. package/dist-server/service/chat-session/chat-session.d.ts +60 -0
  83. package/dist-server/service/chat-session/chat-session.js +159 -0
  84. package/dist-server/service/chat-session/chat-session.js.map +1 -0
  85. package/dist-server/service/chat-session/index.d.ts +3 -0
  86. package/dist-server/service/chat-session/index.js +7 -0
  87. package/dist-server/service/chat-session/index.js.map +1 -0
  88. package/dist-server/service/chat-session/session-activity-broadcast.d.ts +30 -0
  89. package/dist-server/service/chat-session/session-activity-broadcast.js +34 -0
  90. package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -0
  91. package/dist-server/service/chat-session/session-activity-publish.d.ts +12 -0
  92. package/dist-server/service/chat-session/session-activity-publish.js +37 -0
  93. package/dist-server/service/chat-session/session-activity-publish.js.map +1 -0
  94. package/dist-server/service/chat-session/session-activity-subscription.d.ts +14 -0
  95. package/dist-server/service/chat-session/session-activity-subscription.js +87 -0
  96. package/dist-server/service/chat-session/session-activity-subscription.js.map +1 -0
  97. package/dist-server/service/chat-session/session-anchor.d.ts +26 -0
  98. package/dist-server/service/chat-session/session-anchor.js +100 -0
  99. package/dist-server/service/chat-session/session-anchor.js.map +1 -0
  100. package/dist-server/service/chat-session/session-inbox.d.ts +80 -0
  101. package/dist-server/service/chat-session/session-inbox.js +133 -0
  102. package/dist-server/service/chat-session/session-inbox.js.map +1 -0
  103. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +49 -0
  104. package/dist-server/service/chat-session-participant/chat-session-participant.js +141 -0
  105. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
  106. package/dist-server/service/chat-session-participant/index.d.ts +4 -0
  107. package/dist-server/service/chat-session-participant/index.js +10 -0
  108. package/dist-server/service/chat-session-participant/index.js.map +1 -0
  109. package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
  110. package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
  111. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
  112. package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
  113. package/dist-server/service/chat-session-participant/presence.js +22 -0
  114. package/dist-server/service/chat-session-participant/presence.js.map +1 -0
  115. package/dist-server/service/chat-session-resolver.d.ts +93 -0
  116. package/dist-server/service/chat-session-resolver.js +706 -0
  117. package/dist-server/service/chat-session-resolver.js.map +1 -0
  118. package/dist-server/service/index.d.ts +24 -0
  119. package/dist-server/service/index.js +39 -0
  120. package/dist-server/service/index.js.map +1 -0
  121. package/dist-server/service/subscribe-auth.d.ts +14 -0
  122. package/dist-server/service/subscribe-auth.js +33 -0
  123. package/dist-server/service/subscribe-auth.js.map +1 -0
  124. package/dist-server/service/types.d.ts +5 -0
  125. package/dist-server/service/types.js +12 -0
  126. package/dist-server/service/types.js.map +1 -0
  127. package/dist-server/tsconfig.tsbuildinfo +1 -0
  128. package/package.json +48 -0
  129. package/server/index.ts +28 -0
  130. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
  131. package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
  132. package/server/service/chat-message/chat-message-broadcast.ts +27 -0
  133. package/server/service/chat-message/chat-message-publish.ts +22 -0
  134. package/server/service/chat-message/chat-message-subscription.ts +45 -0
  135. package/server/service/chat-message/chat-message.ts +195 -0
  136. package/server/service/chat-message/fold-history.test.ts +98 -0
  137. package/server/service/chat-message/fold-history.ts +77 -0
  138. package/server/service/chat-message/history-summary.test.ts +127 -0
  139. package/server/service/chat-message/history-summary.ts +100 -0
  140. package/server/service/chat-message/index.ts +12 -0
  141. package/server/service/chat-message/llm-history.test.ts +194 -0
  142. package/server/service/chat-message/llm-history.ts +163 -0
  143. package/server/service/chat-session/chat-session.ts +193 -0
  144. package/server/service/chat-session/index.ts +5 -0
  145. package/server/service/chat-session/session-activity-broadcast.ts +57 -0
  146. package/server/service/chat-session/session-activity-publish.ts +44 -0
  147. package/server/service/chat-session/session-activity-subscription.ts +67 -0
  148. package/server/service/chat-session/session-anchor.ts +106 -0
  149. package/server/service/chat-session/session-inbox.test.ts +185 -0
  150. package/server/service/chat-session/session-inbox.ts +161 -0
  151. package/server/service/chat-session-participant/chat-session-participant.ts +135 -0
  152. package/server/service/chat-session-participant/index.ts +6 -0
  153. package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
  154. package/server/service/chat-session-participant/invite-notification.ts +48 -0
  155. package/server/service/chat-session-participant/presence.test.ts +39 -0
  156. package/server/service/chat-session-participant/presence.ts +21 -0
  157. package/server/service/chat-session-resolver.ts +644 -0
  158. package/server/service/index.ts +37 -0
  159. package/server/service/session-anchor.test.ts +133 -0
  160. package/server/service/subscribe-auth.test.ts +57 -0
  161. package/server/service/subscribe-auth.ts +34 -0
  162. package/server/service/types.ts +15 -0
  163. package/things-factory.config.js +1 -0
  164. package/tsconfig.json +9 -0
@@ -0,0 +1,418 @@
1
+ /**
2
+ * renderMarkdown 단위 테스트.
3
+ *
4
+ * 핵심 회귀 방지 영역:
5
+ * 1. 기본 markdown (bold, italic, list, code, link).
6
+ * 2. 보안 — raw HTML, javascript: 링크 차단.
7
+ * 3. CJK + 구두점 + ** 인접의 flanking 실패 케이스 (한국어 채팅 응답에서 빈번).
8
+ * 4. 코드 펜스/인라인 코드 안의 ** 는 건드리지 않음.
9
+ */
10
+ import { renderMarkdown, sanitizeHtml, escapeHtml, extractCjkStrongs, extractMentions, stripMentionRefids, injectMentionRefids } from './markdown';
11
+ describe('renderMarkdown — 기본 markdown', () => {
12
+ test('빈 문자열은 빈 문자열 반환', () => {
13
+ expect(renderMarkdown('')).toBe('');
14
+ });
15
+ test('일반 텍스트는 <p> 로 감쌈', () => {
16
+ expect(renderMarkdown('hello')).toContain('<p>hello</p>');
17
+ });
18
+ test('** 로 굵게', () => {
19
+ expect(renderMarkdown('**bold** text')).toContain('<strong>bold</strong>');
20
+ });
21
+ test('* 로 기울임', () => {
22
+ expect(renderMarkdown('an *italic* word')).toContain('<em>italic</em>');
23
+ });
24
+ test('- 로 unordered list', () => {
25
+ const out = renderMarkdown('- a\n- b\n- c');
26
+ expect(out).toContain('<ul>');
27
+ expect(out).toContain('<li>a</li>');
28
+ expect(out).toContain('<li>b</li>');
29
+ expect(out).toContain('<li>c</li>');
30
+ });
31
+ test('inline code', () => {
32
+ expect(renderMarkdown('use `foo` here')).toContain('<code>foo</code>');
33
+ });
34
+ test('fenced code block', () => {
35
+ const md = '```\nconst x = 1\n```';
36
+ expect(renderMarkdown(md)).toContain('<pre>');
37
+ expect(renderMarkdown(md)).toContain('<code>');
38
+ });
39
+ test('헤더', () => {
40
+ expect(renderMarkdown('# Title')).toContain('<h1>Title</h1>');
41
+ expect(renderMarkdown('## Sub')).toContain('<h2>Sub</h2>');
42
+ });
43
+ test('breaks: 단일 newline 은 <br>', () => {
44
+ expect(renderMarkdown('line1\nline2')).toContain('<br>');
45
+ });
46
+ });
47
+ describe('renderMarkdown — 보안', () => {
48
+ test('raw <script> 차단', () => {
49
+ const out = renderMarkdown("<script>alert('x')</script>");
50
+ expect(out).not.toContain('<script>');
51
+ expect(out).not.toContain('alert');
52
+ });
53
+ test('raw <iframe> 차단', () => {
54
+ const out = renderMarkdown('<iframe src=x></iframe>');
55
+ expect(out).not.toContain('<iframe');
56
+ });
57
+ test('inline HTML strong 차단 (renderer.html → 빈 문자열)', () => {
58
+ const out = renderMarkdown('Hello <strong>world</strong>');
59
+ // raw HTML 은 제거되고 텍스트만 남거나 비어야 함
60
+ expect(out).not.toContain('<strong>world</strong>');
61
+ });
62
+ test('javascript: 링크 차단', () => {
63
+ const out = renderMarkdown("[click](javascript:alert(1))");
64
+ expect(out).not.toMatch(/href="javascript:/i);
65
+ });
66
+ test('data: 링크 차단', () => {
67
+ const out = renderMarkdown('[x](data:text/html,<script>alert(1)</script>)');
68
+ expect(out).not.toMatch(/href="data:/i);
69
+ });
70
+ test('정상 https 링크는 통과', () => {
71
+ const out = renderMarkdown('[ok](https://example.com)');
72
+ expect(out).toMatch(/href="https:\/\/example\.com"/);
73
+ });
74
+ });
75
+ describe('sanitizeHtml — DOMPurify defense-in-depth', () => {
76
+ // Node 환경 (jest) 에선 window 부재 → fallback 동작 검증.
77
+ // 브라우저에서의 실제 정화는 통합 테스트 영역.
78
+ test('window 부재 시 입력 그대로 통과 (fallback)', () => {
79
+ // 직접 호출 — window undefined 환경에서 DOMPurify 안 도는 게 정상
80
+ const html = '<p>safe</p><script>bad</script>';
81
+ const out = sanitizeHtml(html);
82
+ // 노드 환경에서 fallback 이라 그대로 통과 — 브라우저 환경에선 script 제거됨
83
+ expect(typeof out).toBe('string');
84
+ });
85
+ test('window 있을 때 (jsdom mock) — script 차단 확인', () => {
86
+ // jsdom 은 jest 의 testEnvironment 가 jsdom 일 때만 동작 — 본 setup 은 node.
87
+ // 본 test 는 sanitizeHtml 의 *contract* 만 확인 (string in → string out).
88
+ const out = sanitizeHtml('<p>hello</p>');
89
+ expect(out).toContain('hello');
90
+ });
91
+ test('빈 문자열 안전 처리', () => {
92
+ expect(sanitizeHtml('')).toBe('');
93
+ });
94
+ test('null/undefined fallback', () => {
95
+ // 타입상 string 만 받지만 방어적으로 — 빈 문자열로 처리
96
+ expect(sanitizeHtml('')).toBe('');
97
+ });
98
+ });
99
+ describe('renderMarkdown — XSS 우회 시도 차단 (defense-in-depth)', () => {
100
+ // 본 테스트들은 marked 의 1차 차단 + DOMPurify 의 2차 차단이 모두 작동하는지
101
+ // 회귀 방지. node 환경에선 sanitizeHtml 이 fallback 이지만, marked 의 1차
102
+ // renderer.html → '' 차단만으로도 대부분 케이스 차단 검증 가능.
103
+ test('이미지 onerror 핸들러 시도 — img 태그 자체에 onerror attribute 없음', () => {
104
+ const out = renderMarkdown('![alt](x" onerror="alert(1))');
105
+ // 결과 텍스트에 escape 된 형태로 onerror= 라는 글자 자체는 남을 수 있지만,
106
+ // *실행 가능한 attribute* 형태 (속성 이름으로) 는 없어야 한다.
107
+ // 정밀 검사: <img 태그 안에 onerror=" 형태가 없는지
108
+ expect(out).not.toMatch(/<img\b[^>]*\bonerror\s*=/i);
109
+ // 또한 escape 안 된 따옴표 + onerror 조합 자체도 없어야
110
+ expect(out).not.toMatch(/"\s+onerror=/i);
111
+ });
112
+ test('img src=javascript: 차단', () => {
113
+ const out = renderMarkdown('![x](javascript:alert(1))');
114
+ expect(out).not.toMatch(/src="javascript:/i);
115
+ });
116
+ test('SVG xss 패턴 차단', () => {
117
+ const out = renderMarkdown('<svg onload=alert(1)>');
118
+ expect(out).not.toContain('<svg');
119
+ expect(out).not.toMatch(/onload/i);
120
+ });
121
+ test('style="javascript:..." 차단', () => {
122
+ const out = renderMarkdown('[click](#){:style="javascript:alert(1)"}');
123
+ expect(out).not.toMatch(/style="javascript:/i);
124
+ });
125
+ test('mention span 안의 사용자 token escape', () => {
126
+ const out = renderMarkdown('#"><script>alert(1)</script>');
127
+ expect(out).not.toMatch(/<script>/i);
128
+ });
129
+ test('event handler attribute 차단 (onmouseover 등)', () => {
130
+ // raw HTML 이 marked.renderer.html 에서 빈 문자열로 → onmouseover 자체가 출력 안됨
131
+ const out = renderMarkdown('<a href="#" onmouseover="alert(1)">x</a>');
132
+ expect(out).not.toMatch(/onmouseover/i);
133
+ });
134
+ test('vbscript: 스킴 차단 (legacy IE 잔재)', () => {
135
+ const out = renderMarkdown('[x](vbscript:msgbox(1))');
136
+ expect(out).not.toMatch(/href="vbscript:/i);
137
+ });
138
+ });
139
+ describe('renderMarkdown — CJK + 구두점 인접 (회귀 방지)', () => {
140
+ // 이 그룹은 사용자 보고 사례. CommonMark flanking 규칙으로 인해 ** 가
141
+ // 닫히지 않던 케이스들을 모두 회복해야 한다.
142
+ const cases = [
143
+ ["closing ** 이 punct + CJK 사이", "**'rect'**의", '<strong>&#039;rect&#039;</strong>의'],
144
+ ["키 **'fillStyle'**의", "키 **'fillStyle'**의", '<strong>&#039;fillStyle&#039;</strong>의'],
145
+ ["AGV 이름 + 조사", "**'AGV-1'**의 위치", '<strong>&#039;AGV-1&#039;</strong>의'],
146
+ ["다중 인접 한글", "사용자가 **'AGV-1'**컴포넌트를", '<strong>&#039;AGV-1&#039;</strong>컴포넌트를'],
147
+ ["bold 안 . 으로 끝", '**foo.**의', '<strong>foo.</strong>의'],
148
+ ["bold 안 ) 으로 끝", '**foo)**의', '<strong>foo)</strong>의'],
149
+ ["한 ** punct 시작 인접", "한**'foo'**", '한<strong>&#039;foo&#039;</strong>'],
150
+ ["양쪽 모두 CJK + punct", "한**'foo'**글", '한<strong>&#039;foo&#039;</strong>글']
151
+ ];
152
+ for (const [name, input, expected] of cases) {
153
+ test(name, () => {
154
+ const out = renderMarkdown(input);
155
+ expect(out).toContain(expected);
156
+ });
157
+ }
158
+ test('정상 케이스는 그대로 marked 가 처리', () => {
159
+ // 닫는 ** 가 공백 직전인 경우 — flanking 통과 (marked native; entity 표기는 &#39;)
160
+ const a = renderMarkdown("키 **'fillStyle'** 의");
161
+ expect(a).toMatch(/<strong>(&#0?39;|')fillStyle(&#0?39;|')<\/strong>/);
162
+ const b = renderMarkdown("Type **'rect'** here");
163
+ expect(b).toMatch(/<strong>(&#0?39;|')rect(&#0?39;|')<\/strong>/);
164
+ });
165
+ test('사용자 실제 케이스 — 산업용 다크 모드 대시보드', () => {
166
+ const md = "**공장 설비 및 생산 현황을 한눈에 파악하기 위한 '산업용 다크 모드 대시보드'** 도 확인해보라.";
167
+ const out = renderMarkdown(md);
168
+ expect(out).toContain('<strong>');
169
+ expect(out).toContain('대시보드');
170
+ });
171
+ test('punct 가 끝/시작에 없으면 변경 없음', () => {
172
+ // marked 자체가 처리할 수 있는 케이스 — 우회 안 일어남
173
+ const out = renderMarkdown('한**foo**글');
174
+ expect(out).toContain('<strong>foo</strong>');
175
+ });
176
+ test('일본어 인접', () => {
177
+ expect(renderMarkdown("これは**'値'**です")).toContain('<strong>&#039;値&#039;</strong>');
178
+ });
179
+ test('중국어 인접', () => {
180
+ expect(renderMarkdown("这是**'值'**测试")).toContain('<strong>&#039;值&#039;</strong>');
181
+ });
182
+ test('인라인 코드 안의 ** 는 건드리지 않음', () => {
183
+ // 코드 안의 ** 는 그대로 텍스트로 남아야 함
184
+ const out = renderMarkdown('`**not bold**`');
185
+ expect(out).toContain('<code>**not bold**</code>');
186
+ });
187
+ test('코드 펜스 안의 ** 는 건드리지 않음', () => {
188
+ const md = '```\n**not bold**\n```';
189
+ const out = renderMarkdown(md);
190
+ expect(out).toContain('<code>');
191
+ // 펜스 안의 ** 는 marked 가 텍스트로 그대로 둠
192
+ expect(out).toMatch(/\*\*not bold\*\*/);
193
+ });
194
+ test('placeholder 토큰 (BAICJKSTRONG…) 이 출력에 노출되지 않음', () => {
195
+ const out = renderMarkdown("키 **'fillStyle'**의");
196
+ expect(out).not.toContain('BAICJKSTRONG');
197
+ expect(out).not.toContain('ENDMARK');
198
+ });
199
+ test('치환된 strong 내용은 HTML escape 됨', () => {
200
+ // CJK-fix 경로를 타는 케이스에 < > 가 섞여도 안전하게 escape 되어야 한다.
201
+ // before='', after='의' (CJK), innerStart=', innerEnd=' (둘 다 punct) → fix 트리거.
202
+ const out = renderMarkdown("**'<bad>'**의");
203
+ expect(out).not.toContain('<bad>');
204
+ expect(out).toContain('&lt;bad&gt;');
205
+ expect(out).toContain('<strong>');
206
+ });
207
+ test('여러 strong 가 한 줄에 있어도 모두 처리', () => {
208
+ const out = renderMarkdown("**'A'**과 **'B'**는 다르다");
209
+ // 둘 다 strong 으로 렌더되어야 함
210
+ const matches = out.match(/<strong>/g) ?? [];
211
+ expect(matches.length).toBe(2);
212
+ });
213
+ });
214
+ describe('extractCjkStrongs — 내부 동작', () => {
215
+ test('필요 없는 케이스는 stored 비어 있음', () => {
216
+ const r = extractCjkStrongs('regular **bold** text');
217
+ expect(r.stored).toEqual([]);
218
+ expect(r.processed).toBe('regular **bold** text');
219
+ });
220
+ test('필요한 케이스만 추출', () => {
221
+ const r = extractCjkStrongs("키 **'fillStyle'**의");
222
+ expect(r.stored).toEqual(["'fillStyle'"]);
223
+ expect(r.processed).toContain('BAICJKSTRONG');
224
+ });
225
+ test('인라인 코드 안의 ** 는 추출 안 함', () => {
226
+ const r = extractCjkStrongs("`**'foo'**의` 와 텍스트");
227
+ expect(r.stored).toEqual([]);
228
+ expect(r.processed).toContain("`**'foo'**의`");
229
+ });
230
+ test('코드 펜스 안의 ** 는 추출 안 함', () => {
231
+ const r = extractCjkStrongs("```\n**'foo'**의\n```");
232
+ expect(r.stored).toEqual([]);
233
+ });
234
+ test('짝이 안 맞는 ** 는 무시', () => {
235
+ const r = extractCjkStrongs('한**foo and 글');
236
+ expect(r.stored).toEqual([]);
237
+ });
238
+ });
239
+ describe('extractMentions — `#token` placeholder 추출', () => {
240
+ test('빈 문자열은 빈 결과', () => {
241
+ const r = extractMentions('');
242
+ expect(r.stored).toEqual([]);
243
+ expect(r.processed).toBe('');
244
+ });
245
+ test('단일 mention — 영문', () => {
246
+ const r = extractMentions('보드의 #motor1 위치');
247
+ expect(r.stored).toEqual([{ trigger: '#', token: 'motor1' }]);
248
+ expect(r.processed).toMatch(/보드의 BAIMENTION0ENDMENTION 위치/);
249
+ });
250
+ test('단일 mention — 한글', () => {
251
+ const r = extractMentions('#경광등1 빨갛게');
252
+ expect(r.stored).toEqual([{ trigger: '#', token: '경광등1' }]);
253
+ });
254
+ test('다중 mention — 순서대로 인덱스', () => {
255
+ const r = extractMentions('#a 와 #b 비교');
256
+ expect(r.stored).toEqual([
257
+ { trigger: '#', token: 'a' },
258
+ { trigger: '#', token: 'b' }
259
+ ]);
260
+ expect(r.processed).toMatch(/BAIMENTION0ENDMENTION 와 BAIMENTION1ENDMENTION/);
261
+ });
262
+ test('단어 중간 # 는 mention 아님', () => {
263
+ const r = extractMentions('foo#bar 는 무시');
264
+ expect(r.stored).toEqual([]);
265
+ });
266
+ test('# 직후 공백 (markdown ATX 헤딩) 은 매칭 안 함', () => {
267
+ expect(extractMentions('# Title').stored).toEqual([]);
268
+ expect(extractMentions('## Sub').stored).toEqual([]);
269
+ expect(extractMentions('### 한글헤딩').stored).toEqual([]);
270
+ });
271
+ test('인라인 코드 안의 # 는 건드리지 않음', () => {
272
+ const r = extractMentions('use `#define` here');
273
+ expect(r.stored).toEqual([]);
274
+ expect(r.processed).toBe('use `#define` here');
275
+ });
276
+ test('코드 펜스 안의 # 는 건드리지 않음', () => {
277
+ const r = extractMentions('```\n#abc in code\n```\n그리고 #def 는 mention');
278
+ expect(r.stored).toEqual([{ trigger: '#', token: 'def' }]);
279
+ });
280
+ test('구두점 직후 # — 매칭', () => {
281
+ const r = extractMentions('(#abc) [#def]');
282
+ expect(r.stored).toEqual([
283
+ { trigger: '#', token: 'abc' },
284
+ { trigger: '#', token: 'def' }
285
+ ]);
286
+ });
287
+ test('@user 멘션도 추출 — trigger 보존', () => {
288
+ const r = extractMentions('@경철 도와주세요');
289
+ expect(r.stored).toEqual([{ trigger: '@', token: '경철' }]);
290
+ });
291
+ test('# 와 @ 혼합 — 순서대로', () => {
292
+ const r = extractMentions('@경철 #motor1 같이 봐');
293
+ expect(r.stored).toEqual([
294
+ { trigger: '@', token: '경철' },
295
+ { trigger: '#', token: 'motor1' }
296
+ ]);
297
+ });
298
+ test('이메일 (user@host) — mention 아님', () => {
299
+ const r = extractMentions('a@b.com 은 이메일');
300
+ expect(r.stored).toEqual([]);
301
+ });
302
+ test('renderMarkdown 통합 — span 으로 복원', () => {
303
+ const html = renderMarkdown('보드의 #motor1 위치');
304
+ expect(html).toContain('<span class="mention">#motor1</span>');
305
+ });
306
+ test('renderMarkdown 통합 — 한글 mention', () => {
307
+ const html = renderMarkdown('#경광등1 빨갛게');
308
+ expect(html).toContain('<span class="mention">#경광등1</span>');
309
+ });
310
+ test('renderMarkdown 통합 — heading 과 mention 공존', () => {
311
+ const html = renderMarkdown('# Title\n그리고 #foo 참조');
312
+ expect(html).toContain('<h1>Title</h1>');
313
+ expect(html).toContain('<span class="mention">#foo</span>');
314
+ });
315
+ test('renderMarkdown 통합 — 코드 안 # 는 평범한 code', () => {
316
+ const html = renderMarkdown('use `#define`');
317
+ expect(html).toContain('<code>#define</code>');
318
+ expect(html).not.toContain('<span class="mention">');
319
+ });
320
+ test('renderMarkdown 통합 — XSS 안전 (token 의 < > escape)', () => {
321
+ const html = renderMarkdown('#<script>');
322
+ expect(html).not.toContain('<script>');
323
+ // < > 이전까지만 토큰 — 매칭 안 될 수도 있고 매칭되더라도 escape
324
+ if (html.includes('class="mention"')) {
325
+ expect(html).toContain('&lt;');
326
+ }
327
+ });
328
+ });
329
+ describe('renderMarkdown — `#token{refid:N}` inline marker → data-refid', () => {
330
+ test('marker 없으면 span 에 data-refid 없음', () => {
331
+ const html = renderMarkdown('보드의 #motor1 위치');
332
+ expect(html).toContain('<span class="mention">#motor1</span>');
333
+ expect(html).not.toContain('data-refid');
334
+ });
335
+ test('marker 있으면 data-refid embed 되고 marker 자체는 출력 안 됨', () => {
336
+ const html = renderMarkdown('#motor1{refid:35} 빨갛게');
337
+ expect(html).toContain('<span class="mention" data-refid="35">#motor1</span>');
338
+ expect(html).not.toContain('{refid:');
339
+ });
340
+ test('다중 mention — 각 token 별 refid 분리', () => {
341
+ const html = renderMarkdown('#a{refid:10} 와 #b{refid:20}');
342
+ expect(html).toContain('data-refid="10">#a</span>');
343
+ expect(html).toContain('data-refid="20">#b</span>');
344
+ });
345
+ test('marker 일부만 있을 때 — marker 없는 mention 은 data-refid 없음', () => {
346
+ const html = renderMarkdown('#a{refid:10} 와 #unknown');
347
+ expect(html).toContain('data-refid="10">#a</span>');
348
+ expect(html).toContain('<span class="mention">#unknown</span>');
349
+ });
350
+ test('한글 mention — data-refid 정상 embed', () => {
351
+ const html = renderMarkdown('#경광등1{refid:35} 빨갛게');
352
+ expect(html).toContain('data-refid="35">#경광등1</span>');
353
+ });
354
+ test('marker 안 본문 — escapeHtml 안전 (XSS)', () => {
355
+ const html = renderMarkdown('#motor1{refid:35} <script>');
356
+ expect(html).not.toContain('<script>');
357
+ expect(html).toContain('data-refid="35"');
358
+ });
359
+ });
360
+ describe('stripMentionRefids — marker 제거', () => {
361
+ test('단일 marker 제거', () => {
362
+ expect(stripMentionRefids('#motor1{refid:35} 빨갛게')).toBe('#motor1 빨갛게');
363
+ });
364
+ test('다중 marker 제거', () => {
365
+ expect(stripMentionRefids('#a{refid:1} 와 #b{refid:2}')).toBe('#a 와 #b');
366
+ });
367
+ test('marker 없으면 원본 그대로', () => {
368
+ expect(stripMentionRefids('#motor1 빨갛게')).toBe('#motor1 빨갛게');
369
+ expect(stripMentionRefids('아무 마커 없는 텍스트')).toBe('아무 마커 없는 텍스트');
370
+ });
371
+ test('빈/널 안전', () => {
372
+ expect(stripMentionRefids('')).toBe('');
373
+ expect(stripMentionRefids(null)).toBe('');
374
+ expect(stripMentionRefids(undefined)).toBe('');
375
+ });
376
+ });
377
+ describe('injectMentionRefids — popup picks 를 본문에 marker 주입', () => {
378
+ test('단일 token 주입', () => {
379
+ expect(injectMentionRefids('#motor1 빨갛게', [{ token: 'motor1', refid: 35 }])).toBe('#motor1{refid:35} 빨갛게');
380
+ });
381
+ test('다중 token — 각각 주입', () => {
382
+ expect(injectMentionRefids('#a 와 #b', [
383
+ { token: 'a', refid: 10 },
384
+ { token: 'b', refid: 20 }
385
+ ])).toBe('#a{refid:10} 와 #b{refid:20}');
386
+ });
387
+ test('matches 가 없는 token 은 건드리지 않음', () => {
388
+ expect(injectMentionRefids('#motor1 와 #other', [{ token: 'motor1', refid: 35 }])).toBe('#motor1{refid:35} 와 #other');
389
+ });
390
+ test('이미 marker 있으면 덮어쓰기 안 함 (idempotent)', () => {
391
+ expect(injectMentionRefids('#motor1{refid:99} 빨갛게', [{ token: 'motor1', refid: 35 }])).toBe('#motor1{refid:99} 빨갛게');
392
+ });
393
+ test('단어 중간 # 는 매칭 안 함', () => {
394
+ expect(injectMentionRefids('foo#motor1 끝', [{ token: 'motor1', refid: 35 }])).toBe('foo#motor1 끝');
395
+ });
396
+ test('빈 picks / 빈 텍스트 안전', () => {
397
+ expect(injectMentionRefids('#a', [])).toBe('#a');
398
+ expect(injectMentionRefids('', [{ token: 'a', refid: 1 }])).toBe('');
399
+ expect(injectMentionRefids('#a', null)).toBe('#a');
400
+ expect(injectMentionRefids('#a', undefined)).toBe('#a');
401
+ });
402
+ test('한글 token 주입', () => {
403
+ expect(injectMentionRefids('#경광등1 빨갛게', [{ token: '경광등1', refid: 42 }])).toBe('#경광등1{refid:42} 빨갛게');
404
+ });
405
+ });
406
+ describe('escapeHtml — HTML 이스케이프', () => {
407
+ test('< > & " \' 모두 escape', () => {
408
+ expect(escapeHtml('<script>')).toBe('&lt;script&gt;');
409
+ expect(escapeHtml('a & b')).toBe('a &amp; b');
410
+ expect(escapeHtml('"q"')).toBe('&quot;q&quot;');
411
+ expect(escapeHtml("'a'")).toBe('&#039;a&#039;');
412
+ });
413
+ test('일반 문자는 변경 없음', () => {
414
+ expect(escapeHtml('hello world')).toBe('hello world');
415
+ expect(escapeHtml('한글 テスト 中文')).toBe('한글 テスト 中文');
416
+ });
417
+ });
418
+ //# sourceMappingURL=markdown.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.test.js","sourceRoot":"","sources":["../../client/components/markdown.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * renderMarkdown 단위 테스트.\n *\n * 핵심 회귀 방지 영역:\n * 1. 기본 markdown (bold, italic, list, code, link).\n * 2. 보안 — raw HTML, javascript: 링크 차단.\n * 3. CJK + 구두점 + ** 인접의 flanking 실패 케이스 (한국어 채팅 응답에서 빈번).\n * 4. 코드 펜스/인라인 코드 안의 ** 는 건드리지 않음.\n */\nimport {\n renderMarkdown,\n sanitizeHtml,\n escapeHtml,\n extractCjkStrongs,\n extractMentions,\n stripMentionRefids,\n injectMentionRefids\n} from './markdown'\n\ndescribe('renderMarkdown — 기본 markdown', () => {\n test('빈 문자열은 빈 문자열 반환', () => {\n expect(renderMarkdown('')).toBe('')\n })\n\n test('일반 텍스트는 <p> 로 감쌈', () => {\n expect(renderMarkdown('hello')).toContain('<p>hello</p>')\n })\n\n test('** 로 굵게', () => {\n expect(renderMarkdown('**bold** text')).toContain('<strong>bold</strong>')\n })\n\n test('* 로 기울임', () => {\n expect(renderMarkdown('an *italic* word')).toContain('<em>italic</em>')\n })\n\n test('- 로 unordered list', () => {\n const out = renderMarkdown('- a\\n- b\\n- c')\n expect(out).toContain('<ul>')\n expect(out).toContain('<li>a</li>')\n expect(out).toContain('<li>b</li>')\n expect(out).toContain('<li>c</li>')\n })\n\n test('inline code', () => {\n expect(renderMarkdown('use `foo` here')).toContain('<code>foo</code>')\n })\n\n test('fenced code block', () => {\n const md = '```\\nconst x = 1\\n```'\n expect(renderMarkdown(md)).toContain('<pre>')\n expect(renderMarkdown(md)).toContain('<code>')\n })\n\n test('헤더', () => {\n expect(renderMarkdown('# Title')).toContain('<h1>Title</h1>')\n expect(renderMarkdown('## Sub')).toContain('<h2>Sub</h2>')\n })\n\n test('breaks: 단일 newline 은 <br>', () => {\n expect(renderMarkdown('line1\\nline2')).toContain('<br>')\n })\n})\n\ndescribe('renderMarkdown — 보안', () => {\n test('raw <script> 차단', () => {\n const out = renderMarkdown(\"<script>alert('x')</script>\")\n expect(out).not.toContain('<script>')\n expect(out).not.toContain('alert')\n })\n\n test('raw <iframe> 차단', () => {\n const out = renderMarkdown('<iframe src=x></iframe>')\n expect(out).not.toContain('<iframe')\n })\n\n test('inline HTML strong 차단 (renderer.html → 빈 문자열)', () => {\n const out = renderMarkdown('Hello <strong>world</strong>')\n // raw HTML 은 제거되고 텍스트만 남거나 비어야 함\n expect(out).not.toContain('<strong>world</strong>')\n })\n\n test('javascript: 링크 차단', () => {\n const out = renderMarkdown(\"[click](javascript:alert(1))\")\n expect(out).not.toMatch(/href=\"javascript:/i)\n })\n\n test('data: 링크 차단', () => {\n const out = renderMarkdown('[x](data:text/html,<script>alert(1)</script>)')\n expect(out).not.toMatch(/href=\"data:/i)\n })\n\n test('정상 https 링크는 통과', () => {\n const out = renderMarkdown('[ok](https://example.com)')\n expect(out).toMatch(/href=\"https:\\/\\/example\\.com\"/)\n })\n})\n\ndescribe('sanitizeHtml — DOMPurify defense-in-depth', () => {\n // Node 환경 (jest) 에선 window 부재 → fallback 동작 검증.\n // 브라우저에서의 실제 정화는 통합 테스트 영역.\n\n test('window 부재 시 입력 그대로 통과 (fallback)', () => {\n // 직접 호출 — window undefined 환경에서 DOMPurify 안 도는 게 정상\n const html = '<p>safe</p><script>bad</script>'\n const out = sanitizeHtml(html)\n // 노드 환경에서 fallback 이라 그대로 통과 — 브라우저 환경에선 script 제거됨\n expect(typeof out).toBe('string')\n })\n\n test('window 있을 때 (jsdom mock) — script 차단 확인', () => {\n // jsdom 은 jest 의 testEnvironment 가 jsdom 일 때만 동작 — 본 setup 은 node.\n // 본 test 는 sanitizeHtml 의 *contract* 만 확인 (string in → string out).\n const out = sanitizeHtml('<p>hello</p>')\n expect(out).toContain('hello')\n })\n\n test('빈 문자열 안전 처리', () => {\n expect(sanitizeHtml('')).toBe('')\n })\n\n test('null/undefined fallback', () => {\n // 타입상 string 만 받지만 방어적으로 — 빈 문자열로 처리\n expect(sanitizeHtml('' as any)).toBe('')\n })\n})\n\ndescribe('renderMarkdown — XSS 우회 시도 차단 (defense-in-depth)', () => {\n // 본 테스트들은 marked 의 1차 차단 + DOMPurify 의 2차 차단이 모두 작동하는지\n // 회귀 방지. node 환경에선 sanitizeHtml 이 fallback 이지만, marked 의 1차\n // renderer.html → '' 차단만으로도 대부분 케이스 차단 검증 가능.\n\n test('이미지 onerror 핸들러 시도 — img 태그 자체에 onerror attribute 없음', () => {\n const out = renderMarkdown('![alt](x\" onerror=\"alert(1))')\n // 결과 텍스트에 escape 된 형태로 onerror= 라는 글자 자체는 남을 수 있지만,\n // *실행 가능한 attribute* 형태 (속성 이름으로) 는 없어야 한다.\n // 정밀 검사: <img 태그 안에 onerror=\" 형태가 없는지\n expect(out).not.toMatch(/<img\\b[^>]*\\bonerror\\s*=/i)\n // 또한 escape 안 된 따옴표 + onerror 조합 자체도 없어야\n expect(out).not.toMatch(/\"\\s+onerror=/i)\n })\n\n test('img src=javascript: 차단', () => {\n const out = renderMarkdown('![x](javascript:alert(1))')\n expect(out).not.toMatch(/src=\"javascript:/i)\n })\n\n test('SVG xss 패턴 차단', () => {\n const out = renderMarkdown('<svg onload=alert(1)>')\n expect(out).not.toContain('<svg')\n expect(out).not.toMatch(/onload/i)\n })\n\n test('style=\"javascript:...\" 차단', () => {\n const out = renderMarkdown('[click](#){:style=\"javascript:alert(1)\"}')\n expect(out).not.toMatch(/style=\"javascript:/i)\n })\n\n test('mention span 안의 사용자 token escape', () => {\n const out = renderMarkdown('#\"><script>alert(1)</script>')\n expect(out).not.toMatch(/<script>/i)\n })\n\n test('event handler attribute 차단 (onmouseover 등)', () => {\n // raw HTML 이 marked.renderer.html 에서 빈 문자열로 → onmouseover 자체가 출력 안됨\n const out = renderMarkdown('<a href=\"#\" onmouseover=\"alert(1)\">x</a>')\n expect(out).not.toMatch(/onmouseover/i)\n })\n\n test('vbscript: 스킴 차단 (legacy IE 잔재)', () => {\n const out = renderMarkdown('[x](vbscript:msgbox(1))')\n expect(out).not.toMatch(/href=\"vbscript:/i)\n })\n})\n\ndescribe('renderMarkdown — CJK + 구두점 인접 (회귀 방지)', () => {\n // 이 그룹은 사용자 보고 사례. CommonMark flanking 규칙으로 인해 ** 가\n // 닫히지 않던 케이스들을 모두 회복해야 한다.\n const cases: Array<[string, string, string]> = [\n [\"closing ** 이 punct + CJK 사이\", \"**'rect'**의\", '<strong>&#039;rect&#039;</strong>의'],\n [\"키 **'fillStyle'**의\", \"키 **'fillStyle'**의\", '<strong>&#039;fillStyle&#039;</strong>의'],\n [\"AGV 이름 + 조사\", \"**'AGV-1'**의 위치\", '<strong>&#039;AGV-1&#039;</strong>의'],\n [\"다중 인접 한글\", \"사용자가 **'AGV-1'**컴포넌트를\", '<strong>&#039;AGV-1&#039;</strong>컴포넌트를'],\n [\"bold 안 . 으로 끝\", '**foo.**의', '<strong>foo.</strong>의'],\n [\"bold 안 ) 으로 끝\", '**foo)**의', '<strong>foo)</strong>의'],\n [\"한 ** punct 시작 인접\", \"한**'foo'**\", '한<strong>&#039;foo&#039;</strong>'],\n [\"양쪽 모두 CJK + punct\", \"한**'foo'**글\", '한<strong>&#039;foo&#039;</strong>글']\n ]\n\n for (const [name, input, expected] of cases) {\n test(name, () => {\n const out = renderMarkdown(input)\n expect(out).toContain(expected)\n })\n }\n\n test('정상 케이스는 그대로 marked 가 처리', () => {\n // 닫는 ** 가 공백 직전인 경우 — flanking 통과 (marked native; entity 표기는 &#39;)\n const a = renderMarkdown(\"키 **'fillStyle'** 의\")\n expect(a).toMatch(/<strong>(&#0?39;|')fillStyle(&#0?39;|')<\\/strong>/)\n const b = renderMarkdown(\"Type **'rect'** here\")\n expect(b).toMatch(/<strong>(&#0?39;|')rect(&#0?39;|')<\\/strong>/)\n })\n\n test('사용자 실제 케이스 — 산업용 다크 모드 대시보드', () => {\n const md = \"**공장 설비 및 생산 현황을 한눈에 파악하기 위한 '산업용 다크 모드 대시보드'** 도 확인해보라.\"\n const out = renderMarkdown(md)\n expect(out).toContain('<strong>')\n expect(out).toContain('대시보드')\n })\n\n test('punct 가 끝/시작에 없으면 변경 없음', () => {\n // marked 자체가 처리할 수 있는 케이스 — 우회 안 일어남\n const out = renderMarkdown('한**foo**글')\n expect(out).toContain('<strong>foo</strong>')\n })\n\n test('일본어 인접', () => {\n expect(renderMarkdown(\"これは**'値'**です\")).toContain('<strong>&#039;値&#039;</strong>')\n })\n\n test('중국어 인접', () => {\n expect(renderMarkdown(\"这是**'值'**测试\")).toContain('<strong>&#039;值&#039;</strong>')\n })\n\n test('인라인 코드 안의 ** 는 건드리지 않음', () => {\n // 코드 안의 ** 는 그대로 텍스트로 남아야 함\n const out = renderMarkdown('`**not bold**`')\n expect(out).toContain('<code>**not bold**</code>')\n })\n\n test('코드 펜스 안의 ** 는 건드리지 않음', () => {\n const md = '```\\n**not bold**\\n```'\n const out = renderMarkdown(md)\n expect(out).toContain('<code>')\n // 펜스 안의 ** 는 marked 가 텍스트로 그대로 둠\n expect(out).toMatch(/\\*\\*not bold\\*\\*/)\n })\n\n test('placeholder 토큰 (BAICJKSTRONG…) 이 출력에 노출되지 않음', () => {\n const out = renderMarkdown(\"키 **'fillStyle'**의\")\n expect(out).not.toContain('BAICJKSTRONG')\n expect(out).not.toContain('ENDMARK')\n })\n\n test('치환된 strong 내용은 HTML escape 됨', () => {\n // CJK-fix 경로를 타는 케이스에 < > 가 섞여도 안전하게 escape 되어야 한다.\n // before='', after='의' (CJK), innerStart=', innerEnd=' (둘 다 punct) → fix 트리거.\n const out = renderMarkdown(\"**'<bad>'**의\")\n expect(out).not.toContain('<bad>')\n expect(out).toContain('&lt;bad&gt;')\n expect(out).toContain('<strong>')\n })\n\n test('여러 strong 가 한 줄에 있어도 모두 처리', () => {\n const out = renderMarkdown(\"**'A'**과 **'B'**는 다르다\")\n // 둘 다 strong 으로 렌더되어야 함\n const matches = out.match(/<strong>/g) ?? []\n expect(matches.length).toBe(2)\n })\n})\n\ndescribe('extractCjkStrongs — 내부 동작', () => {\n test('필요 없는 케이스는 stored 비어 있음', () => {\n const r = extractCjkStrongs('regular **bold** text')\n expect(r.stored).toEqual([])\n expect(r.processed).toBe('regular **bold** text')\n })\n\n test('필요한 케이스만 추출', () => {\n const r = extractCjkStrongs(\"키 **'fillStyle'**의\")\n expect(r.stored).toEqual([\"'fillStyle'\"])\n expect(r.processed).toContain('BAICJKSTRONG')\n })\n\n test('인라인 코드 안의 ** 는 추출 안 함', () => {\n const r = extractCjkStrongs(\"`**'foo'**의` 와 텍스트\")\n expect(r.stored).toEqual([])\n expect(r.processed).toContain(\"`**'foo'**의`\")\n })\n\n test('코드 펜스 안의 ** 는 추출 안 함', () => {\n const r = extractCjkStrongs(\"```\\n**'foo'**의\\n```\")\n expect(r.stored).toEqual([])\n })\n\n test('짝이 안 맞는 ** 는 무시', () => {\n const r = extractCjkStrongs('한**foo and 글')\n expect(r.stored).toEqual([])\n })\n})\n\ndescribe('extractMentions — `#token` placeholder 추출', () => {\n test('빈 문자열은 빈 결과', () => {\n const r = extractMentions('')\n expect(r.stored).toEqual([])\n expect(r.processed).toBe('')\n })\n\n test('단일 mention — 영문', () => {\n const r = extractMentions('보드의 #motor1 위치')\n expect(r.stored).toEqual([{ trigger: '#', token: 'motor1' }])\n expect(r.processed).toMatch(/보드의 BAIMENTION0ENDMENTION 위치/)\n })\n\n test('단일 mention — 한글', () => {\n const r = extractMentions('#경광등1 빨갛게')\n expect(r.stored).toEqual([{ trigger: '#', token: '경광등1' }])\n })\n\n test('다중 mention — 순서대로 인덱스', () => {\n const r = extractMentions('#a 와 #b 비교')\n expect(r.stored).toEqual([\n { trigger: '#', token: 'a' },\n { trigger: '#', token: 'b' }\n ])\n expect(r.processed).toMatch(/BAIMENTION0ENDMENTION 와 BAIMENTION1ENDMENTION/)\n })\n\n test('단어 중간 # 는 mention 아님', () => {\n const r = extractMentions('foo#bar 는 무시')\n expect(r.stored).toEqual([])\n })\n\n test('# 직후 공백 (markdown ATX 헤딩) 은 매칭 안 함', () => {\n expect(extractMentions('# Title').stored).toEqual([])\n expect(extractMentions('## Sub').stored).toEqual([])\n expect(extractMentions('### 한글헤딩').stored).toEqual([])\n })\n\n test('인라인 코드 안의 # 는 건드리지 않음', () => {\n const r = extractMentions('use `#define` here')\n expect(r.stored).toEqual([])\n expect(r.processed).toBe('use `#define` here')\n })\n\n test('코드 펜스 안의 # 는 건드리지 않음', () => {\n const r = extractMentions('```\\n#abc in code\\n```\\n그리고 #def 는 mention')\n expect(r.stored).toEqual([{ trigger: '#', token: 'def' }])\n })\n\n test('구두점 직후 # — 매칭', () => {\n const r = extractMentions('(#abc) [#def]')\n expect(r.stored).toEqual([\n { trigger: '#', token: 'abc' },\n { trigger: '#', token: 'def' }\n ])\n })\n\n test('@user 멘션도 추출 — trigger 보존', () => {\n const r = extractMentions('@경철 도와주세요')\n expect(r.stored).toEqual([{ trigger: '@', token: '경철' }])\n })\n\n test('# 와 @ 혼합 — 순서대로', () => {\n const r = extractMentions('@경철 #motor1 같이 봐')\n expect(r.stored).toEqual([\n { trigger: '@', token: '경철' },\n { trigger: '#', token: 'motor1' }\n ])\n })\n\n test('이메일 (user@host) — mention 아님', () => {\n const r = extractMentions('a@b.com 은 이메일')\n expect(r.stored).toEqual([])\n })\n\n test('renderMarkdown 통합 — span 으로 복원', () => {\n const html = renderMarkdown('보드의 #motor1 위치')\n expect(html).toContain('<span class=\"mention\">#motor1</span>')\n })\n\n test('renderMarkdown 통합 — 한글 mention', () => {\n const html = renderMarkdown('#경광등1 빨갛게')\n expect(html).toContain('<span class=\"mention\">#경광등1</span>')\n })\n\n test('renderMarkdown 통합 — heading 과 mention 공존', () => {\n const html = renderMarkdown('# Title\\n그리고 #foo 참조')\n expect(html).toContain('<h1>Title</h1>')\n expect(html).toContain('<span class=\"mention\">#foo</span>')\n })\n\n test('renderMarkdown 통합 — 코드 안 # 는 평범한 code', () => {\n const html = renderMarkdown('use `#define`')\n expect(html).toContain('<code>#define</code>')\n expect(html).not.toContain('<span class=\"mention\">')\n })\n\n test('renderMarkdown 통합 — XSS 안전 (token 의 < > escape)', () => {\n const html = renderMarkdown('#<script>')\n expect(html).not.toContain('<script>')\n // < > 이전까지만 토큰 — 매칭 안 될 수도 있고 매칭되더라도 escape\n if (html.includes('class=\"mention\"')) {\n expect(html).toContain('&lt;')\n }\n })\n})\n\ndescribe('renderMarkdown — `#token{refid:N}` inline marker → data-refid', () => {\n test('marker 없으면 span 에 data-refid 없음', () => {\n const html = renderMarkdown('보드의 #motor1 위치')\n expect(html).toContain('<span class=\"mention\">#motor1</span>')\n expect(html).not.toContain('data-refid')\n })\n\n test('marker 있으면 data-refid embed 되고 marker 자체는 출력 안 됨', () => {\n const html = renderMarkdown('#motor1{refid:35} 빨갛게')\n expect(html).toContain('<span class=\"mention\" data-refid=\"35\">#motor1</span>')\n expect(html).not.toContain('{refid:')\n })\n\n test('다중 mention — 각 token 별 refid 분리', () => {\n const html = renderMarkdown('#a{refid:10} 와 #b{refid:20}')\n expect(html).toContain('data-refid=\"10\">#a</span>')\n expect(html).toContain('data-refid=\"20\">#b</span>')\n })\n\n test('marker 일부만 있을 때 — marker 없는 mention 은 data-refid 없음', () => {\n const html = renderMarkdown('#a{refid:10} 와 #unknown')\n expect(html).toContain('data-refid=\"10\">#a</span>')\n expect(html).toContain('<span class=\"mention\">#unknown</span>')\n })\n\n test('한글 mention — data-refid 정상 embed', () => {\n const html = renderMarkdown('#경광등1{refid:35} 빨갛게')\n expect(html).toContain('data-refid=\"35\">#경광등1</span>')\n })\n\n test('marker 안 본문 — escapeHtml 안전 (XSS)', () => {\n const html = renderMarkdown('#motor1{refid:35} <script>')\n expect(html).not.toContain('<script>')\n expect(html).toContain('data-refid=\"35\"')\n })\n})\n\ndescribe('stripMentionRefids — marker 제거', () => {\n test('단일 marker 제거', () => {\n expect(stripMentionRefids('#motor1{refid:35} 빨갛게')).toBe('#motor1 빨갛게')\n })\n\n test('다중 marker 제거', () => {\n expect(stripMentionRefids('#a{refid:1} 와 #b{refid:2}')).toBe('#a 와 #b')\n })\n\n test('marker 없으면 원본 그대로', () => {\n expect(stripMentionRefids('#motor1 빨갛게')).toBe('#motor1 빨갛게')\n expect(stripMentionRefids('아무 마커 없는 텍스트')).toBe('아무 마커 없는 텍스트')\n })\n\n test('빈/널 안전', () => {\n expect(stripMentionRefids('')).toBe('')\n expect(stripMentionRefids(null)).toBe('')\n expect(stripMentionRefids(undefined)).toBe('')\n })\n})\n\ndescribe('injectMentionRefids — popup picks 를 본문에 marker 주입', () => {\n test('단일 token 주입', () => {\n expect(\n injectMentionRefids('#motor1 빨갛게', [{ token: 'motor1', refid: 35 }])\n ).toBe('#motor1{refid:35} 빨갛게')\n })\n\n test('다중 token — 각각 주입', () => {\n expect(\n injectMentionRefids('#a 와 #b', [\n { token: 'a', refid: 10 },\n { token: 'b', refid: 20 }\n ])\n ).toBe('#a{refid:10} 와 #b{refid:20}')\n })\n\n test('matches 가 없는 token 은 건드리지 않음', () => {\n expect(\n injectMentionRefids('#motor1 와 #other', [{ token: 'motor1', refid: 35 }])\n ).toBe('#motor1{refid:35} 와 #other')\n })\n\n test('이미 marker 있으면 덮어쓰기 안 함 (idempotent)', () => {\n expect(\n injectMentionRefids('#motor1{refid:99} 빨갛게', [{ token: 'motor1', refid: 35 }])\n ).toBe('#motor1{refid:99} 빨갛게')\n })\n\n test('단어 중간 # 는 매칭 안 함', () => {\n expect(\n injectMentionRefids('foo#motor1 끝', [{ token: 'motor1', refid: 35 }])\n ).toBe('foo#motor1 끝')\n })\n\n test('빈 picks / 빈 텍스트 안전', () => {\n expect(injectMentionRefids('#a', [])).toBe('#a')\n expect(injectMentionRefids('', [{ token: 'a', refid: 1 }])).toBe('')\n expect(injectMentionRefids('#a', null)).toBe('#a')\n expect(injectMentionRefids('#a', undefined)).toBe('#a')\n })\n\n test('한글 token 주입', () => {\n expect(\n injectMentionRefids('#경광등1 빨갛게', [{ token: '경광등1', refid: 42 }])\n ).toBe('#경광등1{refid:42} 빨갛게')\n })\n})\n\ndescribe('escapeHtml — HTML 이스케이프', () => {\n test('< > & \" \\' 모두 escape', () => {\n expect(escapeHtml('<script>')).toBe('&lt;script&gt;')\n expect(escapeHtml('a & b')).toBe('a &amp; b')\n expect(escapeHtml('\"q\"')).toBe('&quot;q&quot;')\n expect(escapeHtml(\"'a'\")).toBe('&#039;a&#039;')\n })\n\n test('일반 문자는 변경 없음', () => {\n expect(escapeHtml('hello world')).toBe('hello world')\n expect(escapeHtml('한글 テスト 中文')).toBe('한글 テスト 中文')\n })\n})\n"]}