← ~/parksy-logs/webzine
docs/dev-logs/061-tistory-verify-readability-bugfix-2026-07-14.md
개발일지 061 — 티스토리 발행 파이프라인 실사용 검증 + 버그 5건 수정 + verify_readability.py 신설
[세션] · 061-tistory-verify-readability-bugfix-2026-07-14

일자: 2026-07-14 선행: 060번 (파이프라인 v2.0 "구현 완료" 선언, 단 "publisher.py 실제 테스트 실행"은 ⏸ 미완으로 남김) 이번 세션: 그 미완 항목을 실제로 돌려봄 → 전부 깨져있었음을 raw 증거로 확인 → 근본원인 5건 수정 → 가독성 자동검증 도구 신설


1. 출발점 — "발행됐다"는 로그가 거짓이었다

060번 개발일지 이후 실제로 publisher.py/_tistory_publish.py로 발행을 시도한 첫 실사용 세션. https://programmer-park.tistory.com/71~74 4개 포스트가 이미 "발행 완료" 로그를 남긴 상태였음.

raw 증거 — RSS로 직접 확인:

$ curl -s https://programmer-park.tistory.com/rss
...
<item><title>IT OJT 매뉴얼 003</title><link>.../71</link>...
<item><title>Tistory 네이티브 HTML 테스트</title><link>.../72</link>...
<item><title>인라인 스타일 테스트</title><link>.../73</link>...
<item><title>IT OJT 매뉴얼 003</title><link>.../74</link>...

raw 증거 — 실제 HTML 본문:

$ curl -s https://programmer-park.tistory.com/74 | grep tt_article_useless
<div class="tt_article_useless_p_margin contents_style"><p data-ke-size="size16"></p></div>

4개 전부 동일 — 본문이 완전히 비어있었다.


2. 버그 #1 — tinymce.setContent()만으로는 발행 시 반영 안 됨

_tistory_publish.py의 본문 주입 로직:

tinymce.activeEditor.setContent(html);  // 이것만으로는 부족

setContent는 에디터 화면(iframe)엔 반영되지만, 티스토리의 React 상태(발행 버튼이 실제로 읽는 값)로는 동기화 안 됨.

수정:

ed.setContent(html);
try { ed.fire('change'); } catch(e2) {}
try { ed.fire('input'); } catch(e2) {}
try { ed.save(); } catch(e2) {}

검증: 수정 후 재발행 → curl 실측 본문 길이 17,000+자 확인.


3. 버그 #2 — 제목 중복 렌더링

generator.pybuild_booklet()이 커버에 <h1>{title}</h1>을 항상 찍는데, 초안 마크다운 첫 줄도 # 제목으로 시작해서 같은 제목이 두 번 나옴.

수정:

content_md = re.sub(r'^#\s+.+\n+', '', content_md, count=1)

4. 버그 #3 — :::alert-blue 제목처럼 같은 줄에 제목 붙으면 통째로 미변환

정규식 r':::alert-(\w+)\n(.*?):::'은 타입 바로 뒤에 \n이 와야 매칭되는데, 초안에 :::alert-blue 💎 요약\n처럼 타입 뒤에 제목 텍스트가 붙어있어서 매칭 자체가 실패.

1차 수정 시도((?:[ \t]+(.+))?)도 DOTALL 하의 그리디 .+가 줄바꿈까지 삼켜서 제목 그룹이 본문까지 통째로 먹어버리는 2차 버그 발생 — [^\n]+로 줄 안으로 제한해서 해결.

:::callout 도 동일 패턴의 버그, 동일 방식으로 수정.


5. 버그 #4 — --- 구분선이 <hr>로 안 바뀌어 화면에 텍스트로 노출

md_to_html()에 수평선 변환 규칙 자체가 없었음. 추가:

html = re.sub(r'^-{3,}$', '<hr>', html, flags=re.MULTILINE)

6. 버그 #5 (제일 큰 것) — 제목(h2/h3)이 화면에서 안 보임

로컬 파일 기준 가독성 검증은 통과했는데, 실제 라이브 페이지에서는 h2/h3가 거의 안 보였다. 박씨가 직접 "안 보여"라고 지적. Playwright headless 스크린샷 + 크롭으로 raw 확인:

