diff --git a/app/integrations/google_sheets.py b/app/integrations/google_sheets.py index cf9e844..00c3f84 100644 --- a/app/integrations/google_sheets.py +++ b/app/integrations/google_sheets.py @@ -30,6 +30,16 @@ SCOPES = ["https://www.googleapis.com/auth/spreadsheets"] TOKEN_URI = "https://oauth2.googleapis.com/token" +def _hex_color(value: str) -> dict[str, float]: + """"RRGGBB" → Sheets API 색상.""" + v = (value or "").lstrip("#") + return { + "red": int(v[0:2], 16) / 255.0, + "green": int(v[2:4], 16) / 255.0, + "blue": int(v[4:6], 16) / 255.0, + } + + class GoogleSheetsWriter: """시트 1장을 통째로 다시 쓰는 용도의 얇은 래퍼.""" @@ -157,10 +167,13 @@ class GoogleSheetsWriter: last_col: int, title_row: int, widths: dict[int, int], + header_bg: str = "", + row_highlights: list[tuple[int, int, str]] | None = None, ) -> dict[str, Any]: """시트를 만들고(있으면 비우고) 표를 통째로 기록한다. 좌표는 모두 1-based(엑셀과 동일). 반환값에 시트 URL 을 담는다. + header_bg / row_highlights 의 색은 "RRGGBB" 16진 문자열. """ if not self.enabled: raise RuntimeError(self.reason or "Google Sheets 미설정") @@ -213,12 +226,19 @@ class GoogleSheetsWriter: "textFormat": {"bold": True, "fontSize": 16}}, "userEnteredFormat(horizontalAlignment,verticalAlignment,textFormat)", )) - requests.append(_fmt( - header_row, first_col, header_row, last_col, - {"horizontalAlignment": "CENTER", "verticalAlignment": "MIDDLE", - "textFormat": {"bold": True}}, - "userEnteredFormat(horizontalAlignment,verticalAlignment,textFormat)", - )) + header_fmt: dict[str, Any] = { + "horizontalAlignment": "CENTER", "verticalAlignment": "MIDDLE", + "textFormat": {"bold": True}, + } + header_fields = "userEnteredFormat(horizontalAlignment,verticalAlignment,textFormat)" + if header_bg: + header_fmt["backgroundColor"] = _hex_color(header_bg) + header_fields = ( + "userEnteredFormat(horizontalAlignment,verticalAlignment," + "textFormat,backgroundColor)" + ) + requests.append(_fmt(header_row, first_col, header_row, last_col, + header_fmt, header_fields)) if last_row >= first_data_row: requests.append(_fmt( first_data_row, first_col, last_row, last_col, @@ -245,6 +265,14 @@ class GoogleSheetsWriter: } }) + # 특정 행 구간 배경색(예: 파렛트 출고 블록) + for (r1, r2, color) in (row_highlights or []): + requests.append(_fmt( + r1, first_col, r2, last_col, + {"backgroundColor": _hex_color(color)}, + "userEnteredFormat(backgroundColor)", + )) + for col, width in widths.items(): requests.append({ "updateDimensionProperties": { diff --git a/app/modules/cupang/export.py b/app/modules/cupang/export.py index 271b50e..cc97462 100644 --- a/app/modules/cupang/export.py +++ b/app/modules/cupang/export.py @@ -28,7 +28,10 @@ COL_FIRST = 2 COL_LAST = 12 # 센터 단위로 병합되는 열 MERGE_COLS = (3, 4, 5, 6, 7, 11, 12) -WIDTHS = {2: 6, 3: 12, 4: 12, 5: 12, 6: 11, 7: 10, 8: 12, 9: 26, 10: 8, 11: 12, 12: 11} +WIDTHS = {1: 10, 2: 6, 3: 12, 4: 12, 5: 12, 6: 11, 7: 10, 8: 12, 9: 26, 10: 8, 11: 12, 12: 11} +HEADER_BG = "DBE9F7" # 머리글 행 배경 +PALLET_BG = "FAE2D5" # 출고방식이 파렛트인 센터 블록 배경 +PALLET_METHOD = "파렛트" TITLE_ROW = 2 COMPANY_ROW = 4 @@ -36,9 +39,20 @@ HEADER_ROW = 5 FIRST_DATA_ROW = 6 +def with_dow(value: str) -> str: + """YYYY-MM-DD → "YYYY-MM-DD(요일)". 날짜가 아니면 원문 그대로.""" + text = (value or "").strip() + try: + d = _date.fromisoformat(text) + except ValueError: + return text + return f"{text}({DOW[d.weekday()]})" + + def sheet_title(ship_date: str) -> str: - """시트명 = 출고일 YYYYMMDD.""" - return _date.fromisoformat(ship_date).strftime("%Y%m%d") + """시트명 = 출고일 YYYYMMDD(요일).""" + d = _date.fromisoformat(ship_date) + return f"{d.strftime('%Y%m%d')}({DOW[d.weekday()]})" def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, Any]: @@ -53,6 +67,7 @@ def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, An d = _date.fromisoformat(ship_date) tag = d.strftime("%Y%m%d") title = f"{tag}({DOW[d.weekday()]}) 쿠팡로켓 밀크런 출고리스트" + pallet_ranges: list[tuple[int, int]] = [] cells: dict[tuple[int, int], Any] = {(TITLE_ROW, COL_FIRST): title, (COMPANY_ROW, COL_FIRST): COMPANY} @@ -77,12 +92,17 @@ def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, An row += 1 end = row - 1 + method = sh.get("ship_method") or "" + if method == PALLET_METHOD: + pallet_ranges.append((start, end)) + block = { - 3: str(sh.get("document_date") or ""), - 4: str(sh.get("ship_date") or ""), - 5: str(sh.get("center_arrival_date") or ""), + # 날짜는 요일까지 표기 — 2026-09-03(목) + 3: with_dow(str(sh.get("document_date") or "")), + 4: with_dow(str(sh.get("ship_date") or "")), + 5: with_dow(str(sh.get("center_arrival_date") or "")), 6: sh.get("center_name_snapshot") or "", - 7: sh.get("ship_method") or "", + 7: method, 11: sh.get("outbound_summary") or "", 12: WORKER, } @@ -91,13 +111,19 @@ def build_table(ship_date: str, shipments: list[dict[str, Any]]) -> dict[str, An if end > start: merges.append((start, col, end, col)) - return {"title": title, "cells": cells, "merges": merges, "last_row": row - 1} + return { + "title": title, + "cells": cells, + "merges": merges, + "last_row": row - 1, + "pallet_ranges": pallet_ranges, + } def build_workbook(ship_date: str, shipments: list[dict[str, Any]]) -> Any: - """xlsx 워크북(openpyxl). 시트명 = YYYYMMDD.""" + """xlsx 워크북(openpyxl). 시트명 = YYYYMMDD(요일).""" from openpyxl import Workbook - from openpyxl.styles import Alignment, Border, Font, Side + from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter table = build_table(ship_date, shipments) @@ -121,11 +147,13 @@ def build_workbook(ship_date: str, shipments: list[dict[str, Any]]) -> Any: title_cell.font = Font(size=16, bold=True) title_cell.alignment = center_align + header_fill = PatternFill("solid", fgColor=HEADER_BG) for col in range(COL_FIRST, COL_LAST + 1): head = ws.cell(row=HEADER_ROW, column=col) head.font = Font(bold=True) head.alignment = center_align head.border = box + head.fill = header_fill for row in range(FIRST_DATA_ROW, table["last_row"] + 1): for col in range(COL_FIRST, COL_LAST + 1): @@ -133,6 +161,13 @@ def build_workbook(ship_date: str, shipments: list[dict[str, Any]]) -> Any: cell.border = box cell.alignment = left_align if col == 9 else center_align + # 파렛트 출고 블록은 배경색으로 구분 + pallet_fill = PatternFill("solid", fgColor=PALLET_BG) + for (r1, r2) in table["pallet_ranges"]: + for row in range(r1, r2 + 1): + for col in range(COL_FIRST, COL_LAST + 1): + ws.cell(row=row, column=col).fill = pallet_fill + for col, width in WIDTHS.items(): ws.column_dimensions[get_column_letter(col)].width = width ws.row_dimensions[TITLE_ROW].height = 28 diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index 47f6047..ed00eb2 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -265,8 +265,8 @@ def _push_to_google_sheet(store: Any, ship_date: str) -> dict[str, Any]: from app.integrations.google_sheets import get_writer # noqa: WPS433 from .export import ( # noqa: WPS433 - COL_FIRST, COL_LAST, FIRST_DATA_ROW, HEADER_ROW, TITLE_ROW, WIDTHS, - build_table, sheet_title, + COL_FIRST, COL_LAST, FIRST_DATA_ROW, HEADER_BG, HEADER_ROW, PALLET_BG, + TITLE_ROW, WIDTHS, build_table, sheet_title, ) spreadsheet_id = (os.getenv("CUPANG_SHEET_ID") or "").strip() @@ -295,6 +295,8 @@ def _push_to_google_sheet(store: Any, ship_date: str) -> dict[str, Any]: last_col=COL_LAST, title_row=TITLE_ROW, widths=WIDTHS, + header_bg=HEADER_BG, + row_highlights=[(r1, r2, PALLET_BG) for (r1, r2) in table["pallet_ranges"]], ) except Exception as exc: # noqa: BLE001 - 시트 기록 실패는 경고로만 알린다 return {"ok": False, "skipped": False, "reason": f"{type(exc).__name__}: {exc}"}