Skip to content

fix: 복수 제목행 반복 시 2행 이상 출력 정정 (closes #594)#601

Closed
oksure wants to merge 2 commits intoedwardkim:develfrom
oksure:contrib/fix-table-header-repeat
Closed

fix: 복수 제목행 반복 시 2행 이상 출력 정정 (closes #594)#601
oksure wants to merge 2 commits intoedwardkim:develfrom
oksure:contrib/fix-table-header-repeat

Conversation

@oksure
Copy link
Copy Markdown
Contributor

@oksure oksure commented May 5, 2026

문제

페이지를 넘어가는 표에서 제목행 반복이 적용될 때, 제목행이 2개 이상 행으로 구성된 경우 첫 번째 제목행만 반복되고 나머지는 누락됩니다.

원인: table_partial.rs 에서 render_rows.push(0) 으로 행 0만 하드코딩.

수정 내용

  • is_header 셀이 있는 모든 행을 수집하여 header_rows 벡터 구성
  • 연속 페이지에서 모든 제목행을 반복 렌더링
  • 셀 범위 판별 로직도 다중 제목행을 인식하도록 업데이트

검증

cargo test   # 전체 통과
cargo clippy -- -D warnings   # 경고 없음
  • samples/synam-001.hwp p15 (분할 표): 정상 렌더링 확인
  • samples/aift.hwp (41 페이지, 대형 표 다수): export-pdf 에러 없음

참고: 첨부된 테스트.hwp 는 로컬에 없어 직접 검증하지 못했으나, 코드 변경은 리포터가 지적한 table_partial.rs:154 의 단일 행 하드코딩을 정확히 수정합니다.


피드백 있으시면 말씀해주세요.

기존 코드는 repeat_header 시 행 0 만 제목행으로 반복했으나,
제목행이 2개 이상인 표에서 두 번째 이후 제목행이 누락되었음.

수정: is_header 셀이 존재하는 모든 행을 수집하여 반복 렌더링.
Copilot AI review requested due to automatic review settings May 5, 2026 03:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes pagination rendering for split tables when repeat_header is enabled and the table header consists of multiple rows, by repeating all rows that contain is_header cells (instead of hardcoding row 0 only).

Changes:

  • Collects all rows containing is_header cells into header_rows for continuation-page rendering.
  • Renders repeated header rows on continuation pages and avoids duplicate row insertion.
  • Updates cell-inclusion range logic to account for repeated multi-row headers.
Comments suppressed due to low confidence (1)

src/renderer/layout/table_partial.rs:175

  • header_rows.contains(...) is used inside the per-row loop and again per-cell during rendering. For large tables, this introduces avoidable O(header_rows.len()) scans in hot paths. Since you already build a seen boolean vector, consider retaining a boolean membership structure (or a HashSet) for O(1) header-row membership checks while still keeping header_rows for ordering.
            let mut seen = vec![false; row_count];
            for c in &table.cells {
                if c.is_header && (c.row as usize) < row_count && !seen[c.row as usize] {
                    seen[c.row as usize] = true;
                    header_rows.push(c.row as usize);
                }
            }
            header_rows.sort_unstable();
        }
        let mut render_rows: Vec<usize> = Vec::new();
        render_rows.extend_from_slice(&header_rows);
        for r in start_row..end_row.min(row_count) {
            if !header_rows.contains(&r) {
                render_rows.push(r);
            }
        }

        // 렌더링 영역의 행별 y 위치 계산 (0부터 시작)
        let mut render_row_y: Vec<f64> = Vec::new(); // 각 render_rows 항목의 시작 y
        let mut y_accum = 0.0;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/renderer/layout/table_partial.rs Outdated
Comment on lines +154 to +170
let mut header_rows: Vec<usize> = Vec::new();
if is_continuation && table.repeat_header && start_row > 0 {
let mut seen = vec![false; row_count];
for c in &table.cells {
if c.is_header && (c.row as usize) < row_count && !seen[c.row as usize] {
seen[c.row as usize] = true;
header_rows.push(c.row as usize);
}
}
header_rows.sort_unstable();
}
let mut render_rows: Vec<usize> = Vec::new();
render_rows.extend_from_slice(&header_rows);
for r in start_row..end_row.min(row_count) {
render_rows.push(r);
if !header_rows.contains(&r) {
render_rows.push(r);
}
Comment thread src/renderer/layout/table_partial.rs Outdated
Comment on lines 153 to 167
// is_continuation && repeat_header → is_header 셀이 있는 모든 행을 반복
let mut header_rows: Vec<usize> = Vec::new();
if is_continuation && table.repeat_header && start_row > 0 {
let mut seen = vec![false; row_count];
for c in &table.cells {
if c.is_header && (c.row as usize) < row_count && !seen[c.row as usize] {
seen[c.row as usize] = true;
header_rows.push(c.row as usize);
}
}
header_rows.sort_unstable();
}
let mut render_rows: Vec<usize> = Vec::new();
render_rows.extend_from_slice(&header_rows);
for r in start_row..end_row.min(row_count) {
Comment thread src/renderer/layout/table_partial.rs Outdated
Comment on lines 153 to 171
// is_continuation && repeat_header → is_header 셀이 있는 모든 행을 반복
let mut header_rows: Vec<usize> = Vec::new();
if is_continuation && table.repeat_header && start_row > 0 {
let mut seen = vec![false; row_count];
for c in &table.cells {
if c.is_header && (c.row as usize) < row_count && !seen[c.row as usize] {
seen[c.row as usize] = true;
header_rows.push(c.row as usize);
}
}
header_rows.sort_unstable();
}
let mut render_rows: Vec<usize> = Vec::new();
render_rows.extend_from_slice(&header_rows);
for r in start_row..end_row.min(row_count) {
render_rows.push(r);
if !header_rows.contains(&r) {
render_rows.push(r);
}
}
- header_rows 수집을 r < start_row 조건으로 제한하여,
  데이터 범위 내 is_header 행이 의도치 않게 상단으로 재배치되는 문제 방지
- render_rows 루프에서 중복 제거 조건도 제거 (header_rows ⊂ [0, start_row))
- 페이지네이션 높이 예약: MeasuredTable 에 header_row_flags 추가 필요 (후속 작업)
@oksure
Copy link
Copy Markdown
Contributor Author

oksure commented May 5, 2026

Addressed review feedback in 80db71a:

  1. header_rows scope (comment baseline/line_height/line_spacing 역공학 #1): r < start_row 조건 추가 — 데이터 범위 내 is_header 행이 상단으로 재배치되는 문제 방지. render_rows 루프에서 중복 제거 조건도 제거 (불필요해짐).

  2. pagination height (comment 편집 파이프라인 파생 갭 수정 #2): 인지하고 있습니다. 현재 MeasuredTableheader_row_flags: Vec<bool> 필드가 없어 pagination engine 에서 다중 제목행 높이를 정확히 예약할 수 없습니다. 이 부분은 MeasuredTable 확장과 함께 후속 작업으로 처리하는 것이 적절할 것 같습니다.

  3. regression test (comment 셀 내 TAC 이미지 수직 배치 버그 수정 #3): 리포터 첨부 샘플이 로컬에 없어 직접 테스트를 추가하지 못했습니다. 메인테이너 쪽에서 샘플 기반 integration test 를 추가해주시면 좋겠습니다.

edwardkim added a commit that referenced this pull request May 5, 2026
v0.7.9 후속 patch 사이클 (5/4 ~ 5/6).

## 신규 기능

- **CLI 바이너리 릴리즈** (Issue #608/#612, @almet 의 요청)
  - 4 플랫폼 GitHub Release 자산 첨부 (Linux x86_64 / macOS x86_64+aarch64 /
    Windows x86_64) + SHA-256 체크섬
- **PNG raster backend** (PR #599, @seo-rii) — render P4 단계
  - native Skia 기반 PageLayerTree → PNG export, native-skia feature gate
  - **AI 파이프라인 + VLM 연동 도입** (메인테이너 후속 정정):
    - --vlm-target claude (1568 longest edge / 1.15 MP, Claude Vision 정합)
    - --scale / --max-dimension (자동 scale 계산)
    - export-png CLI 명령 + 매뉴얼 (한글 + 영문 dual)
    - 한글 폰트 fallback chain + char 단위 fallback (공백 두부 정정) +
      --font-path 동적 로딩

## 외부 PR cherry-pick (13 PR / 7 컨트리뷰터)

- @planet6897 / Jaeook Ryu (협업): PR #587/#589/#561/#564/#570/#575/
  #580/#584/#592/#593/#567
- @oksure (Hyunwoo Park): PR #600 (closes #513)
- @seo-rii: PR #599 (refs #536)
- @cskwork / @johndoekim / @nameofSEOKWONHONG / @jangster77 — 사이클 누적

## 메인테이너 정정

Skia 폰트 영역 5개 정정 (한글 fallback / font-path / char-fallback /
VLM 옵션 / export-png CLI).

## 인프라

- CI 빌드 안정성 (Cargo.toml [[example]] required-features)
- 광범위 페이지네이션 회귀 sweep 도구 (164 fixture / 1,614 페이지 자동)

## 후속 이슈

- #613 (VLM 프리셋 확장)
- #614 (DPI 메타데이터)
- #615 (pua_oldhangul.rs U+F53A 한컴 정합)
- #598 (rhwp-studio 각주 삭제, 외부 컨트리뷰터 공개)

## 잔여 PR (v0.7.11 후속 patch)

PR #601, #602 (@oksure) / PR #607 (@dicebattle) / PR #609 (@jangster77,
Task #604) / PR #611 (@kihyunnn).

상세: CHANGELOG.md (한글) / CHANGELOG_EN.md (영문).
edwardkim added a commit that referenced this pull request May 7, 2026
- @oksure (Hyunwoo Park) Task #594 복수 제목행 반복 정정 PR 검토
- 단일 파일 (table_partial.rs +22/-12), 2 commits (본질 + Copilot review address)
- 본 환경 검증: cargo test 1141 passed / clippy 0 / svg_snapshot 6/6 / 광범위 sweep 회귀 0
- 정량 측정: PR 본문 명시 권위 샘플 (synam-001 35 + aift 77 페이지) 모두 byte-identical (회귀 0)
- Copilot review 4 코멘트 자체 검토 응답 정합 (header_rows scope + seen O(1) + pagination 후속 영역)
- Issue #594 첨부 테스트.hwp 본 환경 미존재 → 작업지시자 시각 판정 게이트 권고
- 옵션 A (cherry-pick + 시각 판정) + 옵션 B (후속 권유) 결합 권장

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardkim added a commit that referenced this pull request May 7, 2026
- 13.1 핀셋 cherry-pick (0059557, author Hyunwoo Park 보존)
- 13.2 결정적 재검증 (1141 passed / clippy 0 / WASM 4,588,023 bytes)
- 13.3 작업지시자 안내 영역 (aift.hwp 페이지 수 47/77 차이 + 시각적 개선 미발현 본 환경 fixture 영역)
- 13.4 권장 처리 방향 정합 (옵션 A-1 권위 샘플 도입 / A-2 그대로 머지 + 후속 / A-3 close + 후속 PR 권유)
- 13.5 WASM 산출물 정합 (PR #642 baseline +705 bytes)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardkim added a commit that referenced this pull request May 7, 2026
- 47 페이지 (page_num=41) = global_idx=46 영역 dump-pages 분석
- pi=579/581/584 모두 단일 제목행 영역 → 본 PR fix 분기 미발현 (회귀 0 정합)
- examples/inspect_pr601.rs 진단 스크립트 신규 (aift.hwp is_header sweep)
- 다중 제목행 표 발견: s2 pi=147 (7×4) + s2 pi=745 (9×14) — 분할 미발현
- 결론: 본 환경 aift.hwp 다중 제목행 + 분할 표 발현 영역 부재 → 시각적 개선 미발현 정합
- 권장: 옵션 A-1 (테스트.hwp 도입 후 재시각 판정) / A-2 (회귀 0 입증으로 머지 + 후속) / A-3 (close + 후속 PR 권유)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edwardkim added a commit that referenced this pull request May 7, 2026
 2 commits squash + 옵션 A-2 회귀 0 입증으로 머지, 시각 발현 본 환경 권위 영역 부재)
@edwardkim
Copy link
Copy Markdown
Owner

검토 + 핀셋 cherry-pick 머지 완료. 감사합니다.

처리 결과 — 옵션 A-2 진행

결정적 재검증 (본 환경)

검증 결과
cargo test --lib --release 1141 passed (회귀 0)
svg_snapshot / issue_546 / issue_554 / issue_598_footnote_marker_nav 모두 통과
cargo clippy --release --lib 0건
Docker WASM 빌드 4,588,023 bytes (PR #642 baseline 4,587,318 +705 — header_rows Vec allocation 정합)

광범위 페이지네이션 회귀 sweep

  • 총 167 fixture (161 hwp + 6 hwpx) / 1,687 페이지 / 차이 0

정량 측정 (PR 본문 명시 권위 샘플)

샘플 페이지 byte 차이
samples/synam-001.hwp 35 페이지 35 identical / 0 differ
samples/aift.hwp 77 페이지 77 identical / 0 differ

→ 두 샘플의 표는 단일 제목행 영역 으로 본 PR fix 의 분기 미발현 (회귀 0 정합 입증).

시각 발현 영역 정밀 분석 (examples/inspect_pr601.rs 진단 스크립트 신규)

본 환경 aift.hwp 의 모든 표 영역 sweep:

영역 개수
is_header 부재 표 85 개
단일 제목행 표 3 개 (47 페이지 영역의 pi=579 / pi=581 / pi=584 모두)
다중 제목행 표 2 개 (s2 pi=147 7×4 + s2 pi=745 9×14) — 한 페이지에 완전히 들어감, 분할 미발현

→ 본 환경 aift.hwp 의 다중 제목행 + 분할 표 발현 영역 부재. 본 PR 본문 명시 "synam-001.hwp p15 (분할 표): 정상 렌더링 확인" + "aift.hwp (대형 표 다수): export-pdf 에러 없음" 은 회귀 0 입증 영역으로 정합하지만, 본 PR fix 의 시각적 효과 발현은 Issue #594 첨부 테스트.hwp 영역의 권위 케이스에서만 가능.

Copilot review 4 코멘트 자체 검토 응답 정합

본 PR 의 본질 — 옵션 A-2 진행 정합

본 PR 의 처리 본질에서 가장 우수한 점:

  1. 본질 진단 정확table_partial.rs:154render_rows.push(0) 단일 행 하드코딩 영역 정확 식별
  2. HWP IR 표준 직접 사용is_header 셀 동적 수집, 휴리스틱 미도입
  3. 케이스별 명시 가드 — 4 분기 (is_continuation + repeat_header + start_row > 0 + r < start_row) 로 회귀 위험 좁음
  4. 회귀 0 입증 — 본 환경 권위 샘플 (synam-001 35 + aift 77 페이지) 모두 byte-identical
  5. Copilot review 자체 검토 응답 정합 — 본질 영역 + 후속 영역 분리 명료
  6. 활발한 컨트리뷰터 영역 (oksure 5번째 PR) — 차분/사실 중심 톤 + Copilot review 응답 정합

후속 영역 (별도 task 후보)

  1. MeasuredTable.header_row_flags 추가 — 다중 제목행 높이 예약 (Copilot review 편집 파이프라인 파생 갭 수정 #2)
  2. 회귀 테스트 영역 — Issue HWP->PDF 변환 시 페이지가 넘어가는 표에서 복수 제목행 반복 시 제목 출력이 깨지는 문제 #594 첨부 테스트.hwp 본 환경 도입 + integration test
  3. 본 환경 권위 샘플 영역 — 다중 제목행 + 분할 표 발현 fixture 도입

수고하셨습니다.

@edwardkim edwardkim closed this May 7, 2026
edwardkim added a commit that referenced this pull request May 7, 2026
PR #601 처리 (옵션 A-2 정합):
- mydocs/pr/archives/pr_601_report.md 신규 (처리 보고서)
- mydocs/pr/pr_601_review.md → mydocs/pr/archives/pr_601_review.md 이동
- mydocs/orders/20260507.md 갱신 (PR #601 항목 + Issue #652 신규 영역)

Issue #652 권위 자료 영역 (HWP5 vpos 부풉 결함):
- samples/hwpx/aift.hwpx (한컴 2020 변환본, 74 페이지 한컴 정합)
- samples/hwpx/aift-2020.pdf (한컴 2010/2020 PDF 권위 정답지, 74 페이지)
- 본 환경 결함 영역 좁힘: HWP5 만 +3 페이지 부풉 (HWPX 정합)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants