diff --git a/app/utils/__init__.py b/app/utils/__init__.py index c05a831..42ca6b0 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -27,48 +27,28 @@ def parse_metadata(text, default_group=GROUP_DEFAULT): def parse_and_clean_metadata(content, ui_group=GROUP_DEFAULT, ui_tags=None): """ - 본문에서 메타데이터($ , #)를 추출하고 삭제한 뒤, UI 입력값과 합쳐 최하단에 재배치합니다. + [비파괴적 버전] 본문에서 메타데이터를 추출하되, 본문의 내용은 훼손하지 않습니다. + 단, 이전에 자동 생성되었던 푸터 블록만 식별하여 제거합니다. """ if ui_tags is None: ui_tags = [] if not content: return content, ui_group, ui_tags - # 1. 기존에 생성된 푸터 블록(수평선 + 메타데이터)을 제거 - # 파일의 가장 마지막에 위치한 수평선(---, ***, ___)과 그 뒤에 따라오는 메타데이터($ , #) 줄들만 식별하여 제거합니다. + # 1. 기존에 자동 생성되었던 푸터 블록만 제거 (원본 본문 보호를 위함) content = content.strip() - # 패턴 설명: 줄바꿈 + 수평선 + 줄바꿈 + (줄 시작이 $ 또는 #이며 뒤에 공백이 없는 줄들의 반복) + 끝 footer_regex = r'\n+[\*\-\_]{3,}\s*\n(?:^[\$\#][^\s\#].*$(?:\n|$))*$' content = re.sub(footer_regex, '', content, flags=re.MULTILINE).strip() - # 2. 본문에서 기호 정보 추출 + # 2. 본문에서 기호 정보 추출 (추출만 수행하고 본문에서 삭제는 하지 않음) content_group, content_tags = parse_metadata(content) - # 3. 본문에서 기호 패턴 삭제 - # $그룹 삭제 - content = re.sub(r'\$\w+', '', content) - # #태그 삭제 (헤더 및 내부 링크 제외, 태그는 # 뒤에 바로 문자가 와야 함) - content = re.sub(r'(? `; } else { - // 본문에서 하단 메타데이터 블록(--- 이후)을 제외하고 렌더링 (중복 표시 방지) - let content = memo.content || ''; - const footerIndex = content.lastIndexOf('\n\n---\n'); - const displayContent = footerIndex !== -1 ? content.substring(0, footerIndex) : content; + // 렌더링 시에는 태그와 그룹명을 시각적으로 가립니다 (원본 보존 정책) + const displayContent = stripMetadata(memo.content || ''); // marked로 파싱한 후 DOMPurify로 살균하여 XSS 방지 htmlContent = DOMPurify.sanitize(marked.parse(displayContent)); diff --git a/static/js/components/ModalManager.js b/static/js/components/ModalManager.js index e337077..e6e172d 100644 --- a/static/js/components/ModalManager.js +++ b/static/js/components/ModalManager.js @@ -164,21 +164,10 @@ export const ModalManager = { const memo = memos.find(m => m.id == id); if (!memo) return; - import('../utils.js').then(({ parseInternalLinks, fixImagePaths }) => { - // 메모 본문과 메타데이터 푸터 분리 렌더링 - let content = memo.content || ''; - const footerIndex = content.lastIndexOf('\n\n---\n'); - let html; - - if (footerIndex !== -1) { - const mainBody = content.substring(0, footerIndex); - const footerPart = content.substring(footerIndex + 5).trim(); // '---' 이후 - - html = DOMPurify.sanitize(marked.parse(mainBody)); - html += ``; - } else { - html = DOMPurify.sanitize(marked.parse(content)); - } + import('../utils.js').then(({ parseInternalLinks, fixImagePaths, stripMetadata }) => { + // 렌더링 시에는 태그와 그룹명을 시각적으로 가립니다 (원본 보존 정책) + const displayContent = stripMetadata(memo.content || ''); + let html = DOMPurify.sanitize(marked.parse(displayContent)); html = parseInternalLinks(html); html = fixImagePaths(html); diff --git a/static/js/utils.js b/static/js/utils.js index 4f170eb..84cfd71 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -41,3 +41,31 @@ export function fixImagePaths(html) { return `\\/\\?\\-"; + + // 2. 본문 내 $그룹 제거 + const groupRegex = new RegExp("\\$[^" + excludeChars + "]+", "g"); + processed = processed.replace(groupRegex, ''); + + // 3. 본문 내 #태그 제거 (마크다운 헤더 및 내부 링크 제외) + // 패턴 설명: 공백 또는 줄 시작 뒤의 #로 시작하고, 뒤에 숫자가 아닌 문자가 오는 태그 매칭 + const tagRegex = new RegExp("(^|\\s)#[^" + excludeChars + "0-9]+[^" + excludeChars + "]*", "g"); + processed = processed.replace(tagRegex, '$1'); + + return processed.trim(); +}