Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions mydocs/plans/task_m100_1488.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Task M100 #1488 수행 계획

- 이슈: #1488 [HWPX] Rowbreak 표 페이지네이션 여분 페이지/겹침
- 브랜치: `local/task_m100_1488`
- 작성일: 2026-06-25
- 모드: 내부 타스크

## 목표

`samples/rowbreak-problem-pages.hwpx` 의 RowBreak(쪽나눔) 표 분할 시 발생하는
여분의 거의 빈 연속 페이지와 본문/표 셀 겹침·잘림을 한컴 정답지(`pdf/rowbreak-problem-pages-2024.pdf`, 18페이지)
수준으로 보정한다.

## 현황 (clean devel = 22 페이지, 정답지 18 페이지)

이슈 제기 시점(commit `e678104`)에는 24 페이지였으나 현재 devel(`4538a02c`)은 22 페이지로
일부 개선되었다. 여전히 4 페이지 초과 + 다수 시각 결함이 남아 있다.

### 핵심 근본 원인 — 셀 내부 cut 분할 조기 종료

문제 표는 **섹션 1, 문단 28, control[0]** 의 `1행×1열 RowBreak 표`:

- 셀 1개에 18개 문단(`paras=18`), 셀 높이 `h=86740 HU`(≈12인치)로 페이지보다 큼 → 분할 필수
- `p[0..5]`: 실제 본문 텍스트(약 918px)
- `p[6..15]`: **빈 문단**(text_len=0). 각 문단 vpos 가 직전 문단 끝보다 작은 **vpos 리셋(오버레이)** 구조
- `p[16],p[17]`: TAC 사각형(`lh=20245`, `lh=19288`) — 페이지 2의 다이어그램

`RHWP_TABLE_DRIFT=1 dump-pages` 진단 (pi=28 sec=1):

```
fragment 1: consumed=185.6 avail=186.5 (첫 조각, v_offset로 가용 제한)
fragment 2: consumed=84.9 avail=954.8 ← 954px 가용에 85px만 배치 후 페이지 넘김
fragment 3: consumed=32.8 avail=954.8 ← 33px
fragment 4: consumed=61.8 avail=954.8 ← 62px (빈 줄 2개)
fragment 5: consumed=61.8 avail=954.8 ← 62px
fragment 6: consumed=61.8 avail=954.8 ← 62px
fragment 7: consumed=918.0 avail=954.8 ← 실제 본문 (마지막 페이지)
```

→ fragment 2~6 이 가득 빈 페이지(`dump-pages` used=0.0px)를 **5장** 생성한다.
`advance_row_cut`(`src/renderer/layout/table_layout.rs`)이 vpos 리셋·오버레이 단위(unit)에서
가용 예산이 충분(954px)함에도 32~85px만 소비하고 컷을 끊는 것이 직접 원인이다.

### 추정 메커니즘

셀 내부 빈 문단들은 vpos 가 앞 본문과 겹치도록 리셋된다(오버레이 도식 구조).
`cell_units`/`advance_row_cut` 가 이 겹침/리셋 경계를 "더 넣으면 넘친다"로 오판하거나
리셋을 hard-break 로 처리하여, 한 페이지에 묶일 수 있는 유닛들을 페이지마다 1~2개씩만
배치한다. 결과적으로 본문(p[0..5])·도식(p[16..17])이 같은 셀 영역을 공유하는 구조가
세로로 펼쳐지며 여분 페이지 + 본문/도식 겹침(페이지 2)으로 나타난다.

### 시각 결함 ↔ 근본 원인 매핑 (Stage 1에서 확정)

| 이슈 결함 | 1차 추정 |
|----------|---------|
| 17~22p 여분 빈 페이지 | cut 분할 조기 종료 (위) |
| 2p 본문·도식 겹침 | 동일 셀 오버레이 분할 |
| 10p/23p 하단 표 잘림 | PartialTable 높이 초과(LAYOUT_OVERFLOW) |
| 7p 셀 텍스트 행 겹침 | 별도 — Stage 1에서 분리 판정 |
| 12p 파란 콜아웃 텍스트 잘림 | 별도 — Stage 1에서 분리 판정 |

