Skip to content

fix(xlsx): tabular 헤더 자동판정 강화 — 비병합 제목행을 헤더로 잡지 않기 - #335

Merged
inoray merged 5 commits into
developfrom
bugfix/331-전처리기xlsx-tabular-모드-헤더-자동판정-강화-비병합-제목행을-헤더로-잡지-않기
Jul 24, 2026

Hidden character warning

The head ref may contain hidden characters: "bugfix/331-\uc804\ucc98\ub9ac\uae30xlsx-tabular-\ubaa8\ub4dc-\ud5e4\ub354-\uc790\ub3d9\ud310\uc815-\uac15\ud654-\ube44\ubcd1\ud569-\uc81c\ubaa9\ud589\uc744-\ud5e4\ub354\ub85c-\uc7a1\uc9c0-\uc54a\uae30"
Merged

fix(xlsx): tabular 헤더 자동판정 강화 — 비병합 제목행을 헤더로 잡지 않기#335
inoray merged 5 commits into
developfrom
bugfix/331-전처리기xlsx-tabular-모드-헤더-자동판정-강화-비병합-제목행을-헤더로-잡지-않기

Conversation

@HeechanKim-Genon

@HeechanKim-Genon HeechanKim-Genon commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

fix(xlsx): tabular 헤더 자동판정 강화 — 비병합 제목행을 헤더로 잡지 않기

Closes #331

문제

  • tabular 모드로 xlsx 적재 시, 표 상단의 병합 안 된 단일 셀 제목행(□ 제조업 등(건설업 외 업종))을 헤더행으로 오인함
  • 재현 문서: 붙임1. 사망사고 고위험요인(SIF) 아카이브(제조업 등, 건설업).xlsx (벡터DB 2180, 문서 ID 120235)
  • 진짜 헤더(연번·기인물·고위험작업·상황 …)가 데이터행으로 밀려서, 컬럼이 col_N 무명 키로 저장되고 column_map엔 제목행 하나만 남음
# 픽스 전 (제조업 시트)
headers    → ['□ 제조업 등(건설업 외 업종)', '', '', '', '', '', '', '', '']
column_map → {"field_9ed00533": "□ 제조업 등(건설업 외 업종)"}   # 제목행 하나만
컬럼 키    → col_2 ~ col_9 (무명) + field_9ed00533

원인

  • _detect_header가 "가로병합 없는 첫 행 = 컬럼명행(leaf)"으로 판정함 (converters/xlsx_processor.py)
  • 제목행 인정은 가로병합이 있을 때만 됨. SIF 제목행 □ 제조업 등은 병합 안 된 단일 셀이라 제목행으로 안 잡히고 leaf(헤더)로 잡힘
  • 결국 진짜 헤더행이 data_start = leaf + 1로 밀려서 데이터가 됨
  • 처음 설계 의도는 "모든 칸이 찬 첫 행 = 헤더"였는데 구현에 안 들어가 있었음

수정

변경 파일: genon/preprocessor/converters/xlsx_processor.py, genon/preprocessor/tests/unit/test_xlsx_processor.py

(1) _detect_header — 모든 칸이 찬 행만 헤더로 잡기

기존엔 "가로병합 없는 첫 행"을 무조건 헤더행으로 확정했음. 그래서 □ 제조업 등 같은 제목행이 그대로 헤더가 됨. 이를 방지하기 위해, 한 칸이라도 비면 헤더로 보지 않고, 모든 칸이 꽉 찬 첫 행만 헤더로 잡게 함.

        if hms:  # 수평 병합 있음 → 계층 헤더행
            group_rows.append(i)
            i += 1
            continue
