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
1 change: 1 addition & 0 deletions mydocs/orders/20260626.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

| Issue | 타스크 | 상태 | 비고 |
|------|--------|------|------|
| #1562 | HWPX 폼 컨트롤 caption `&&` 표시 정합 보정 | 진행중 | `local/task1562`, 저장값 보존 + 표시 계층 보정 |
| #1270 | HWPX 캡션 내 인라인 이미지 렌더링 누락 정정 | 완료 | 인라인 depth 1 구현·검증 완료, 플로팅은 후속. 완료: 10:58 |
## 공통 — 운영 작업

Expand Down
140 changes: 140 additions & 0 deletions mydocs/plans/task_m100_1562.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# 수행계획서 — Task #1562

> HWPX 폼 컨트롤 caption `&&`가 한컴과 다르게 `&&`로 표시됨

- **이슈**: [#1562](https://github.com/edwardkim/rhwp/issues/1562)
- **Parent**: [#1534](https://github.com/edwardkim/rhwp/issues/1534)
- **Review context**: [PR #1536](https://github.com/edwardkim/rhwp/pull/1536)
- **마일스톤**: v1.0.0 (M100)
- **브랜치**: `local/task1562` (base: `local/devel`)
- **작성일**: 2026-06-26

---

## 1. 배경 / 문제

#1534 / PR #1536에서 HWPX 폼 컨트롤 `caption` 속성의 XML escape 누적 손상은
해결했다. 그 결과 저장 모델과 XML 직렬화는 다음 상태를 안정적으로 유지한다.

| 계층 | 값 |
|------|----|
| XML 원문 | `caption="IP R&&D연계"` |
| XML attribute decode 후 저장값 | `IP R&&D연계` |
| roundtrip 후 XML | `caption="IP R&&D연계"` 유지 |

남은 문제는 **표시 계층**이다. 한컴 뷰어는 동일 원본을 `IP R&D연계`로 표시하지만,
현재 rhwp 렌더러는 저장값을 그대로 출력하여 `IP R&&D연계`로 보인다.

이 이슈는 XML escaping 문제가 아니므로 parser/serializer에서 `&&`를 `&`로 바꾸면
안 된다. 저장값은 보존하고, 폼 컨트롤 caption을 화면에 그릴 때만 한컴 표시 결과와
맞춘다.

## 2. 현재 코드 조사 결과

폼 caption은 HWPX 파싱 후 `FormObject.caption`에 저장되고, layout 단계에서
`FormObjectNode.caption`으로 복사된 뒤 각 렌더러가 직접 출력한다.

| 영역 | 확인 위치 | 현재 동작 |
|------|-----------|-----------|
| HWPX parse | `src/parser/hwpx/section.rs` `parse_form_object` | `caption` 속성을 `form.caption`에 저장 |
| HWPX serialize | `src/serializer/hwpx/form.rs` | `form.caption`을 XML attribute로 재출력 |
| Layout/render tree | `src/renderer/layout/paragraph_layout.rs` | `f.caption.clone()`을 `FormObjectNode.caption`에 복사 |
| SVG | `src/renderer/svg.rs` `render_form_object` | `escape_xml(&form.caption)` 직접 출력 |
| Web Canvas | `src/renderer/web_canvas.rs` `render_form_object` | `ctx.fill_text(&form.caption, ...)` 직접 출력 |
| Skia | `src/renderer/skia/renderer.rs` `draw_form_control` | `canvas.draw_str(&form.caption, ...)` 직접 출력 |

따라서 수정 후보는 parser/serializer가 아니라 **폼 caption 표시 문자열 생성 지점**이다.

## 3. 해결 방향

### 권장 방향

폼 컨트롤 caption 전용 display helper를 도입한다.

- 저장 모델: `FormObject.caption` / `FormObjectNode.caption` 값은 그대로 유지
- 직렬화: `caption="R&&D"` 형태 유지
- 표시: 폼 caption을 출력하기 직전에 helper를 통해 `R&&D`를 `R&D`로 변환
- 적용 대상: PushButton, CheckBox, RadioButton의 `caption`
- 비적용 대상: 본문 텍스트, 표/그림/도형 `<hp:caption>`, combo/edit `text`, 모든 XML attribute 전역

### `&` 처리 정책

1차 구현은 #1562에서 관측된 **double ampersand 표시 escape**를 고정한다.

- `&&` → `&`
- 일반 본문과 저장값에는 적용하지 않음
- 단일 `&`의 access-key prefix 제거/밑줄 표시는 한컴 실물 샘플 근거가 추가로 확인될 때
구현계획서에서 확장 여부 판단

단일 `&`까지 추정 처리하면 실제 literal `&` caption을 손상시킬 수 있으므로, 이번 이슈의
완료 조건은 `R&&D` 계열 표시 정합으로 제한한다.

## 4. 구현 후보

구현계획서에서 다음 중 하나를 확정한다.

| 안 | 내용 | 장점 | 리스크 |
|----|------|------|--------|
| A (권장) | `FormObjectNode` 또는 renderer 공통 모듈에 `display_form_caption()` 추가 후 SVG/Web Canvas/Skia에서 사용 | 저장값 보존과 표시 변환 분리, 중복 최소화 | 호출 누락 시 렌더러 간 차이 발생 |
| B | layout 단계에서 `FormObjectNode`에 `display_caption` 필드 추가 | 렌더러는 단순해짐 | render tree JSON/API에서 저장값과 표시값 혼동 가능 |
| C | 각 렌더러에서 local replace 적용 | 변경 최소 | 중복, 향후 규칙 확장 시 누락 위험 |

권장은 A다. `FormObjectNode.caption`은 저장/원본 의미를 유지하고, 렌더러가 그릴 때만
공통 helper를 호출한다.

## 5. 검증 전략

1. **표시 helper 단위 테스트**
- `R&&D` → `R&D`
- `IP R&&D연계` → `IP R&D연계`
- `R&&D 자율성트랙(일반)` → `R&D 자율성트랙(일반)`
- `R&D` 같은 단일 `&` 입력은 1차 구현에서 보존
2. **SVG 렌더 회귀**
- `samples/hwpx/form-002.hwpx` page 0 SVG에 `R&amp;D`가 있고 `R&amp;&amp;D`가 없는지 확인
- `cargo test --test svg_snapshot form_002` 통과
- 의도된 visual diff로 `tests/golden_svg/form-002/page-0.svg` 갱신
3. **저장값 보존 회귀**
- `cargo test --test issue_1534_hwpx_form_caption_escape` 통과 유지
- roundtrip 후 XML caption이 `R&amp;&amp;D` 형태를 유지하는지 확인
4. **렌더러별 확인**
- SVG renderer
- Web Canvas renderer
- Skia native renderer
- user-visible text/HTML/Markdown export 경로가 실제 caption을 출력하는 경우 동일 정책 적용 여부 확인
5. **기본 게이트**
- `cargo fmt --check`
- 관련 targeted tests
- 필요 시 `cargo clippy --all-targets -- -D warnings`

## 6. 산출물

- 구현계획서: `mydocs/plans/task_m100_1562_impl.md`
- 소스 수정: 렌더러 공통 helper 및 SVG/Web Canvas/Skia caption 출력 경로
- 테스트: helper 단위 테스트 또는 targeted integration test
- Golden 갱신: `tests/golden_svg/form-002/page-0.svg`
- 단계별 완료보고서: `mydocs/working/task_m100_1562_stage{N}.md`
- 최종 보고서: `mydocs/report/task_m100_1562_report.md`

## 7. 리스크

- **저장값 손상**: parser/serializer에서 변환하면 #1534 해결을 되돌릴 수 있음.
구현계획서에서 저장 계층 비수정 원칙을 다시 확인한다.
- **렌더러 누락**: SVG만 고치면 Canvas/Skia와 표시가 달라질 수 있음. 공통 helper와
호출 지점 목록으로 방지한다.
- **단일 `&` 추정 처리**: 근거 없이 mnemonic prefix 전체 규칙을 적용하면 literal `&`
caption을 손상시킬 수 있음. 이번 단계에서는 `&&` 표시 escape에 한정한다.
- **Golden diff 오판**: SVG golden 변경은 `R&&D` → `R&D` 텍스트 차이가 의도된 변경인지
확인한 뒤 반영한다.

## 8. 단계 개략

1. 구현계획서 작성 — helper 위치, 호출 지점, 테스트 파일 확정
2. Stage 1 — 표시 helper + red/green 테스트 작성
3. Stage 2 — SVG/Web Canvas/Skia 적용 + golden 갱신
4. Stage 3 — #1534 저장 안정성 회귀와 렌더 targeted 검증
5. Stage 4 — 최종 보고서 작성 및 PR 준비

---

> 본 수행계획서 승인 후 구현계획서(`task_m100_1562_impl.md`)를 작성하여 재승인 요청한다.
> 소스 수정은 구현계획서 승인 전까지 착수하지 않는다.
Loading