[버그] 표 파싱 시 오른쪽 끝 빈 열이 삭제됨 — 서식 문서의 입력란 소실 (fill 경로와 불일치)
# [버그] 표 파싱 시 오른쪽 끝 빈 열이 삭제됨 — 서식 문서의 입력란 소실 (fill 경로와 불일치)
`parseHwpx`(및 CLI `kordoc <file>`)로 표를 파싱하면 **표 오른쪽 끝의 빈 열이 통째로 삭제**됩니다. 신청서·확인서 같은 서식 문서에서 그 빈 열은 사람이 값을 적는 **입력란**이라, 라벨만 남고 값을 적을 칸이 사라져 서식이 서식으로서 기능을 잃습니다.
같은 라이브러리의 **`fillForm`은 그 셀을 정상적으로 인식해 값을 채웁니다.** 즉 한 셀을 두고 fill은 "있다", parse는 "없다"로 답합니다.
- kordoc 버전: **4.0.8** (npm 최신, 4.0.6에서도 동일)
- 입력: 외부 파일 불필요 — `markdownToHwpx` 산출물로 재현
- Node.js: v24
## 재현 (외부 파일 없이 3줄)
```js
import { markdownToHwpx, parseHwpx } from 'kordoc';
const hwpx = await markdownToHwpx(`| 성명 | |\n| --- | --- |\n| 연락처 | |\n`);
const r = await parseHwpx(hwpx);
console.log((r.blocks ?? r).filter(b => b.type === 'table')[0].table);
```
**결과:**
```
rows: 2, cols: 1
cells: [["성명"], ["연락처"]]
```
**기대:** `cols: 2` — `성명 | (빈 입력란)`
kordoc이 생성한 2열 서식 표를 kordoc이 1열로 되읽습니다(라운드트립 불일치).
## fill 경로와의 불일치
같은 문서, 같은 셀인데 두 API의 답이 다릅니다.
```js
const buf = Buffer.from(await markdownToHwpx(`| 성명 | |\n| --- | --- |\n| 연락처 | |\n`));
await parseHwpx(buf); // → cols: 1 (입력란 없음)
await fillForm(buf, { '성명': '홍길동' }, 'hwpx-preserve'); // → 그 입력란에 "홍길동" 삽입 성공
```
`fillForm`은 `scanSectionXml`로 섹션 XML을 직접 읽어 트림의 영향을 받지 않는 반면, `parseHwpx`는 `buildTable`을 거쳐 열이 삭제됩니다.
부수적으로 `kordoc fill <file> --dry-run`(필드 목록)은 파싱 경로를 쓰기 때문에, **실제로는 채울 수 있는 필드가 목록에 나타나지 않습니다.**
## 원인으로 보이는 코드
`src/table/builder.ts`의 `trimAndReturn` (4.0.6·4.0.8 동일):
```js
function trimAndReturn(grid, numRows, maxCols) {
let effectiveCols = maxCols;
while (effectiveCols > 0) {
const colEmpty = grid.every((row) => !row[effectiveCols - 1]?.text?.trim());
if (!colEmpty) break;
effectiveCols--; // 오른쪽 끝부터 빈 열을 계속 깎음
}
...
}
```
오른쪽 끝부터 "그 열 전체가 빈 텍스트"인 동안 열을 잘라냅니다. 문서→마크다운 변환에서 여백 열을 정리하려는 의도로 보이나, **서식 문서에서는 그 빈 열 자체가 콘텐츠**입니다.
특징:
- **위치에만 의존합니다.** 같은 표 안에서도 가운데 빈 열은 보존되고, 오른쪽 끝 빈 열만 삭제됩니다.
- `.trim()`이라 **공백만 든 셀도 빈 것으로 처리**됩니다.
- `src/table/builder.ts`는 포맷 공용 모듈이라 HWPX 외 다른 포맷 파서도 같은 경로를 씁니다.
## 실사용 문서에서의 양상
한컴오피스에서 HWP → HWPX로 저장한 서식 문서의 빈 셀은 아래 구조입니다(이슈 #29에서 보고된 것과 같은 형태 — `<hp:t>` 없이 self-closing run):
```xml
<hp:tc>
<hp:subList>
<hp:p><hp:run charPrIDRef="34"/></hp:p> <!-- hp:t 없음 -->
</hp:subList>
<hp:cellAddr colAddr="1" rowAddr="0"/>
<hp:cellSpan colSpan="1" rowSpan="1"/>
</hp:tc>
```
`trimAndReturn`은 텍스트 유무만 보므로 **`hp:t`가 있는 빈 셀(kordoc 생성본)과 없는 빈 셀(한컴 저장본) 모두 동일하게 트림**됩니다. 위 3줄 재현으로 두 경우가 같은 결과를 냅니다.
## 기대 동작
빈 열도 원문 `colCnt`/`cellAddr` 그대로 보존되면 좋겠습니다. 마크다운 가독성을 위해 트림이 필요하다면, 파싱 옵션(`ParseOptions`)으로 끌 수 있으면 서식 문서를 다루는 쪽에서 선택할 수 있을 것 같습니다.
## 관련
- #29 / #30 — `fill_form`이 `<hp:t>` 없는 빈 셀에 값을 못 넣던 문제. 당시 "한컴 저장본의 빈 셀에는 `hp:t`가 없다"는 점이 확인되어 fill 경로가 수정되었는데, 파싱 경로의 트림은 그대로입니다.
issue