+       # 병합 없는 행: '모든 칸이 채워진' 행만 컬럼명행(leaf)으로 본다.
+       # 병합으로 채워진 상위행은 위 hms 분기에서 이미 group/title 로 빠지므로,
+       # 여기 도달하는 건 '병합 없는 행'뿐 → 계층헤더가 깨질 일 없음.
+       #   ne=값 있는 칸, used_cols=표가 쓰는 칸 → 둘이 같아야(전부 채움) 헤더
+       if len(used_cols) >= 3 and len(ne) < len(used_cols):
+           title_rows.append(i)   # 빈 칸 있음(제목/배너행) → 스킵하고 다음 행 탐색
+           i += 1
+           continue
        leaf_idx = i  # 병합 없는 첫 '모든 칸이 찬' 행 → 컬럼명행
        break
  • "전부 채움"만 헤더로 잡는 이유:
    • □ 제조업 등(9칸 중 1칸), 부분행(4칸 중 2~3칸)은 모두 빈 칸이 있으므로 헤더가 아니라 제목/배너행으로 스킵됨
    • 병합으로 채워진 상위 그룹행(계층 헤더)은 이 판정에 오기 전에 if hms: 에서 이미 group 으로 빠지므로 계층헤더는 안 깨짐
    • 참고: 헤더에 빈 칸이 섞인 표(예: 연도 뒤 '증감' 열 헤더를 비워두는 통계표)는 이 규칙에서 헤더로 잡히지 않음. 그런 표는 실무상 드물다는 판단에 따라 "전부 채운 행만 헤더" 방침으로 확정
  • 3열 이상일 때만 적용. 2열 이하 표는 이 기준이 불안정해서 기존 동작(첫 비병합 행 = 헤더) 그대로 둠 → 기존 테스트 다 보존됨

(2) load_tables — 계층 헤더 flatten 중복 접기

(1) 넣으니까 건설업 시트 2줄짜리 계층 헤더가 처음으로 제대로 잡혔는데, 세로병합된 컬럼(연번·재해종류 등)이 상위행·하위행에 같은 값으로 채워져서 연번_연번처럼 이름이 겹침. 직전 조각이랑 같으면 안 붙이게 함.

        for c in used_cols:
            parts = []
            for g in group_rows:
                v = brows[g][c].strip() if c < len(brows[g]) else ""
-               if v:
+               if v and (not parts or parts[-1] != v):   # 직전과 같은 라벨이면 중복이라 skip
                    parts.append(v)
            lv = brows[leaf_idx][c].strip() if ... else ""
-           if lv:
+           if lv and (not parts or parts[-1] != lv):
                parts.append(lv)
            headers.append("_".join(parts))   # '연번_연번' → '연번', '고위험작업·상황_공종' 은 유지

검증

  • 유닛 8개 추가 → test_xlsx_processor.py 18 passed, 3 skipped(실샘플/docling/e2e는 의존성 없어 skip)
    • 배너 스킵(SIF형) / 전부 채운 행만 헤더(부분행·제목행 스킵) / 좁은 표 부분행 배너 배제 / 다중 배너 / 배너+계층헤더 / 2열 레거시 유지 / header_row override 우선 / 세로병합 중복 접기
  • 실제 SIF 원본을 수정 파서에 직접 투입 → 두 시트 다 정상(총 6032 벡터)
# 픽스 후 (제조업 시트, 2573행)
title      → '□ 제조업 등(건설업 외 업종)'   # 제목행은 컨텍스트로 분리
headers    → ['연번','산재업종(대분류)','산재업종(중분류)','산재업종(소분류)',
              '재해개요','기인물','고위험작업·상황','재해유발요인','위험성 감소대책(예시)']
column_map → 9개 컬럼명 전부 보존 (기인물 → field_02198145)

# 픽스 후 (건설업 시트, 3459행)
headers    → ['연번','고위험작업·상황_공종','고위험작업·상황_작업명','고위험작업·상황_단위작업명',
              '재해종류','재해개요','기인물','재해유발요인','위험성 감소대책(예시)']
              # 계층(가로병합) 헤더 보존 + 세로병합 중복('연번_연번') 접힘

