# -*- coding: utf-8 -*- """보고서 시트 모형 → 엑셀(.xlsx) 쓰기 (report_xlsx.py). 시트 모형 = report_*.js 가 만든 { name, title, rows:[{A:값,…,_k:종류,_link:시트}] } (rows[i] = i+1 행). 근거: 지식DB STmate/old/20_분석 19 §3(목차·안내 시트) · 20 §3(SET_REP 서식) · 06 §3(출력별 반올림). 쓰는 것: 값 · 글꼴(제목·머리·묶음 줄 굵게) · 테두리 · 숫자 서식(정수 #,##0 · 소수 자리 그대로) · 열 폭 · 인쇄 설정(A4 · 폭 맞춤 · 머리 행 반복 · 쪽 넘김) · 목차 바로가기 하이퍼링크. """ from __future__ import annotations import re from io import BytesIO import openpyxl from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import column_index_from_string, get_column_letter from openpyxl.worksheet.pagebreak import Break COL_RE = re.compile(r"^[A-Z]{1,2}$") BAD_TITLE = re.compile(r"[\[\]:*?/\\]") THIN = Side(style="thin", color="808080") BOX = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) HEAD_FILL = PatternFill("solid", start_color="E8EEF5", end_color="E8EEF5") FONT = "맑은 고딕" BOLD_KINDS = {"head", "T1", "T2", "T3", "T4", "T5", "sub", "total", "final", "hopyo", "parent"} def _cells(row: dict) -> dict: return {k: v for k, v in row.items() if COL_RE.match(k) and v not in (None, "")} def _num_format(v: float) -> str: if float(v).is_integer(): return "#,##0" digits = len(repr(float(v)).split(".")[1].rstrip("0")) return "#,##0." + "0" * min(max(digits, 1), 6) def sheet_title(name: str, used: set[str]) -> str: """엑셀 시트 이름 — 금지 글자 걷고 31자 · 겹치면 번호.""" base = BAD_TITLE.sub("", str(name or "Sheet"))[:31] or "Sheet" title, i = base, 2 while title in used: title = f"{base[:28]}({i})" i += 1 used.add(title) return title def write_sheet(ws, sheet: dict) -> None: rows = sheet.get("rows") or [] width: dict[int, int] = {} max_col = 1 head_rows = 0 for r_idx, row in enumerate(rows, start=1): kind = row.get("_k", "") if kind == "head": head_rows = r_idx if kind == "page" and r_idx > 1: ws.row_breaks.append(Break(id=r_idx - 1)) for col, val in _cells(row).items(): c_idx = column_index_from_string(col) max_col = max(max_col, c_idx) cell = ws.cell(row=r_idx, column=c_idx, value=val) bold = kind in BOLD_KINDS or kind == "title" size = 16 if kind == "title" else 10 cell.font = Font(name=FONT, size=size, bold=bold) if isinstance(val, (int, float)) and not isinstance(val, bool): cell.number_format = _num_format(val) cell.alignment = Alignment(horizontal="right", vertical="center") else: text = str(val) cell.alignment = Alignment( horizontal="center" if kind == "head" else "left", vertical="center", wrap_text="\n" in text, ) if kind == "head": cell.fill = HEAD_FILL if kind not in ("title", "pname") and r_idx > 2: cell.border = BOX n = max(len(s) for s in str(val).split("\n")) if not isinstance(val, float) else 12 width[c_idx] = max(width.get(c_idx, 0), n) link = row.get("_link") if link: target = next((c for c in ("D", "A") if c in row), None) if target: cell = ws[f"{target}{r_idx}"] cell.hyperlink = f"#'{link}'!A1" cell.font = Font(name=FONT, size=10, color="1D4ED8", underline="single") if rows and rows[0].get("_k") == "title" and max_col > 1: ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=max_col) ws.cell(row=1, column=1).alignment = Alignment(horizontal="center", vertical="center") for c_idx in range(1, max_col + 1): w = width.get(c_idx, 4) ws.column_dimensions[get_column_letter(c_idx)].width = min(max(w * 1.6 + 2, 6), 48) ws.page_setup.paperSize = ws.PAPERSIZE_A4 ws.page_setup.orientation = "landscape" if max_col > 8 else "portrait" ws.page_setup.fitToWidth = 1 ws.page_setup.fitToHeight = 0 ws.sheet_properties.pageSetUpPr.fitToPage = True if head_rows: ws.print_title_rows = f"1:{head_rows}" ws.freeze_panes = ws.cell(row=head_rows + 1, column=1) if head_rows else None def build_report_xlsx(sheets: list[dict]) -> bytes: """시트 모형 여럿 → 한 통 엑셀.""" wb = openpyxl.Workbook() wb.remove(wb.active) used: set[str] = set() for sheet in sheets: ws = wb.create_sheet(title=sheet_title(sheet.get("name", ""), used)) write_sheet(ws, sheet) if not wb.worksheets: wb.create_sheet("빈 보고서") buf = BytesIO() wb.save(buf) return buf.getvalue() def build_csv(sheet: dict) -> str: """시트 하나 → CSV(쉼표 · 큰따옴표 감쌈 · 엑셀 한글용 BOM 은 호출 쪽).""" rows = sheet.get("rows") or [] max_col = max( (column_index_from_string(k) for r in rows for k in _cells(r)), default=1, ) out = [] for row in rows: vals = [] for c_idx in range(1, max_col + 1): v = row.get(get_column_letter(c_idx)) s = "" if v is None else str(v) vals.append('"' + s.replace('"', '""') + '"' if re.search(r'[",\n]', s) else s) out.append(",".join(vals)) return "\r\n".join(out) + "\r\n"