# -*- coding: utf-8 -*- """ xlsxfill.py — LG PRI 표준 양식 xlsx 헬퍼 (ref 파일 불필요 · 완전 독립) 스타일 상수는 ref(공소수정기·FMM) 파일에서 직접 추출한 값으로 하드코딩됨. 모든 파일 구조(배너·헤더·데이터·그룹병합)를 openpyxl만으로 재현한다. 사용법: import sys sys.path.insert(0, os.path.dirname(__file__)) from xlsxfill import ( make_workbook, set_col_widths, set_row_height, write_banner, write_header_row, write_data_rows, group_merge, BANNER_FONT, BANNER_FILL, HEADER_FONT, HEADER_FILL, DATA_FONT, DATA_FILL_ODD, DATA_FILL_EVEN, FUNC_BANNER_FONT, FUNC_HEADER_FONT, FUNC_DATA_FONT, TACT_FONT, TACT_BORDER, _apply, _border, ) 전제조건: Python 3.9+, openpyxl >= 3.1 실행: set PYTHONUTF8=1 && py build_02설계_xlsx.py """ import openpyxl from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Border, Side, Alignment, GradientFill from openpyxl.utils import get_column_letter from openpyxl.utils.cell import range_boundaries # ───────────────────────────────────────────── # 스타일 상수 (ref 파일 직접 추출값) # ───────────────────────────────────────────── _F = Font _P = PatternFill _B = Border _S = Side _A = Alignment def _border(left=None, right=None, top=None, bottom=None): def s(v): return _S(border_style=v) if v else _S() return _B(left=s(left), right=s(right), top=s(top), bottom=s(bottom)) # 배너 행 (B2) — 진네이비 배경, 흰 굵은 글자 BANNER_FONT = _F(name="맑은 고딕", size=13, bold=True, color="FFFFFFFF") BANNER_FILL = _P("solid", fgColor="FF1F4E78") BANNER_BORDER = _border("medium","medium","medium","medium") BANNER_ALIGN = _A(horizontal="center", vertical="center", wrap_text=True) # 부제 행 (B3) — 테두리만 SUBBANNER_FONT = _F(name="맑은 고딕", size=9) SUBBANNER_BORDER = _border("medium","medium",None,None) SUBBANNER_ALIGN = _A(horizontal="left", vertical="center", wrap_text=True) # 헤더 행 (B4/B5 등) — 파랑 배경, 흰 굵은 글자 HEADER_FONT = _F(name="맑은 고딕", size=11, bold=True, color="FFFFFFFF") HEADER_FILL = _P("solid", fgColor="FF2E75B6") HEADER_BORDER_FIRST = _border("medium","thin","thin","thin") HEADER_BORDER_MID = _border("thin","thin","thin","thin") HEADER_BORDER_LAST = _border("thin","medium","thin","thin") HEADER_ALIGN = _A(horizontal="center", vertical="center", wrap_text=True) # 데이터 행 — 교대 배경(연파랑) + 흰 배경 DATA_FONT = _F(name="맑은 고딕", size=10) DATA_FILL_ODD = _P("solid", fgColor="FFDDEBF7") # 연파랑 (홀수행) DATA_FILL_EVEN = _P("solid", fgColor="FFFFFFFF") # 흰색 (짝수행) DATA_BORDER_FIRST = _border("medium","thin","thin","thin") DATA_BORDER_MID = _border("thin","thin","thin","thin") DATA_BORDER_LAST = _border("thin","medium","thin","thin") DATA_ALIGN_L = _A(horizontal="left", vertical="center", wrap_text=True) DATA_ALIGN_C = _A(horizontal="center", vertical="center", wrap_text=True) # 기능명세서 전용 스타일 (FUNC ref: 맑은 고딕, 14pt 배너) FUNC_BANNER_FONT = _F(name="맑은 고딕", size=14, bold=True) FUNC_HEADER_FONT = _F(name="맑은 고딕", size=11, bold=True) FUNC_HEADER_BORDER = _border("thin","thin","thin","thin") FUNC_DATA_FONT = _F(name="맑은 고딕", size=11) FUNC_DATA_BORDER = _border("thin","thin","thin","thin") FUNC_DATA_ALIGN = _A(horizontal="left", vertical="center", wrap_text=True) # TactTime 전용 (단순 thin 테두리, 색상 없음) TACT_FONT = _F(name="맑은 고딕", size=11) TACT_BORDER = _border("thin","thin","thin","thin") TACT_ALIGN = _A(horizontal="center", vertical="center", wrap_text=True) TACT_ALIGN_L= _A(horizontal="left", vertical="center", wrap_text=True) # ───────────────────────────────────────────── # 헬퍼 함수 # ───────────────────────────────────────────── _UNSET = object() # write_header_row fill/font 기본값 sentinel def make_workbook(): wb = Workbook() wb.remove(wb.active) # 기본 Sheet 제거 return wb def set_col_widths(ws, widths: dict): """widths: {'A':4.4, 'B':16.0, ...}""" for col, w in widths.items(): ws.column_dimensions[col].width = w def set_row_height(ws, row, height): ws.row_dimensions[row].height = height def _apply(cell, font=None, fill=None, border=None, alignment=None): if font: cell.font = font if fill: cell.fill = fill if border: cell.border = border if alignment: cell.alignment = alignment def write_banner(ws, row, col_start, col_end, title, subtitle=None, banner_h=26, sub_h=24): """배너 2행(제목+부제)을 쓰고 병합한다.""" set_row_height(ws, row, banner_h) # 제목 c = ws.cell(row, col_start, title) _apply(c, BANNER_FONT, BANNER_FILL, BANNER_BORDER, BANNER_ALIGN) L = get_column_letter if col_end > col_start: ws.merge_cells(f"{L(col_start)}{row}:{L(col_end)}{row}") # 우측 셀 border 유지 ws.cell(row, col_end).border = _border(right="medium", top="medium", bottom="medium") if subtitle is not None: set_row_height(ws, row+1, sub_h) c2 = ws.cell(row+1, col_start, subtitle) _apply(c2, SUBBANNER_FONT, border=SUBBANNER_BORDER, alignment=SUBBANNER_ALIGN) if col_end > col_start: ws.merge_cells(f"{L(col_start)}{row+1}:{L(col_end)}{row+1}") ws.cell(row+1, col_end).border = _border(right="medium") def write_header_row(ws, row, col_start, headers, height=22, font=_UNSET, fill=_UNSET): """헤더 행 작성. 첫/마지막 열 medium 테두리. fill=None 을 넘기면 배경 없음(흰색). 생략하면 HEADER_FILL(파랑) 적용. font=None 을 넘기면 폰트 미설정. 생략하면 HEADER_FONT 적용. """ set_row_height(ws, row, height) _font = HEADER_FONT if font is _UNSET else font _fill = HEADER_FILL if fill is _UNSET else fill n = len(headers) for i, h in enumerate(headers): col = col_start + i c = ws.cell(row, col, h) if i == 0: border = HEADER_BORDER_FIRST elif i == n-1: border = HEADER_BORDER_LAST else: border = HEADER_BORDER_MID _apply(c, _font, _fill, border, HEADER_ALIGN) def write_data_rows(ws, data_start, col_start, rows, center_cols=None, height=None, font=None, odd_fill=True): """ rows: list[list] 각 행의 값. center_cols: set of 0-based 인덱스 → center align. 나머지 left. odd_fill: True=홀/짝 교대 배경, False=배경 없음. """ n_cols = max((len(r) for r in rows), default=0) center_cols = center_cols or set() for i, rowvals in enumerate(rows): r = data_start + i if height: set_row_height(ws, r, height) is_odd = (i % 2 == 0) fill = (DATA_FILL_ODD if is_odd else DATA_FILL_EVEN) if odd_fill else None for j in range(n_cols): col = col_start + j c = ws.cell(r, col) c.value = rowvals[j] if j < len(rowvals) else None if j == 0: border = DATA_BORDER_FIRST elif j == n_cols-1: border = DATA_BORDER_LAST else: border = DATA_BORDER_MID align = DATA_ALIGN_C if j in center_cols else DATA_ALIGN_L _apply(c, font or DATA_FONT, fill, border, align) def group_merge(ws, col, data_start, n, key_rows): """동일 key_rows 값의 연속 행을 col열에서 수직 병합.""" i = 0 while i < n: k = key_rows[i] if k is None: i += 1 continue j = i + 1 while j < n and key_rows[j] == k: j += 1 if j - i > 1: L = get_column_letter(col) ws.merge_cells(f"{L}{data_start+i}:{L}{data_start+j-1}") i = j