2026-08-06 11:29:00 +09:00

205 lines
8.4 KiB
Python

# -*- coding: utf-8 -*-
"""
xlsxfill.py — LG PRI 표준 양식 xlsx 헬퍼 (ref 파일 불필요 · 완전 독립)
스타일 상수는 LG PRI 표준 양식에서 추출한 값으로 하드코딩됨(원본 ref 파일에 의존하지 않는다).
모든 파일 구조(배너·헤더·데이터·그룹병합)를 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
실행(저장소 루트에서, 맨 파일명 금지 — 셸 cwd는 호출마다 초기화된다):
PYTHONUTF8=1 py .claude/skills/ubisam-deliverables/references/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