204 lines
8.2 KiB
Python
204 lines
8.2 KiB
Python
# -*- 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
|