관측된 `LAYOUT_OVERFLOW`: sec0 p2/p3/p6, sec1 p1/p2/p14 (PartialTable/Table).

## 범위

- 셀 내부 vpos 리셋/오버레이 단위에서 `advance_row_cut` 가 가용 예산을 정상 소비하도록 보정
(여분 빈 연속 페이지 제거)
- 분할 표 PartialTable 의 페이지 하단 잘림(LAYOUT_OVERFLOW) 보정
- 페이지 2 본문·도식 겹침 완화 (위 분할 보정과 연동 범위 내)
- 회귀 테스트 추가: 본 샘플의 페이지 수 + 핵심 페이지의 overflow 부재 가드
- 7p/12p 결함은 Stage 1 분리 판정 후 본 타스크 포함 여부 결정

## 비범위

- 표 렌더러/레이아웃 엔진 전면 재작성
- HWP3 등 타 포맷 분할 로직 변경
- 한컴 수동 시각 판정 자체 대체

## 검증 기준

- `export-svg`/`export-png` 출력 페이지 수가 정답지(18p) 기준으로 수렴 (최소 17~22→18~20, 여분 빈 페이지 0)
- 문제 표(sec1 pi=28) 분할 시 가득 빈 used=0.0px 페이지가 발생하지 않음
- 대상 페이지에서 `LAYOUT_OVERFLOW` 경고 미발생(또는 허용 오차 내)
- `samples/hwpx/` baseline (`cargo test --test hwpx_roundtrip_baseline`) 무회귀
- 전체 `cargo test` 통과 (레이아웃/표 분할 회귀 가드, [[feedback_full_cargo_test_before_pr]] 정합)
- 기존 표 분할 관련 테스트(`pagination`, Task #993/#1022/#1025 계보) 무회귀

## 진행 절차

1. **Stage 1 — 진단 확정**: `advance_row_cut` 조기 종료 지점을 계측해 vpos 리셋/오버레이가
직접 원인임을 확정. 각 시각 결함을 근본 원인별로 분류(7p/12p 포함 여부 결정). 코드 무수정.
2. **Stage 2 — 분할 보정 구현**: cut 단위 패킹/예산 소비 로직 보정. 여분 페이지 제거 + overflow 완화.
3. **Stage 3 — 검증**: 페이지 수/overflow/회귀 테스트, 정답지 PDF 시각 대조, 전체 cargo test.

> 본 계획서 승인 후 구현 계획서(`task_m100_1488_impl.md`, 최소 3 / 최대 6 단계)를 작성해 재승인 요청한다.
> 소스 수정은 구현 계획서 승인 이후에만 진행한다.
64 changes: 64 additions & 0 deletions mydocs/plans/task_m100_1488_impl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Task M100 #1488 구현 계획

- 이슈: #1488 [HWPX] Rowbreak 표 페이지네이션 여분 페이지/겹침
- 브랜치: `local/task_m100_1488`
- 작성일: 2026-06-25
- 수행계획서: [`task_m100_1488.md`](task_m100_1488.md) (승인 완료)

## 기본값 (수행계획서 승인 시 확정)

- 목표 페이지 수: 정답지 `pdf/rowbreak-problem-pages-2024.pdf` = **18페이지** 기준 수렴
- 7p 셀 텍스트 겹침 / 12p 콜아웃 잘림: Stage 1 분류 결과에 따라 포함/분리 결정

## 대상 코드

- `src/renderer/layout/table_layout.rs` — `cell_units`, `advance_row_cut`, `row_cut_content_height`,
`row_cut_range_has_visible_content`
- `src/renderer/typeset.rs` — 표 분할 walk 루프(약 10795~11200, Task #993/#1022/#1025 계보)
- 회귀 테스트: `src/renderer/pagination/tests.rs` 또는 `tests/` 통합 테스트

## 단계

### Stage 1 — 진단 계측 및 결함 분류 (코드 무수정)

- `RHWP_TABLE_DRIFT` 등 기존 진단으로 sec1 pi=28 분할의 fragment 별 소비/유닛 경계 확정.
- `advance_row_cut` 가 가용 954px 중 32~85px 소비 후 컷을 끊는 직접 원인 식별:
vpos 리셋 hard-break 처리인지, 유닛 높이 오버레이 합산 오판인지 구분.
- 6개 시각 결함(2/7/10/12/16/23p + 17~22 여분)을 근본 원인별로 분류 →
7p/12p 본 타스크 포함 여부 확정.
- 산출: `mydocs/working/task_m100_1488_stage1.md` (진단 결과 + 결함 매핑 표)
- 승인 요청.

### Stage 2 — cut 분할 패킹 보정 (여분 페이지 제거)

- `advance_row_cut`/`cell_units` 가 vpos 리셋·오버레이 빈 유닛을 가용 예산까지 정상 패킹하도록 보정.
(조기 컷 종료 제거 — 한 페이지에 묶일 유닛을 페이지마다 1~2개씩 흩뿌리지 않도록)
- 기존 정상 분할(보호 블록, 단일행 intra-split, 반복 제목행) 동작 불변 보장.
- 검증: sec1 pi=28 분할이 used=0.0px 빈 페이지 0, 페이지 수 18~20 수렴.
- 산출: 소스 커밋 + `mydocs/working/task_m100_1488_stage2.md`
- 승인 요청.

### Stage 3 — overflow/잔여 시각 결함 보정

- PartialTable 하단 잘림(LAYOUT_OVERFLOW: 10p/23p 등) 보정.
- 페이지 2 본문·도식 겹침이 Stage 2 분할 보정으로 해소되는지 확인, 잔여 시 추가 보정.
- (Stage 1 판정 시) 7p/12p 결함 처리.
- 대상 페이지 `LAYOUT_OVERFLOW` 미발생(허용 오차 내) 확인.
- 산출: 소스 커밋 + `mydocs/working/task_m100_1488_stage3.md`
- 승인 요청.

### Stage 4 — 회귀 테스트 및 전체 검증

- 회귀 테스트 추가: 본 샘플 페이지 수 + 핵심 페이지 overflow 부재 가드.
- `cargo test --test hwpx_roundtrip_baseline` 무회귀.
- 전체 `cargo test` 통과 ([[feedback_full_cargo_test_before_pr]] 정합).
- 정답지 PDF(18p) 시각 대조(export-svg/png).
- 산출: `mydocs/report/task_m100_1488_report.md` + 최종 커밋
- 승인 요청.

## 위험 요소

- 표 cut 분할은 Task #993/#1022/#1025/#474/#713/#1022 등 다수 회귀 가드가 누적된 민감 영역.
단일 게이트 수정이 타 샘플 회귀를 유발할 수 있어 전체 cargo test + baseline 필수.
- vpos 리셋/오버레이는 [[tech_trailing_model_no_ssot]] 처럼 문서별로 정답이 다를 수 있으므로
광범위 통일 대신 조건부 게이트로 최소 변경 지향.
71 changes: 71 additions & 0 deletions mydocs/report/task_m100_1488_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Task M100 #1488 최종 결과 보고서

- 이슈: #1488 [HWPX] Rowbreak 표 페이지네이션 여분 페이지/겹침
- 브랜치: `local/task_m100_1488`
- 작성일: 2026-06-25
- 상태: 핵심 결함 해소, 검증 완료

## 1. 문제

`samples/rowbreak-problem-pages.hwpx` 가 RowBreak 표 분할에서 여분의 거의 빈 연속
페이지와 본문/도식 겹침을 발생시켰다. clean devel(`4538a02c`) 기준 **22페이지**
(정답지 한글 2024 PDF는 **18페이지**), 이슈 제기 시점(`e678104`)에는 24페이지.

## 2. 근본 원인

섹션 1·문단 28의 `1×1 RowBreak 표`: 단일 셀에 본문 텍스트(p[0..5]) + **빈 오버레이
스페이서 문단(p[6..15])** + TAC 사각형 다이어그램(p[16..17])이 같은 세로 영역에 겹쳐
배치된 구조. 셀 높이가 페이지를 초과해 분할이 필요했다.

`cell_units`(`src/renderer/layout/table_layout.rs`)가 빈 오버레이 문단의 vpos 리셋
(동일/역방향 vpos)을 `hard_break_before` 로 표시 → `advance_row_cut` 가 가용 예산과
무관하게 리셋마다 컷을 종료. 그 결과 954px 가용 페이지에 32~85px 만 배치하고 페이지를
넘기는 거의 빈 연속 페이지를 5장 양산했다.

## 3. 해결

**비가시(빈 텍스트) 오버레이 문단이 만든 vpos 리셋을 하드 브레이크에서 제외**한다.
가시 텍스트 문단 사이 리셋(Task #993 의도)은 그대로 보존한다.

- `cell_units`: `para_has_visible_text` 게이트(`c > U+001F && c != U+FFFC`) 추가.
- 텍스트줄 유닛: `line_reset_before(li) && para_has_visible_text`
- 빈/원자 유닛: `reset_before && (has_table_in_para || para_has_visible_text)`
- 빈 오버레이 문단(`internal_reset`)과 #1488 의 `p[6..15]` 는 구조적으로 동일하므로,
가시성(텍스트 유무)을 유일한 판별 신호로 채택.

## 4. 결과

| 항목 | before | after |
|------|--------|-------|
| 페이지 수 | 22 | **18** (정답지 PDF 일치) |
| pi=28 표 분할 | 8페이지(빈 페이지 5장) | 3페이지(15~17) |
| fragment 소비 | 85/33/62/62/62px | 186/945/261px |
| 2p 본문·도식 겹침 | 있음 | 해소 |
| 내용 손실 | — | 없음(18p 전수 대조) |

## 5. 검증

- 전체 `cargo test`: lib 1938 + 통합 전부 통과, 0 실패.
- `hwpx_roundtrip_baseline`: 4/4 통과.
- 단위 회귀: `test_advance_row_cut_empty_overlay_reset_no_hard_break` (+기존 2개 가시
문단으로 갱신, rewind-orphan/Task #993 커버리지 보존).
- 통합 회귀: `tests/issue_1488_rowbreak_empty_overlay_pages.rs` (18p + pi=28 ≤4페이지).
- 시각: 한글 2024 PDF 18페이지 컨택트시트 대조 — 구조 일치, 결함 없음.

## 6. 잔여 사항 (ROOT B — 별도 후속 권장)

표 하단 분할 `LAYOUT_OVERFLOW`(sec0 p2/p3/p6, sec1 p1/p2 등, 2.7~76px 마진 스필)는:

- **모두 기존(clean devel) 문제** (이슈 본문 로그와 동일). 본 수정이 sec1 p14
para28 13.5px overflow 를 해소했고, 신규 overflow 는 미발생.
- **내용 손실 없음** — 초과 fragment 의 나머지는 다음 페이지로 정상 연속.
- 근본은 한컴의 페이지 하단 "표 밀기 vs 부분 배치" 패리티(`typeset.rs`
Task #1025/#1086/#1486/#1105 계보)로, 단일 수정이 다수 샘플 회귀를 유발할 수 있는
고위험 영역. 본 이슈 핵심(여분 빈 페이지)과 분리하여 **별도 후속 이슈**로 등록 권장.

## 7. 변경 파일

- `src/renderer/layout/table_layout.rs` (cell_units 게이트 + 단위 테스트 갱신/추가)
- `tests/issue_1488_rowbreak_empty_overlay_pages.rs` (신규 통합 회귀 테스트)
- `mydocs/plans/task_m100_1488*.md`, `mydocs/working/task_m100_1488_stage{1,2,3}.md`,
본 보고서
76 changes: 76 additions & 0 deletions mydocs/working/task_m100_1488_stage1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Task M100 #1488 Stage 1 — 진단 결과

- 브랜치: `local/task_m100_1488`
- 작성일: 2026-06-25
- 단계: Stage 1 (진단, 코드 무수정)

## 1. 재현 현황

| 항목 | 값 |
|------|-----|
| 샘플 | `samples/rowbreak-problem-pages.hwpx` |
| 정답지 | `pdf/rowbreak-problem-pages-2024.pdf` = **18페이지** |
| clean devel(`4538a02c`) | **22페이지** (4페이지 초과) |
| 이슈 제기 시점(`e678104`) | 24페이지 |

관측 `LAYOUT_OVERFLOW`: sec0 p2/p3/p6, sec1 p1/p2/p14 (PartialTable/Table).

## 2. 근본 원인 A — 오버레이 vpos 리셋 하드 브레이크 남발 (주범, 확정)

문제 표: **섹션 1, 문단 28, control[0]** = `1행×1열 RowBreak 표`, 셀 1개에 18문단, 셀 h=86740 HU(≈1157px, 페이지 가용 ≈954px → 2페이지 분할이 정상).

`RHWP_TABLE_DRIFT=1` 진단 (pi=28 sec=1):

```
fragment 1: consumed=185.6 avail=186.5
fragment 2: consumed=84.9 avail=954.8 ← 954px 가용에 85px만 배치 후 페이지 넘김
fragment 3: consumed=32.8 avail=954.8
fragment 4: consumed=61.8 avail=954.8
fragment 5: consumed=61.8 avail=954.8
fragment 6: consumed=61.8 avail=954.8
fragment 7: consumed=918.0 avail=954.8 ← 실제 본문
```

### 메커니즘 (코드 경로)

- 셀 내부 빈 문단 `p[6..15]`(text_len=0)는 vpos 가 직전 문단 끝보다 작은 **오버레이(역방향) 리셋**.
추가로 각 빈 문단의 두 줄(ls[0],ls[1])이 **동일 vpos**(겹침)라 줄 사이에도 리셋.
- `cell_units`(`table_layout.rs`)가 이 리셋을 `hard_break_before=true`로 표시
(`reset_before` 4284행 / `line_reset_before` 4312행).
- `advance_row_cut`(4399행)는 `j>start && u.hard_break_before`에서 **가용 예산과 무관하게 즉시 컷 종료**.
- → 리셋(오버레이)마다 fragment 1개 = 거의 빈 페이지 1장. 본문(p[0..5])·도식(p[16..17])이
같은 셀을 공유하는 오버레이 구조가 세로로 펼쳐져 여분 페이지 5장 + 페이지 2 본문·도식 겹침으로 나타남.

### 핵심 판단

`hard_break_before`는 Task #993에서 **가시 텍스트 문단 사이**의 진짜 페이지 분할 경계를 위해 도입됨
(`test_advance_row_cut_vpos_reset_hard_break`는 `text_para`(가시 텍스트) 간 리셋을 검증).
**빈(비가시) 오버레이 스페이서 문단이 만든 리셋까지 하드 브레이크로 처리하는 것이 과적용(false positive)**이다.
[[tech_trailing_model_no_ssot]] 교훈에 따라 광범위 통일 대신 **비가시 유닛 리셋만 하드 브레이크에서 제외**하는
조건부 게이트가 적절하다.

## 3. 근본 원인 B — PartialTable fragment 높이 페이지 초과

- `LAYOUT_OVERFLOW`(sec0 p2/p3/p6, sec1 p1/p2/p14)는 분할 표 fragment 가 페이지 바닥을 넘는 케이스.
- 이슈의 10p/23p 하단 표 잘림에 대응. 근본 원인 A 보정으로 fragment 경계가 재정렬되면 일부 자동 해소
가능성이 있으나, 잔여분은 Stage 3에서 fragment 높이/가용 계산 보정으로 처리.

## 4. 시각 결함 ↔ 근본 원인 매핑

| 이슈 결함(24p 기준) | 분류 | 처리 단계 |
|----------|------|----------|
| 17~22p 여분 빈 페이지 | 근본 A | Stage 2 |
| 2p 본문·도식 겹침 | 근본 A (동일 셀 오버레이) | Stage 2 |
| 10p/23p 하단 표 잘림 | 근본 B (LAYOUT_OVERFLOW) | Stage 3 |
| 7p 셀 텍스트 행 겹침 | PartialTable 셀 렌더(분할 표) — A/B 보정 후 재판정 | Stage 3 |
| 12p 파란 콜아웃 잘림 | 1x1 분할 표(pi=13류) — A/B 보정 후 재판정 | Stage 3 |
| 16p 상단 컷오프 | 근본 A 연속(빈 페이지 인접) | Stage 2 |

모든 결함이 RowBreak/PartialTable cut 분할 계열에 수렴한다. 7p/12p 는 별도 신규 로직이 아니라
분할 표 렌더 산물이므로 본 타스크 범위에 포함하되, A/B 보정 후 잔여 여부로 추가 판정한다.

## 5. Stage 2 방향

`cell_units`에서 **비가시(빈 텍스트) 문단/줄이 만든 vpos 리셋은 `hard_break_before`로 표시하지 않는다.**
가시 텍스트 문단 간 리셋(Task #993 의도)은 보존. 이로써 오버레이 스페이서가 페이지를 강제 분할하지 않고
가용 예산까지 정상 패킹되어 여분 빈 페이지가 제거된다. 전체 cargo test + baseline 으로 회귀 검증.
46 changes: 46 additions & 0 deletions mydocs/working/task_m100_1488_stage2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Task M100 #1488 Stage 2 — cut 분할 패킹 보정 (여분 페이지 제거)

- 브랜치: `local/task_m100_1488`
- 작성일: 2026-06-25
- 단계: Stage 2 (구현)

## 변경 요약

`src/renderer/layout/table_layout.rs` `cell_units` — 셀 내부 **비가시(빈 텍스트) 오버레이
스페이서 문단이 만든 vpos 리셋을 `hard_break_before`(강제 페이지 분할)에서 제외**.

- `para_has_visible_text` 게이트 추가 (가시 문자 = `c > U+001F && c != U+FFFC`).
- 텍스트줄 유닛: `line_reset_before(li) && para_has_visible_text`
- 빈/원자 유닛: `reset_before && (has_table_in_para || para_has_visible_text)`
- 중첩표 유닛(4220행)·가시 텍스트 문단 사이 리셋(Task #993 의도)은 보존.

### 근거

`advance_row_cut`은 `hard_break_before` 유닛에서 가용 예산과 무관하게 컷을 종료한다.
문제 셀(sec1 pi=28)의 빈 오버레이 문단 `p[6..15]`는 본문 텍스트 위에 겹쳐 놓인 동일/역방향
vpos 줄을 가져, 리셋마다 fragment(=거의 빈 페이지)를 1장씩 양산했다. 빈(비가시) 문단의
리셋은 페이지 분할점이 아니므로 게이트로 제외한다. 빈 오버레이 문단 `internal_reset`은
#1488 의 `p[6..15]`와 **구조적으로 동일**하여 둘을 가를 신호가 없으므로, 가시성(텍스트 유무)을
유일한 판별 신호로 채택했다.

## 검증

| 항목 | before | after |
|------|--------|-------|
| 페이지 수 | 22 | **18** (정답지 PDF 18 일치) |
| sec1 pi=28 분할 fragment 소비 | 85/33/62/62/62px (빈 페이지 5장) | 186/945/261px (정상 패킹) |
| 가득 빈 used=0.0px 페이지(시각) | 다수 | 0 (페이지 4·16 표시 아티팩트는 PartialTable vpos 렌더, 실제 내용 있음) |
| 페이지 2 본문·도식 겹침 | 있음 | 해소 |

`LAYOUT_OVERFLOW`: sec1 p14 para28(기존 13.5px) 해소. 그 외 기존 overflow(sec0 p2/p3/p6,
sec1 p1/p2)는 Stage 3 대상(ROOT B). 신규 overflow 미발생(pi=28 분할 위치 이동분만).

## 단위 테스트

- `test_advance_row_cut_vpos_reset_hard_break`: 가시 문단(`visible_text_para`)으로 갱신 —
가시 리셋 하드 브레이크 보존 검증 유지.
- `test_advance_row_cut_rowbreak_rewinds_internal_hard_break_orphan`: 가시 문단으로 갱신 —
rewind-orphan 로직 검증 유지.
- `test_advance_row_cut_empty_overlay_reset_no_hard_break`: **신규** — 빈 오버레이 리셋이
하드 브레이크가 아님을 검증(#1488 회귀 가드).
- `cargo test --lib row_cut`: 10/10 통과.
Loading
Loading