영향 범위

  • tabular 모드 xlsx/csv 적재만 해당(docling 모드 무관)
  • 수정 후 재적재 필요. 이미 적재된 벡터(col_N)는 자동으로 안 고쳐짐

Summary by CodeRabbit

  • 개선 사항

    • 병합되지 않은 빈 칸 포함 배너 행을 제목으로 자동 인식합니다.
    • 여러 배너 행과 계층형 헤더를 구분해 실제 열 헤더를 정확히 추출합니다.
    • 반복되는 헤더 라벨을 정리해 중복된 열 이름 생성을 줄였습니다.
    • 좁은 표에서는 기존 헤더 판정 동작을 유지하며, 헤더 행을 직접 지정할 수 있습니다.
  • 테스트

    • 다양한 배너, 병합 헤더 및 헤더 지정 시나리오에 대한 검증을 추가했습니다.

- _detect_header: '가로병합 없는 첫 행=헤더' 로직에 성긴 배너행 스킵 추가.
  3열 이상 && 채워진 칸이 절반 미만이면 제목행(title)으로 보고 다음 행 탐색.
  ('□ 제조업 등' 같은 비병합 단일셀 제목행이 헤더로 오인되던 문제 수정)
- load_tables flatten: 세로병합 ffill 로 상위·leaf 가 같은 라벨인 컬럼의
  '연번_연번' 중복을 연속 중복 제거로 접음.
- 2열 이하 표는 기존 동작 유지(기존 테스트 보존).
- 유닛 7개 추가(배너 스킵/빈칸 헤더 비스킵/다중배너/배너+계층/2열 레거시/
  override 우선/세로병합 중복접기). 실제 SIF 원본으로 두 시트 정상 검증.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

XLSX tabular 처리에서 3열 이상인 표의 희소한 비병합 행을 제목행으로 분류하고, 완전히 채워진 행을 실제 헤더로 선택한다. 계층 헤더 flattening에서는 연속 중복 라벨을 제거하며 관련 동작을 단위 테스트로 검증한다.

Changes

XLSX 헤더 자동 판정 및 중복 제거

Layer / File(s) Summary
비병합 배너행 감지 및 헤더 선택
genon/preprocessor/converters/xlsx_processor.py, genon/preprocessor/tests/unit/test_xlsx_processor.py
3열 이상에서 일부 셀이 비어 있는 비병합 행을 제목행으로 건너뛰고, 완전히 채워진 헤더 선택, 복수 배너행, 2열 이하 표, header_row 지정 동작을 테스트한다.
계층 헤더 라벨 중복 제거
genon/preprocessor/converters/xlsx_processor.py, genon/preprocessor/tests/unit/test_xlsx_processor.py
group과 leaf 라벨의 연속 중복을 제거하고, 배너행과 수평·수직 병합 헤더 조합을 검증한다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant XLSXInput
  participant load_tables
  participant _detect_header
  participant HeaderFlattener
  participant ParsedTable
  XLSXInput->>load_tables: 테이블 데이터 제공
  load_tables->>_detect_header: 헤더 후보 판정
  _detect_header-->>load_tables: title_rows와 leaf header 반환
  load_tables->>HeaderFlattener: group/leaf 라벨 전달
  HeaderFlattener-->>load_tables: 중복 제거된 헤더 생성
  load_tables-->>ParsedTable: title, headers, data_rows 구성
Loading

Possibly related PRs

  • genonai/doc_parser#304: 동일한 XLSX 헤더·제목행 판정 및 load_tables() 계층 헤더 flattening 영역을 수정한다.