근본원인 조사 — CDP CSS.getMatchedStylesForNode로 실제 승리 규칙 확인:

regular | .article_view h1, .article_view h2, .article_view h3, .article_view h4 | color: rgba(0,0,0,.87)
regular | .book-root h1...h6 | color:var(--text-bright)          ← 내 규칙
regular | #article-view h2, #article-view h3, #article-view h4 | color: #000   ← 실제 승자

티스토리 스킨이 ID 선택자(#article-view)로 h2/h3/h4 색을 강제하고 있어서, 클래스 선택자인 내 규칙은 명시도 규칙상 절대 이길 수 없었다. li(.article_view ul li), 링크(#article-view a), blockquote도 동일 패턴으로 확인됨.

수정: booklet.css + generator.pyBOOKLET_CSS 양쪽에 색상/배경 선언 81건에 일괄 !important 적용 (정규식 스크립트로 자동 처리, 수작업 아님).

검증: 수정 후 라이브 재검증 — 첫 시도는 여전히 .btn-purple 대비 1.74:1 실패 → CSS에 .btn-purple 클래스 자체가 없었던 걸 발견 (gold/blue/red 변형도 함께 없었음) → 4종 추가 → 재검증 통과.


7. verify_readability.py 신설 — "발행됐다"를 다시는 안 믿기 위한 장치

publishing/verify_readability.py — Playwright 기반, 실제 렌더링을 열어:

  1. .book-root 내부 텍스트 보유 요소 전수 순회, 조상 체인 알파 합성으로 실제 배경색 계산 → WCAG AA(4.5:1) 대비 미달 검출. (반투명 오버레이를 배경 자체로 오인하는 오탐 방지 로직 포함)
  2. details.acc(아코디언) 전수 클릭 → open 속성이 실제로 토글되는지 인터랙션 검증
사용: python3 verify_readability.py <url 또는 file://경로>
실패 시 exit 1 + 문제 목록. --publish 뒤에는 항상 이걸 통과해야 "완료"로 보고한다.

로컬 파일 검증 → 라이브 URL 재검증까지 이중으로 통과한 뒤에야 "완료"로 처리하는 걸 이번 세션 이후 규칙으로 확정.


8. 부수 정리 — 유령 포스트 4건 비공개 전환

71~74번(전부 버그 #1로 인해 빈 글이었던 것들) 비공개 전환 완료. programmer-park.tistory.com/75 하나만 공개 상태로 최종 확정.

참고: Tistory manage/posts 페이지의 행별 옵션 버튼은 CSS :hover로만 노출되는 0×0 크기 요소라 Playwright 일반 클릭은 전부 타임아웃남 — .hover() 선행 후 force:true 클릭으로 해결.


9. 변경 파일

파일 변경
publishing/generator.py 제목중복 제거, alert/callout 정규식 수정, <hr> 변환 추가, btn 색상 4종 추가, 전체 색상 규칙 !important
publishing/template/booklet.css 동일 내용 미러링
publishing/verify_readability.py 🟢 신규 — 가독성/인터랙션 자동 검증기
dtslib-papyrus/tools/mcp_distributor/_tistory_publish.py setContent 후 change/input 이벤트 + save() + getContent 검증 추가

10. 다음에 할 것

  1. parksy_distribute_tistory MCP 도구 자체의 asyncio.run() 중첩 에러(서버가 이미 이벤트루프 안이라 발생) — 이번엔 spec 파일만 재사용해서 별도 프로세스로 우회했음. MCP 서버 코드 자체 수정은 미완.
  2. _tistory_console.pylist_posts() 셀렉터도 깨져있음("통계"만 반환) — 우선순위 낮음, RSS로 대체 가능해서 급하지 않음.
  3. publisher.py(parksy-logs 자체 버전, TinyMCE 감지 실패로 애초에 안 쓴 것)는 여전히 미수정 — _tistory_publish.py가 사실상 대체재로 검증됨.

"발행 로그가 있다고 발행된 게 아니다. 화면을 직접 열어봐야 발행된 거다."

cat: sessionsrc: docs/dev-logs/061-tistory-verify-readability-bugfix-2026-07-14.md
index F2record F3whitepaper F4reports F5plans F6sessions