Task #639: 한컴 호환 — cover-style 페이지 자동 쪽번호 미표시 (closes #639)#641
Task #639: 한컴 호환 — cover-style 페이지 자동 쪽번호 미표시 (closes #639)#641planet6897 wants to merge 12 commits into
Conversation
aift.hwp 페이지 2, 3 (큰 표만 있는 cover-style) 쪽번호 미표시 메커니즘 분석. Task edwardkim#634 종료 후 분리된 후속 issue. 5가지 가설 (cover-style 휴리스틱 / 셀 내부 PageHide / paragraph header 비트 / 표 attr 비트 / 한컴 자체 휴리스틱) 데이터 기반 검증 + 권고안 결정. 본 task 는 read-only 분석. 결정적 룰 발견 시에만 별도 fix issue 분리.
aift.hwp 페이지 2, 3 (큰 표만 있는 cover-style) 미표시 메커니즘 분석 사전 단계. 데이터 수집: - dump-pages 페이지 1~6 구조 (items, body 사용량, 표 비율) - dump 페이지 1/2/3/6 host paragraph (cc, ps_id, controls) - 문서 전체 PageHide 컨트롤 (정확히 2개: 페이지 4 para 2.34, 페이지 5 para 2.54) - 한컴 PDF 측정 (pypdf): 페이지 1, 6, 7 표시 / 페이지 2, 3, 4, 5 미표시 확정 - 표 attr 매트릭스 (페이지 1: 0x04000006, 2: 0x0600000e, 3: 0x0400000e, 6: 0x0600000e) Stage 0 사전 판정: - H1 (cover-style 휴리스틱): 유력 — 페이지 2/3 만 items=1 + 표 89~98% - H2 (셀 내부 PageHide): 기각 — 문서 전체 PageHide 가 정확히 2개 - H3 (paragraph header 비트): 기각 — 페이지 2 (미표시) host 와 페이지 6 (표시) host 거의 동일 - H4 (표 attr 비트): 기각 — 페이지 6 표 attr = 페이지 2 표 attr (0x0600000e) - H5 (한컴 휴리스틱): H1 변형, Stage 2 교차 검증 후 결정 본 단계는 read-only 분석. 코드 변경 없음.
도구: examples/inspect_637.rs (paragraph header raw + cover-candidate enumeration).
검증 결과:
- H1 cover-style 휴리스틱 → 결정적 룰로 정형화: 페이지가 items=1 인 단일
완전한 Table (PartialTable 아님) 을 포함하고 tac=false 일 때 한컴은
쪽번호 미표시.
- 174 샘플 전수 조사: aift.hwp p2, p3 만 매칭 (전체의 0.06%, 한컴 미표시 일치)
- aift.hwp 의 tac=true items=1 페이지 (p74, p75) 는 한컴 표시 — tac=false
가 결정적 분리자임을 검증
- H2, H3, H4 최종 기각:
* H2: 문서 전체 PageHide 정확히 2개 (페이지 4 para 2.34, 페이지 5 para 2.54)
* H3: 페이지 2 host (미표시) 와 페이지 6 host (표시) 의 raw paragraph
header 가 ps_id 외 byte-for-byte 동일 (cc=9, mask=0x00000800,
break_raw=0x04, raw_header_extra 12B 완전 동일)
* H4: 페이지 6 (표시) 표 attr = 0x0600000e (페이지 2 attr 와 동일)
- H5 (한컴 휴리스틱): H1 의 정확형 등가
회귀 위험: 매우 낮음 (174 샘플 중 2 페이지만 영향, 모두 정합성 개선).
Stage 3 사전 권고: 시나리오 (a) 별도 fix issue 분리 후 본 issue close-as-analysis.
본 단계는 read-only 분석. 코드 변경 없음 (example 추가만).
분석 완료 (Stage 0 → Stage 1 → Stage 3, Stage 2 α 결정으로 생략). 결과: - H1 cover-style 휴리스틱이 결정적 룰로 확정: 페이지 items=1 + 완전한 Table (PartialTable 아님) + tac=false → 한컴 미표시 - 174 샘플 전수 조사: aift p2, p3 만 매칭 (전체의 0.06%) - H2/H3/H4 최종 기각 - 회귀 위험 매우 낮음 (영향 페이지 2건, 모두 정합성 개선) 권고: 시나리오 (a) — 별도 fix issue 분리 후 본 issue close-as-analysis-complete. 코드 변경 0 (examples/inspect_637.rs 분석 도구 추가만). orders/20260506.md 갱신 (edwardkim#637 완료 상태 반영).
GitHub Issue 처리: - edwardkim#637 close-as-analysis-complete (분석 완료 코멘트 포함) - edwardkim#639 등록 (fix issue: cover-style 페이지 자동 쪽번호 미표시) orders 갱신: - edwardkim#637 상태: "완료 (분석 close, fix issue edwardkim#639 분리)" - edwardkim#639 신규 등록 entry 추가 (등록만, 진행 대기) milestone v1.0.0 설정은 planet6897 권한 부족으로 실패 — 메인테이너 수동 설정 필요.
한컴 호환 — cover-style 페이지 (items=1 + 완전한 Table + tac=false) 자동 쪽번호 미표시 구현 task. Task edwardkim#637 분석에서 도출된 결정적 룰의 코드 구현. 본질: src/renderer/pagination/engine.rs:finalize_pages 의 page_hide 결정 직후 cover-style 룰 추가 (예상 +15~25 LOC). 단계: 0a 수행계획서 → 0b 구현계획서 → Stage 1 TDD RED → Stage 2 fix → Stage 3 회귀 검증. 회귀 위험 매우 낮음 (Task edwardkim#637 174 샘플 전수 조사로 영향 페이지 2건 확인). orders 갱신 (edwardkim#639 진행중).
정확한 코드 변경 시그니처/위치/edge case 명시.
핵심 변경:
- src/renderer/pagination/engine.rs (+30 LOC):
* finalize_pages 시그니처 확장 (paragraphs &[Paragraph] 추가)
* 신규 헬퍼 is_cover_style_page (단일 단 + items=1 + 완전 Table + tac=false)
* page.page_hide.is_none() 가드로 기존 PageHide 우선
* 룰 매칭 시 PageHide { hide_page_num: true, ..Default::default() } 설정
- src/renderer/layout/integration_tests.rs (+150 LOC):
* test_639_aift_page2_cover_style_no_page_number (RED→GREEN)
* test_639_aift_page3_cover_style_no_page_number (RED→GREEN)
* test_639_aift_page1_shows_page_number (회귀 가드)
* test_639_aift_page6_shows_page_number (회귀 가드)
* test_639_aift_page74_tac_true_table_shows_page_number (회귀 가드, tac=true 결정적 분리자)
Edge case 9개 검토 (None 처리, 다단, PartialTable, Shape, 기존 PageHide 우선 등).
PageNumberAssigner 와의 상호작용: page.page_hide 만 설정, page_number 는
단조 증가 그대로 유지 (Task edwardkim#634 의 페이지 4, 5 PageHide 동작과 일치).
회귀 검증 전략: cargo test sweep + 174 샘플 page_hide diff sweep.
aift.hwp 페이지 2, 3 cover-style 미표시 룰 검증 통합 테스트 추가. fix 미적용 상태에서 RED 시나리오 정확히 재현. 검출 패턴: - render_page_svg_native 의 footer 쪽번호 "- N -" 는 글자별 분리된 <text> 요소로 출력 (y="1079.16", font-size="10") - 헬퍼 count_footer_page_number_glyphs 로 마커 글리프 수 카운트 - 0 = 미표시, 3+ = 표시 5건 테스트: - test_639_aift_page2_cover_style_no_page_number (RED, FAIL 글리프 3) - test_639_aift_page3_cover_style_no_page_number (RED, FAIL 글리프 3) - test_639_aift_page1_shows_page_number (회귀 가드, PASS) - test_639_aift_page6_shows_page_number (회귀 가드, PASS) - test_639_aift_page74_tac_true_table_shows_page_number (회귀 가드, PASS) Stage 2 fix 적용 시 페이지 2, 3 도 PASS 전환 예상. 검증: $ cargo test --release --lib test_639 test result: FAILED. 3 passed; 2 failed; 0 ignored (2 fail 는 페이지 2, 3 RED 의도 — Stage 2 fix 후 GREEN 전환) probe_637.rs: SVG footer 검출 패턴 사전 조사용 일회성 도구 (Stage 3 정리).
룰 구현: 페이지 items=1 + 단일 단 + 완전한 Table (PartialTable 아님) + tac=false → page.page_hide.hide_page_num=true 자동 설정. Task edwardkim#637 분석에서 도출된 결정적 룰. 174 샘플 중 aift.hwp 페이지 2, 3 만 매칭 (전체의 0.06%, 한컴 PDF 미표시와 일치). 구현 발견: rhwp 의 페이지네이션은 두 경로 존재. - TypesetEngine::typeset_section (기본 main path, src/renderer/typeset.rs) - Paginator::paginate_with_measured (RHWP_USE_PAGINATOR=1 fallback, src/renderer/pagination/engine.rs) 두 경로 모두 동일 fix 적용: - finalize_pages 시그니처에 paragraphs: &[Paragraph] 추가 - 기존 PageHide 적용 직후 cover-style 룰 분기 (page.page_hide.is_none() 가드) - 신규 헬퍼 is_cover_style_page (단일 단 + items=1 + Table + tac=false) 검증: - Stage 1 RED 5건 모두 GREEN 전환: test_639_aift_page2_cover_style_no_page_number .. ok test_639_aift_page3_cover_style_no_page_number .. ok test_639_aift_page1_shows_page_number .. ok (회귀 가드) test_639_aift_page6_shows_page_number .. ok (회귀 가드) test_639_aift_page74_tac_true_table_shows_page_number .. ok (tac=true 분리자) - 전체 cargo test sweep: 1139 + 76+ 통합 PASS, 0 failed - clippy warning 0 코드 변경: - src/renderer/pagination/engine.rs: +29 / -2 - src/renderer/typeset.rs: +29 / -2 본 변경은 렌더 시점 derived state 만 영향 (라운드트립/HWPX 호환성 0 영향).
회귀 검증: - 174 샘플 룰 매칭 재확인: aift.hwp p2, p3 만 (Task edwardkim#637 일치) - cargo test --release: 1139 + 76+ 통합 테스트 PASS, 0 failed - clippy: warning 0 - aift.hwp 페이지 카운트 77 무변화 - 다른 173 샘플 0 영향 (회귀 위험 0 확정) aift.hwp 페이지별 footer 글리프 카운트 검증: - p1 (표시): 3 ✓ - p2 (cover-style fix): 0 ✓ - p3 (cover-style fix): 0 ✓ - p4, p5 (PageHide 기반): 0 유지 ✓ - p6 (정상 표시): 3 ✓ - p74 (tac=true items=1): 4 ✓ (tac=true 분리자) 산출물: - mydocs/working/task_m100_639_stage3.md (Stage 3 보고서) - mydocs/report/task_m100_639_report.md (최종 보고서) - orders/20260506.md 갱신 (edwardkim#639 완료) - examples/probe_637.rs 정리 (Stage 1 일회성 도구) 본 task 완료. close-as-fixed 대기.
|
PR #640 supersede 확정 작업지시자 결정 (α — 분석+fix 함께 머지) 에 따라 PR #640 (Task #637 분석) 을 close 하고
10 커밋 (5 task637 + 5 task639) 단일 머지로 한컴 호환 cover-style fix 완료. |
…`);` + `}` 누락 보정 origin/pr-task639 의 머지 커밋 (10599bc) 에서 conflict 해결 시 test_639_aift_page74_tac_true_table_shows_page_number 의 마지막 assert! 가 닫는 `);` + 함수 닫는 `}` 누락으로 cargo build --tests 컴파일 실패. Task edwardkim#624 doc comment 가 assert! 매크로 인자 영역으로 흡수되어 syntax error. 정정: assert!(...) 를 정상 종료하고 함수 `}` 추가 → Task edwardkim#624 테스트가 별도 함수로 인식. 검증: - cargo build --release --tests 성공 - test_639 5건 + test_624 1건 모두 통과
본 PR close — 본질 결함 발견 반영 (작업지시자 결정 옵션 1)PR #638 (Task #634) close 시 메인테이너 (@edwardkim) 가 본 환경 페이지네이션의 본질 결함 발견 (상세 코멘트). 작업지시자 결정 (옵션 1 — 본 PR close + 새 PR 작성) 에 따라 본 PR close 합니다. 본질 결함 요약aift.hwp page 2 의 셀[167] paragraph[3] 에 PageHide 컨트롤이 정확히 인코딩 (6 필드 모두 true). 본 환경 결함 3 곳:
본 PR base 의 Task #637 H2 가설 ("셀 내부 PageHide") 기각이 본 환경 결함 #1 로 인한 잘못된 측정 결과. 본 PR 접근의 한계cover-style 휴리스틱 (
후속 진행본질 정정 (셀 안 PageHide 정확 검출 + 결함 3건) 새 PR 로 재제출 예정. cover-style 룰 ( 본 PR 의 분석 도구 ( 회귀 가드 신규 권고 (메인테이너 코멘트 인용):
|
PR #641 close — PR #711 (Task #705) 본질 정정 영역으로 재검토 진행본 PR 영역의 cover-style 휴리스틱 (items=1 + Table + tac=false) 영역은 본 환경 영역의 셀 안 PageHide 무시 결함 영역의 우회 (workaround) 영역으로 분석되었습니다. PR #638 메인테이너 댓글 영역에서 안내된 본질 정정 3 영역 (셀 안 PageHide 수집 + hide_border/hide_fill 가드 + dump 분기) 영역과 다른 접근법 영역. supersede 안내컨트리뷰터 (@planet6897) 영역의 후속 PR #711 (Task #705) 영역에서 본질 정정 영역 진행:
본 PR close 사유본 PR 영역의 11 commits 영역의 분석 (Task #637) + cover-style 룰 (Task #639) 영역의 가치 영역 영역 인정 영역 영역. 다만 본질 정정 영역으로의 영역 영역 영역 영역으로 본 PR 의 우회 접근 영역은 PR #711 영역으로 대체 영역. 본 PR 영역은 devel 영역에 반영 영역 부재 영역. PR #711 영역에서 본질 정정 영역으로 재검토 영역 진행 영역. 가치 영역 보존 영역본 PR 영역의 분석 영역 (Task #637 5 commits) + 거버넌스 산출물 12 파일 영역의 작업 영역 — PR #711 의 본질 정정 영역의 기반 영역 정합 영역. 컨트리뷰터 영역의 11 사이클 PR 누적 영역의 가치 영역 영역 영역 정합. 본 PR close + PR #711 영역에서 본질 정정 영역으로 재검토 영역 진행하겠습니다. 컨트리뷰터의 영역 학습 영역 + 본질 정정 영역 영역의 재제출 영역 감사합니다. |
PR #638 (Task #634, 한컴 호환 쪽번호) close 영역 + PR #641 (Task #639, cover-style 룰) close 영역 처리: - 두 PR 모두 devel 반영 부재 (작업지시자 결정 영역 정합) - 컨트리뷰터 (@planet6897) 영역의 후속 PR #711 (Task #705) 영역에서 본질 정정 영역 진행 PR #638 영역의 본질 결함 발견: - pagination/engine.rs 셀 안 PageHide 무시 (page 2 쪽번호 결함의 본질) - layout.rs hide_border/hide_fill 가드 부재 - main.rs dump 셀 안 PageHide 분기 부재 → 컨트리뷰터에게 정석 처리 안내 댓글 등록 영역 PR #641 영역의 cover-style 휴리스틱: - 본 환경 결함 영역의 우회 (workaround) 영역으로 분석 - 174 샘플 영역 중 영향 페이지 2 영역만 매칭 (PR #711 영역의 6 샘플 영역 영역 영역) - 작업지시자 결정: 본 PR close + PR #711 본질 정정으로 재검토 영역 archives 이동: - mydocs/pr/archives/pr_638_review.md - mydocs/pr/archives/pr_641_review.md - mydocs/pr/archives/pr_641_close_report.md (close 결정 + supersede 안내) 5/8 orders 영역 신규: - mydocs/orders/20260508.md - PR #684 (5/8 처리 완료) + PR #638 (close) + PR #641 (close, supersede PR #711) entry 본 사이클 (5/7~5/8) close 패턴 누적: - PR #638 (본질 결함 발견) - PR #641 (우회 접근 → PR #711 supersede) - 메인테이너 게이트웨이 방식의 권위 사례 영역 강화 누적 본 사이클 PR 처리 누적: 17건 (15 merged + 2 closed)
- engine.rs + typeset.rs 두 경로 모두에 collect_pagehide_in_table 헬퍼 추가 - 결함 #1 본질: TypesetEngine::typeset_section 가 main path, Paginator::paginate_with_measured 는 RHWP_USE_PAGINATOR=1 fallback - 두 경로 동시 정정 필요 (PR edwardkim#641 description 의 두 경로 양분 정합) - 외부 paragraph index pi 그대로 사용 → 페이지 매핑 정합성 유지 - 중첩 표 (depth 2+) 재귀 보존 (실측 0건이지만 미래 케이스 대비) 회귀 sweep: 1123 passed, 0 failed (RED 4건 → GREEN, 기존 1119건 유지) Refs edwardkim#705
- engine.rs + typeset.rs 두 경로 모두에 collect_pagehide_in_table 헬퍼 추가 - 결함 #1 본질: TypesetEngine::typeset_section 가 main path, Paginator::paginate_with_measured 는 RHWP_USE_PAGINATOR=1 fallback - 두 경로 동시 정정 필요 (PR #641 description 의 두 경로 양분 정합) - 외부 paragraph index pi 그대로 사용 → 페이지 매핑 정합성 유지 - 중첩 표 (depth 2+) 재귀 보존 (실측 0건이지만 미래 케이스 대비) 회귀 sweep: 1123 passed, 0 failed (RED 4건 → GREEN, 기존 1119건 유지) Refs #705
PR #638/#641 close 시 작업지시자 (메인테이너) 가 발견한 본 환경 결함 3건의 본질 정정. PR #641 (cover-style 휴리스틱 우회) 폐기 후 작업지시자 권고 영역의 본질 정정 supersede. 본질 결함 3건: 1. pagination/engine.rs:519-544 + typeset.rs:2120 — page_hides 수집이 본문 paragraph 만 → 셀 안 PageHide 무시 (두 페이지네이션 경로 양분 정합) 2. layout.rs:411-422 — build_page_background()/build_page_borders() 호출에 hide_fill/hide_border 가드 부재 3. main.rs:1665-1670 — dump 셀 안 controls 매칭에서 PageHide 분기 부재 정정: - collect_pagehide_in_table 재귀 함수 (engine.rs + typeset.rs 두 경로 동기) - layout.rs 의 hide_fill/hide_border 가드 추가 — 6 필드 모두 가드 정합화 - main.rs dump 셀 안 PageHide 분기 추가 Stage 0 198 sample sweep 측정: - 셀 안 PageHide 13건 / 6 샘플 (PR #640 H2 측정 누락 영역의 정량 확인) - 영향 샘플: aift, 2022 국립국어원, KTX, kps-ai, tac-img-02 hwp+hwpx 회귀 가드 (test_705_*) 6건: - aift p2 셀[167] 6 필드 모두 true 검증 - aift p3 셀[31] page_num 검증 - 국립국어원/KTX 셀 안 PageHide 매핑 검증 - 본문 + 셀 안 합산 카운트 검증 작업지시자 시각 판정 ★ 통과 (2026-05-09): - 한컴 PDF 권위본 (pdf/2022년 국립국어원 업무계획-2022.pdf page 3 마지막 줄 "- 1 -") 정합 - 한컴 편집기 [감추기] 다이얼로그 6 필드 영역 정합 - aift.hwp page 2 + 국립국어원 page 3 등 셀 안 PageHide 영역 모두 정합 메인테이너 가드 갱신 commit (3253f7d): - test_634_gukrip_page3_shows_page_number: count == 3 → count == 0 (rhwp 의 한컴 부정합 행위 보존 → 한컴 권위 정합으로 의도 변경) 검증: - cargo test --release: lib 1173 (+6 test_705 신규) + 통합 ALL GREEN, failed 0 - cargo clippy --release: 신규 경고 0 - 광범위 sweep 7 fixture / 170 페이지 / diff 2 (aift p2/p3 의도된 변경, 다른 fixture 회귀 0) - WASM 빌드 4,606,179 bytes PR supersede 영역: - PR #638 (Task #634) close → src 무변경 회귀 가드만 머지 - PR #640 (Task #637) close → 분석 docs (H2 측정 누락 영역의 정량 확인) - PR #641 (Task #639) close → cover-style 휴리스틱 폐기 (작업지시자 권고) - PR #711 (Task #705) — 본질 정정 supersede Closes #705 Co-Authored-By: Jaeook Ryu <jaeook.ryu@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- mydocs/pr/archives/pr_711_review.md (검토 문서, 시각 판정 결과 반영)
- mydocs/pr/archives/pr_711_report.md (처리 보고서, 가드 갱신 명시)
- mydocs/plans/archives/task_m100_705{,_impl}.md
- mydocs/orders/20260509.md: PR #711 행 + 본 사이클 패턴 라인 추가
처리 결과:
- 옵션 A — 7 commits cherry-pick + 메인테이너 가드 갱신 + no-ff merge (2bc982c)
- 작업지시자 시각 판정 ★ 통과 — 한컴 정답지 정합 확정
- test_634_gukrip_page3 가드 갱신 (count=3 → 0, 메인테이너 commit 3253f7d)
- PR #638/#640/#641 close → PR #711 본질 정정 supersede
- Issue #705 close 자동 정합
부수 사실:
- PR #706 form-002 패턴 정합 영역 의 두 번째 사례
- `feedback_visual_judgment_authority` + `feedback_visual_regression_grows` 권위 사례 강화
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
핵심 발견 — 두 페이지네이션 경로 존재
rhwp 의 페이지네이션은 두 경로 양분:
`render_page_svg_native` → `build_page_tree` → `find_page` 경로는 TypesetEngine 사용.
페이지 단위 derived state (page_hide 등) 변경 시 양쪽 동일하게 적용 필수.
구현 룰
```rust
// finalize_pages 내부, 기존 PageHide 적용 직후
if page.page_hide.is_none() && Self::is_cover_style_page(page, paragraphs) {
page.page_hide = Some(crate::model::control::PageHide {
hide_page_num: true,
..Default::default()
});
}
fn is_cover_style_page(page: &PageContent, paragraphs: &[Paragraph]) -> bool {
if page.column_contents.len() != 1 { return false; }
let items = &page.column_contents[0].items;
if items.len() != 1 { return false; }
let (para_idx, ctrl_idx) = match &items[0] {
PageItem::Table { para_index, control_index } => (*para_index, *control_index),
_ => return false,
};
let Some(para) = paragraphs.get(para_idx) else { return false; };
let Some(Control::Table(t)) = para.controls.get(ctrl_idx) else { return false; };
!t.common.treat_as_char
}
```
`page.page_hide.is_none()` 가드로 기존 PageHide 컨트롤 우선. 매칭 시 `hide_page_num=true` 만 설정.
검증
신규 통합 테스트 (5건)
검출 패턴: SVG 의 `<text y="1079.16" font-size="10">` 글리프 카운트.
174 샘플 룰 매칭 재확인
```
Total pages matching rule (items=1 + Table + tac=false): 2
aift.hwp page 2
aift.hwp page 3
```
Task #637 사전 조사 결과와 정확 일치. 174 샘플 중 영향 페이지 2 만.
전체 회귀 검증
회귀 위험 평가
메모리 룰 준수
Test plan
산출물 (mydocs/)
🤖 Generated with Claude Code