Suggested reviewers: inoray

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes match #331 by skipping sparse non-merged title rows, preserving hierarchical headers, and keeping tabular-only behavior.
Out of Scope Changes check ✅ Passed The additional header deduplication and expanded tests stay closely related to the same XLSX header-detection fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: improving tabular XLSX header auto-detection to skip non-merged title/banner rows.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/331-전처리기xlsx-tabular-모드-헤더-자동판정-강화-비병합-제목행을-헤더로-잡지-않기

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@genon/preprocessor/tests/unit/test_xlsx_processor.py`:
- Around line 273-287: Update test_tabular_two_column_banner_preserves_legacy to
use a sparse one-cell first row while retaining a two-column sheet, then assert
legacy parsing keeps that row as the leaf data row rather than applying banner
skipping; preserve the existing two-column headers and downstream data
assertions as appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 08fc234a-6436-4560-be7a-908795522353

📥 Commits

Reviewing files that changed from the base of the PR and between 430216e and 9be1af6.

📒 Files selected for processing (2)
  • genon/preprocessor/converters/xlsx_processor.py
  • genon/preprocessor/tests/unit/test_xlsx_processor.py

Comment thread genon/preprocessor/tests/unit/test_xlsx_processor.py
HeechanKim-Genon and others added 3 commits July 24, 2026 10:56
- _detect_header: '절반 이상 채움' → '빈 칸 2개 이하 && 채워진 칸 > 빈 칸' 으로 조정.
  담당자 의도('전부 채운 첫 행=헤더')에 최대한 맞추되, 진짜 헤더에 빈 칸 한두 개가
  있을 때 데이터행이 헤더로 승격/유실되는 것을 방지(빈 칸 2개까지 허용).
  병합 상위행은 hms 분기에서 이미 group 으로 빠지므로 계층헤더는 안 깨짐.
- 유닛 갱신: 빈칸 한두 개 헤더 비스킵(4열 1빈/5열 2빈), 좁은 표 2칸 배너 배제 추가.
  전체 18 passed, 실제 SIF 두 시트 정상 재확인.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- '빈 칸 2개 이하'(절대 기준)는 표가 넓어지면 정상 헤더(예: 15열 중 3칸 빔)를
  배너로 오판 → 데이터행 승격/유실. 열 수에 비례하는 과반(절반 초과) 기준으로 교체.
  len(ne)*2 <= len(used_cols) 이면 배너/제목행으로 보고 스킵.
  - SIF 배너(9칸 중 1칸), 좁은 표 2칸 배너(4칸 중 2칸)는 과반 미달로 배제
  - 넓은 표 빈칸 헤더(12/15)는 과반이라 유지(데이터 보존)
- 전체 18 passed. 실제 SIF 두 시트 + 사용자 샘플(spacer 열 포함) 정상 확인.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
연도 뒤 '증감' 열의 헤더를 비워두는 통계표(8열 중 5칸=과반). 절대 기준이면
헤더가 스킵돼 첫 데이터행이 헤더로 승격/유실되지만, 과반 기준은 헤더로 유지하고
데이터 3행을 모두 보존함을 고정. 전체 19 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HeechanKim-Genon
HeechanKim-Genon requested a review from inoray July 24, 2026 04:36
담당자 방침: 한 칸이라도 비면 헤더로 보지 않고, 모든 칸이 찬 첫 행만 헤더로 잡음.
- _detect_header: 과반 기준 → len(ne) < len(used_cols) 이면 제목/배너행으로 스킵.
- 헤더에 빈 칸이 섞인 표(증감열 통계표 등)는 헤더로 안 잡히지만, 실무상 드물다는
  판단에 따라 확정. 병합 상위행은 hms 분기에서 먼저 group 으로 빠져 계층헤더는 유지.
- 빈칸 봐주기 검증 테스트 2개 제거, '전부 채운 행만 헤더' 테스트로 대체. 18 passed.
- 실제 SIF 두 시트 정상(strict 규칙에서도 결과 동일) 재확인.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@inoray
inoray merged commit 4d5520c into develop Jul 24, 2026
3 checks passed
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.

[전처리기/xlsx] tabular 모드 헤더 자동판정 강화 — 비병합 제목행을 헤더로 잡지 않기

2 participants