From 0cdd7f4d8b8b9e6e342161e189b819ae417f5410 Mon Sep 17 00:00:00 2001 From: jmj Date: Mon, 29 Jun 2026 13:53:33 +0900 Subject: [PATCH] =?UTF-8?q?fix(feedback):=20=ED=95=98=EC=9D=B4=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EC=98=81=EB=AC=B8=20=EB=8B=A8=EC=96=B4=20?= =?UTF-8?q?=EC=A4=91=EA=B0=84=20=EC=98=A4=EA=B0=95=EC=A1=B0=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회의적 max 리뷰(3/3)에서 발견. 키워드를 단어 경계 없이 substring 매칭해 영문 단어 내부를 잘못 강조('AI'→"detail"/"main", 'Go'→"Google"). - HighlightedText: 순수 ASCII term 은 ASCII 경계 lookaround 로 감싸 단어 중간 매칭 차단. 한글/혼용 구절은 경계 모호 + 긴 구절이라 그대로 유지(둘 다 강조 유지). - lookaround 는 zero-width 라 split 캡처그룹 1개 가정 불변. Co-Authored-By: Claude Opus 4.8 --- .../src/features/feedback/ui/HighlightedText.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/feedback/ui/HighlightedText.tsx b/frontend/src/features/feedback/ui/HighlightedText.tsx index 87fb6b25..313a335b 100644 --- a/frontend/src/features/feedback/ui/HighlightedText.tsx +++ b/frontend/src/features/feedback/ui/HighlightedText.tsx @@ -4,6 +4,18 @@ function escapeRegExp(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } +// 출력 가능한 ASCII(공백~틸드)만으로 이뤄진 term — 영문 키워드 판별용(제어문자 제외). +const ASCII_ONLY = /^[\x20-\x7E]+$/ + +// 단일 term → 정규식 조각. 순수 ASCII(영문 키워드 'AI'·'Go' 등)는 단어 중간 매칭을 +// 막기 위해 ASCII 경계 lookaround 로 감싼다(예: 'AI' 가 "detail"·"main" 내부를 강조하지 않게). +// 한글/혼용 구절은 경계 개념이 모호하고 긴 구절이라 오매칭이 드물어 그대로 둔다. +// lookaround 는 zero-width 라 split 의 '캡처 그룹 1개' 가정을 깨지 않는다. +function termToPattern(t: string): string { + const esc = escapeRegExp(t) + return ASCII_ONLY.test(t) ? `(? 로 강조한다. // terms = AI 가 고른 핵심 구절(highlights) ∪ 다음에 채울 키워드(improvementKeywords). // 긴 구절을 먼저 매칭(부분 겹침 방지), 대소문자 무시. mark 는 html2canvas(PDF)에서도 렌더된다. @@ -19,7 +31,7 @@ export function HighlightedText({ new Set(terms.map((t) => (t ?? '').trim()).filter((t) => t.length >= 2)), ).sort((a, b) => b.length - a.length) if (cleaned.length === 0) return null - return new RegExp(`(${cleaned.map(escapeRegExp).join('|')})`, 'gi') + return new RegExp(`(${cleaned.map(termToPattern).join('|')})`, 'gi') }, [terms]) if (!regex || !text) return <>{text}