style(expense): 승인 목록 엑셀 서식 — 제목행 파랑/금액 쉼표/열너비 자동

- 제목행: 파랑(4472C4) 배경 + 흰 굵은 글씨 + 가운데 정렬
- 금액/합계금액: #,##0 쉼표 표시형식
- A~K 열너비 내용 기준 자동(한글/전각 2칸 계산)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 17:57:21 +09:00
parent 6ae0d90b97
commit 5b0fe2f1a1
+53 -7
View File
@@ -402,12 +402,52 @@ async def approved_page(request: Request) -> HTMLResponse:
)
def _xl_display_len(value: Any) -> int:
"""엑셀 열너비 산정용 표시 폭. 한글/전각은 2칸으로 계산."""
s = "" if value is None else str(value)
width = 0
for ch in s:
# CJK(한글/한자/가나) 및 전각 문자 → 2칸
width += 2 if ord(ch) > 0x2E7F else 1
return width
def _autofit_columns(ws: Any, ncols: int, *, min_w: int = 6, max_w: int = 60) -> None:
"""A~열 내용 기준 자동 너비. 헤더 포함 최댓값 + 여유."""
for col in range(1, ncols + 1):
longest = 0
for cell in ws[ws.cell(row=1, column=col).column_letter]:
longest = max(longest, _xl_display_len(cell.value))
ws.column_dimensions[ws.cell(row=1, column=col).column_letter].width = (
max(min_w, min(max_w, longest + 2))
)
def _style_header(ws: Any, ncols: int) -> None:
"""제목 행: 파랑 배경, 흰 굵은 글씨, 가운데 정렬."""
from openpyxl.styles import Alignment, Font, PatternFill
fill = PatternFill(start_color="FF4472C4", end_color="FF4472C4", fill_type="solid")
font = Font(color="FFFFFFFF", bold=True)
align = Alignment(horizontal="center", vertical="center")
for col in range(1, ncols + 1):
cell = ws.cell(row=1, column=col)
cell.fill = fill
cell.font = font
cell.alignment = align
def _build_approved_xlsx(
items: list[dict[str, Any]], names: dict[str, str]
) -> io.BytesIO:
"""승인완료 목록 + 직원별합계 시트 xlsx 바이트 생성."""
"""승인완료 목록 + 직원별합계 시트 xlsx 바이트 생성.
서식: 제목행 파랑/흰굵은글씨/가운데, 금액 쉼표(#,##0), 열너비 내용 자동.
"""
from openpyxl import Workbook # 지연 import
money_fmt = "#,##0"
wb = Workbook()
ws = wb.active
ws.title = "승인완료"
@@ -416,6 +456,7 @@ def _build_approved_xlsx(
"금액", "메모", "상태", "승인자", "결재일시",
]
ws.append(header)
amount_col = header.index("금액") + 1 # 7 (G열)
for r in items:
owner = r.get("owner", "")
ws.append([
@@ -431,17 +472,22 @@ def _build_approved_xlsx(
r.get("approver_email", "") or "",
r.get("decided_at", "") or "",
])
widths = [12, 14, 28, 10, 12, 24, 12, 30, 10, 24, 22]
for col, w in enumerate(widths, start=1):
ws.column_dimensions[ws.cell(row=1, column=col).column_letter].width = w
# 금액 열 쉼표 표시형식
for row in range(2, ws.max_row + 1):
ws.cell(row=row, column=amount_col).number_format = money_fmt
_style_header(ws, len(header))
_autofit_columns(ws, len(header))
# 직원별 합계 시트
ws2 = wb.create_sheet("직원별합계")
ws2.append(["이름", "이메일", "건수", "합계금액"])
header2 = ["이름", "이메일", "건수", "합계금액"]
ws2.append(header2)
for row in _approved_aggregate(items, names):
ws2.append([row["name"], row["owner"], row["count"], row["total"]])
for col, w in enumerate([14, 28, 8, 16], start=1):
ws2.column_dimensions[ws2.cell(row=1, column=col).column_letter].width = w
for row in range(2, ws2.max_row + 1):
ws2.cell(row=row, column=4).number_format = money_fmt # 합계금액
_style_header(ws2, len(header2))
_autofit_columns(ws2, len(header2))
buf = io.BytesIO()
wb.save(buf)