"""M02 일위대가 실무자료 엑셀꼴 격자(PLAN 329-1) — 올린 엑셀 「일위대가」 · 「단가산출」 시트 (STmate = 「일위대가표」 · 「단가산출근거」 · 338) → A00 산출근거 스프레드시트 Workbook(`A00_Common/spreadsheet/spreadsheet_types.ts` 꼴 · 읽기 전용 보기). xls = xlrd `formatting_info` — 값 · 병합 · 열 폭(÷256 글자) · 행 높이(÷20 pt · 시트 기본과 다른 것만) · 서식 표(글꼴 · 굵게 · 맞춤 · 줄바꿈 · 테두리 · 채움 · 숫자 형식) · 틀고정(파일 값 · 없으면 머리 4행). xlsx = 값만 · 넓은 시트는 머리 글(앞 4 줄) 마지막 열까지 · STmate 숨은 칸 글(「_x0005_`BDCOD…」) 뺌. 결과 `{책, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` — 행 = 엑셀 1부터 [시작, 끝]. """ from __future__ import annotations import io from typing import Any from openpyxl.utils import get_column_letter from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel from M02_MasterTemplete import M02_Hopyo_Stmate as hopyo_stmate SHEETS = (hopyo_excel.SHEET, hopyo_basis.SHEET, hopyo_stmate.SHEET, hopyo_stmate.CALC) HEAD_ROWS = 4 _HALIGN = {1: "left", 2: "center", 3: "right", 6: "centerContinuous", 7: "distributed"} _VALIGN = {0: "top", 1: "center"} _LINES = {1: "thin", 2: "medium", 3: "dashed", 4: "dotted", 5: "thick", 6: "double", 7: "hair"} _SIDES = (("위", "top"), ("아래", "bottom"), ("왼", "left"), ("오른", "right")) class _Styles: """서식 표 — 같은 서식은 한 번만 · [0] = 기본 `{}`.""" def __init__(self) -> None: self.table: list[dict[str, Any]] = [{}] self._index: dict[str, int] = {"{}": 0} def add(self, style: dict[str, Any]) -> int: key = repr(sorted(style.items())) if not style: return 0 if key not in self._index: self._index[key] = len(self.table) self.table.append(style) return self._index[key] def _rgb(book: Any, index: int) -> str | None: got = book.colour_map.get(index) return "#{:02X}{:02X}{:02X}".format(*got) if got else None def _xf_style(book: Any, xf: Any, base: Any) -> dict[str, Any]: font = book.font_list[xf.font_index] out: dict[str, Any] = {} if font.name != base.name: out["글꼴"] = font.name if font.height != base.height: out["크기"] = font.height / 20 for key, on in ( ("굵게", font.bold), ("기울임", font.italic), ("밑줄", font.underline_type), ("취소선", font.struck_out), ): if on: out[key] = True color = _rgb(book, font.colour_index) if color and color != "#000000": out["글자색"] = color if xf.background.fill_pattern == 1 and (fill := _rgb(book, xf.background.pattern_colour_index)): out["채움"] = fill align = xf.alignment if align.hor_align in _HALIGN: out["가로"] = _HALIGN[align.hor_align] if align.vert_align in _VALIGN: out["세로"] = _VALIGN[align.vert_align] if align.text_wrapped: out["줄바꿈"] = True sides = {} for name, side in _SIDES: line = getattr(xf.border, f"{side}_line_style") if line: one = {"선": _LINES.get(line, "dashed")} color = _rgb(book, getattr(xf.border, f"{side}_colour_index")) if color and color != "#000000": one["색"] = color sides[name] = one if sides: out["테두리"] = sides fmt = book.format_map.get(xf.format_key) if fmt and fmt.format_str and fmt.format_str != "General": out["형식"] = fmt.format_str return out def _value(cell: Any) -> Any: import xlrd if cell.ctype == xlrd.XL_CELL_TEXT: return cell.value or None if cell.ctype in (xlrd.XL_CELL_NUMBER, xlrd.XL_CELL_DATE): return int(cell.value) if cell.value == int(cell.value) else cell.value if cell.ctype == xlrd.XL_CELL_BOOLEAN: return bool(cell.value) if cell.ctype == xlrd.XL_CELL_ERROR: return xlrd.error_text_from_code.get(cell.value, "#N/A") return None def _xls_sheet(book: Any, sh: Any, styles: _Styles, sid: str) -> dict[str, Any]: base = book.font_list[0] by_xf: dict[int, int] = {} cells: dict[str, dict[str, Any]] = {} for r in range(sh.nrows): for c in range(sh.ncols): cell = sh.cell(r, c) if cell.xf_index not in by_xf: by_xf[cell.xf_index] = styles.add( _xf_style(book, book.xf_list[cell.xf_index], base) ) one: dict[str, Any] = {} if (value := _value(cell)) is not None: one["값"] = value if by_xf[cell.xf_index]: one["서식"] = by_xf[cell.xf_index] if one: cells[f"{get_column_letter(c + 1)}{r + 1}"] = one merged = [ f"{get_column_letter(c0 + 1)}{r0 + 1}:{get_column_letter(c1)}{r1}" for r0, r1, c0, c1 in sh.merged_cells ] cols = { get_column_letter(c + 1): { "폭": round(info.width / 256, 2), **({"숨김": True} if info.hidden else {}), } for c, info in sh.colinfo_map.items() if c < sh.ncols } rows = { str(r + 1): {"높이": info.height / 20, **({"숨김": True} if info.hidden else {})} for r, info in sh.rowinfo_map.items() if r < sh.nrows and (info.height != sh.default_row_height or info.hidden) } frozen = sh.panes_are_frozen and (sh.horz_split_pos or sh.vert_split_pos) freeze = {"행": sh.horz_split_pos, "열": sh.vert_split_pos} if frozen else {"행": HEAD_ROWS} out = { "id": sid, "이름": sh.name.strip(), "칸": cells, "병합": merged, "열": cols, "행": rows, "틀고정": {k: v for k, v in freeze.items() if v}, } if sh.default_row_height: out["기본"] = {"행높이": sh.default_row_height / 20} return out def _xls(data: bytes) -> dict[str, Any]: import xlrd book = xlrd.open_workbook(file_contents=data, formatting_info=True, logfile=io.StringIO()) styles = _Styles() found = {sh.name.strip(): sh for sh in book.sheets()} sheets = [ _xls_sheet(book, found[name], styles, f"s{at}") for at, name in enumerate((n for n in SHEETS if n in found), 1) ] font = book.font_list[0] return { "기본": {"글꼴": font.name, "크기": font.height / 20}, "서식": styles.table, "시트": sheets, } def _xlsx(name: str, data: bytes) -> dict[str, Any]: # ponytail: xlsx 는 값만(병합 · 폭 · 서식은 openpyxl 비 read_only 로 무거움) — 필요하면 그때 found = hopyo_excel._sheets(name, data) sheets = [] for at, title in enumerate((n for n in SHEETS if n in found), 1): rows = found[title] width = max( (c + 1 for row in rows[:HEAD_ROWS] for c, v in enumerate(row) if v not in (None, "")), default=0, ) cells = { f"{get_column_letter(c + 1)}{r + 1}": { "값": v if isinstance(v, (int, float, bool)) else str(v) } for r, row in enumerate(rows) for c, v in enumerate(row[:width]) if v not in (None, "") and not str(v).startswith("_x000") # STmate 숨은 칸 글 } sheets.append({"id": f"s{at}", "이름": title, "칸": cells, "틀고정": {"행": HEAD_ROWS}}) return {"서식": [{}], "시트": sheets} def read(name: str, data: bytes, parsed: dict[str, Any]) -> dict[str, Any]: """엑셀 → `{책: Workbook, 호표, 단산}` — `parsed` = `M02_Hopyo_Excel.read_book` 결과 (행 범위).""" xlsx = data[:4] == b"PK\x03\x04" or name.lower().endswith(".xlsx") book = _xlsx(name, data) if xlsx else _xls(data) keep = ("번", "이름", "행") return { "책": {"종류": "통합문서", "판": 1, "열": "", **book, "활성": "s1"}, "호표": [{k: h.get(k) for k in keep} for h in parsed.get("호표") or []], "단산": [{k: b.get(k) for k in keep} for b in parsed.get("단산") or []], }