Fix: Final critical data loss bug in metadata parsing

This commit is contained in:
leeyj
2026-04-18 03:01:50 +09:00
parent fba921181b
commit c0dbeb8f18
2 changed files with 50 additions and 14 deletions
+22 -14
View File
@@ -17,8 +17,9 @@ def parse_metadata(text, default_group=GROUP_DEFAULT):
if group_match:
group_name = group_match.group(1)
# #태그 추출 (마크다운 헤더 및 내부 링크[[#ID]] 방지)
tag_matches = re.finditer(r'(?<!#)(?<!\[\[)#(\w+)', text)
# #태그 추출 (마크다운 헤더 # , ## 및 내부 링크[[#ID]] 방지)
# 태그는 반드시 # 바로 뒤에 영문/숫자/한글이 붙어 있어야 하며, 앞에 다른 문자가 없어야 함
tag_matches = re.finditer(r'(?<!#)(?<!\[\[)(?<!\w)#([^\s\#\d\W][^\s\#]*)', text)
for match in tag_matches:
tags.append(match.group(1))
@@ -32,11 +33,12 @@ def parse_and_clean_metadata(content, ui_group=GROUP_DEFAULT, ui_tags=None):
if not content:
return content, ui_group, ui_tags
# 1. 기존에 생성된 푸터 블록(수평선 + 메타데이터)을 모두 제거
# 전후 공백을 제거한 후, 하단의 수평선(---, ***, ___)과 메타데이터 블록을 반복적으로 탐색하여 제거합니다.
# 1. 기존에 생성된 푸터 블록(수평선 + 메타데이터)을 제거
# 파일의 가장 마지막에 위치한 수평선(---, ***, ___)과 그 뒤에 따라오는 메타데이터($ , #) 줄들만 식별하여 제거합니다.
content = content.strip()
# 패턴: (공백+수평선+공백 + (메타데이터 또는 공백))이 문자열 끝에 1회 이상 반복
content = re.sub(r'(?:\s*[\*\-\_]{3,}\s*(?:[\$\#][\s\S]*?)?\s*)+$', '', content).strip()
# 패턴 설명: 줄바꿈 + 수평선 + 줄바꿈 + (줄 시작이 $ 또는 #이며 뒤에 공백이 없는 줄들의 반복) + 끝
footer_regex = r'\n+[\*\-\_]{3,}\s*\n(?:^[\$\#][^\s\#].*$(?:\n|$))*$'
content = re.sub(footer_regex, '', content, flags=re.MULTILINE).strip()
# 2. 본문에서 기호 정보 추출
content_group, content_tags = parse_metadata(content)
@@ -44,8 +46,8 @@ def parse_and_clean_metadata(content, ui_group=GROUP_DEFAULT, ui_tags=None):
# 3. 본문에서 기호 패턴 삭제
# $그룹 삭제
content = re.sub(r'\$\w+', '', content)
# #태그 삭제 (헤더 및 내부 링크 제외)
content = re.sub(r'(?<!#)(?<!\[\[)#\w+', '', content)
# #태그 삭제 (헤더 및 내부 링크 제외, 태그는 # 뒤에 바로 문자가 와야 함)
content = re.sub(r'(?<!#)(?<!\[\[)(?<!\w)#([^\s\#\d\W][^\s\#]*)', '', content)
content = content.strip()
# 4. 데이터 통합
@@ -75,16 +77,22 @@ def generate_auto_title(content):
if not content:
return ""
# 푸터 제외하고 순수 본문만 추출하여 제목 생성
main_content = re.split(r'\n+---\n', content)[0].strip()
# 푸터 제거 (마지막 수평선 블록 제거 로직과 동일)
footer_regex = r'\n+[\*\-\_]{3,}\s*\n(?:^[\$\#][^\s\#].*$(?:\n|$))*$'
main_content = re.sub(footer_regex, '', content, flags=re.MULTILINE).strip()
if not main_content: return ""
lines = main_content.split('\n')
first_line = lines[0].strip()
# 마크다운 헤더 기호(#) 제거
first_line = re.sub(r'^#+\s+', '', first_line).strip()
# 실제 내용이 있는 첫 번째 줄 찾기 (헤더 기호 제외)
title = ""
for line in lines:
stripped = line.strip()
if not stripped: continue
# 마크다운 헤더 기호(#) 제거
title = re.sub(r'^#+\s+', '', stripped).strip()
if title: break
return first_line[:20]
return title[:20]
def extract_links(text):
"""
+28
View File
@@ -0,0 +1,28 @@
# 버그 조치 보고서: 마크다운 문서 저장 시 데이터 소실 (2026-04-18)
## 1. 버그 내용
- **현상**: 마크다운 형식의 메모(가장 흔하게는 `roadmap.md`)를 저장할 때, 본문 중간에 위치한 수평선(`---`) 이후의 내용이 모두 삭제되는 현상 발생.
- **원인**:
- 메모 하단에 `$그룹`, `#태그`를 추출하여 푸터로 모아주는 로직이 본문 내의 마크다운 수평선(`---`)과 그 뒤에 나오는 헤더(`# 제목`)를 '자동 생성된 푸터'로 오인함.
- 정규식이 너무 탐욕적(Greedy)이었으며, 줄 단위 구분이 명확하지 않아 파일 끝까지의 모든 내용을 푸터로 간주하고 삭제함.
## 2. 조치 사항
### 푸터 식별 로직 정교화 (`app/utils/__init__.py`)
- **수단**: 정규식을 파일의 가장 끝에 위치한 **'수평선 + 순수 메타데이터 줄'**의 조합으로만 한정하도록 수정.
- **적용**: `re.MULTILINE` 플래그를 활용하여 줄 시작(`^`)과 끝(`$`)을 명확히 구분하고, `#` 뒤에 공백이 있는 헤더는 절대로 푸터 구성 요소로 보지 않도록 개선.
- **결과**: 본문 중간의 수평선 및 마크다운 헤더 구조가 완벽히 보존됨.
### 태그 추출 정규식 개선
- **수단**: `#` 뒤에 공백이나 숫자가 먼저 오는 경우(마크다운 헤더 등)를 제외하고, 문자로 시작하는 경우만 태그로 인식하도록 강화.
- **적용**: `(?<!\w)#([^\s\#\d\W][^\s\#]*)` 패턴 적용.
### 자동 제목 생성 로직 개선
- **수단**: 첫 번째 `---` 이전만 본문으로 보던 방식에서, 전체 내용 중 가장 상단의 유의미한 텍스트를 찾아 제목으로 사용하도록 변경.
## 3. 결과 및 확인
- 재현 스크립트(`scratch/repro_bug.py`)를 통해 `roadmap.md`와 같은 복합 마크다운 구조에서도 데이터가 삭제되지 않고 정확히 보존됨을 확인.
- 의도적으로 추가한 푸터 메타데이터는 정상적으로 제거 후 재배치됨을 확인.
## 4. 향후 주의사항
- 텍스트 후처리 로직 수정 시 마크다운의 특수 기호와 충돌할 가능성을 항상 염두에 두어야 함.
- 특히 `[\s\S]*`와 같은 광범위한 매칭은 앵커(`$`)가 있더라도 지양하고, 줄 단위 매칭을 우선할 것.