diff --git a/src/features/conversation/services/conversation-center-mappers.helper.spec.ts b/src/features/conversation/services/conversation-center-mappers.helper.spec.ts index ea83c53..ee48c4a 100644 --- a/src/features/conversation/services/conversation-center-mappers.helper.spec.ts +++ b/src/features/conversation/services/conversation-center-mappers.helper.spec.ts @@ -18,6 +18,10 @@ describe('conversation-center-mappers.helper', () => { 'A & B <3>', ); }); + + it('이중 이스케이프는 한 번만 복원한다(&lt; → <)', () => { + expect(stripHtmlToPreview('&lt;b&gt;')).toBe('<b>'); + }); }); describe('toLastMessagePreview', () => { diff --git a/src/features/conversation/services/conversation-center-mappers.helper.ts b/src/features/conversation/services/conversation-center-mappers.helper.ts index 6c2ea16..fc40ddc 100644 --- a/src/features/conversation/services/conversation-center-mappers.helper.ts +++ b/src/features/conversation/services/conversation-center-mappers.helper.ts @@ -6,14 +6,18 @@ * 공백 정리로 충분하다(저장 원문은 그대로 유지). */ export function stripHtmlToPreview(html: string): string { - return html - .replace(/<[^>]*>/g, ' ') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/\s+/g, ' ') - .trim(); + return ( + html + .replace(/<[^>]*>/g, ' ') + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + // & 복원은 반드시 마지막 — 먼저 풀면 "&lt;" 같은 이중 + // 이스케이프가 두 번 풀려 "<"가 된다 (CodeQL js/double-escaping) + .replace(/&/g, '&') + .replace(/\s+/g, ' ') + .trim() + ); } /** 마지막 메시지 row → 미리보기 텍스트. TEXT는 원문, HTML은 태그 제거. */