feat(dispatch): Shopee 플랫폼 추가 + 받는 사람 정보 저장/출고 엑셀
- 플랫폼 TikTok/Shopee 분리: 업로드 화면이 선택에 따라 파일 안내 변경 (TikTok=Order Export xlsx, Shopee=Packing List xlsx / 라벨 PDF 선택) - TikTok Picking List.pdf 업로드 제거(피킹 요약은 데이터 엑셀로 생성) - 파서 공용화 + 헤더 행 자동탐지(Shopee 제목행 대응), 두 포맷 헤더 별칭 통합 - 받는 사람 이름/전화/주소를 박스 단위로 dispatch_db 에 저장(개인정보) · init SQL 갱신 + 기존 DB용 멱등 마이그레이션 dispatch_add_recipient_columns.sql - 출고 작업 카드: Package 제거, 받는 사람 이름(크게)+주소(여러 줄)+전화 표시 - 배치 다운로드 zip 에 취합 출고 엑셀 포함 파일명 YYYY.MM.DD(Ddd)_tictoc.xlsx / _shopee.xlsx (export.py) - 문서(CLAUDE.md, DISPATCH_MODULE.md) PII 보관·Shopee 반영 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
"""TikTok Order Export XLSX 파서 (openpyxl).
|
||||
"""출고 XLSX 파서 (openpyxl) — TikTok / Shopee 공용.
|
||||
|
||||
요구사항 2/11 구현. pandas 대신 레포에 이미 있는 openpyxl 로 읽어 의존성을
|
||||
가볍게 유지한다(동작/결과는 동일: 헤더 정규화 → 표준키 매핑 → 박스 묶기).
|
||||
pandas 대신 레포에 이미 있는 openpyxl 로 읽어 의존성을 가볍게 유지한다
|
||||
(동작: 헤더 정규화 → 표준키 매핑 → 박스 묶기).
|
||||
|
||||
- 첫 시트의 첫 행을 헤더로 본다. 헤더 앞뒤 공백 제거 후 표준키로 매핑한다.
|
||||
- 매핑되지 않는 열(고객 이름/주소/전화 등)은 읽기 단계에서 버린다 → 개인정보
|
||||
미보관(요구사항 11).
|
||||
- TikTok: 03_TikTok_Order_Export.xlsx, Shopee: Packing List.Doorstep Delivery.xlsx.
|
||||
두 포맷 모두 같은 표준키로 매핑된다(store.COLUMN_ALIASES). 헤더가 첫 행이 아닐
|
||||
수 있어(Shopee 는 제목 행이 위에 붙는 경우가 있다) 상단 몇 행을 훑어 필수 컬럼이
|
||||
잡히는 첫 행을 헤더로 본다.
|
||||
- 받는 사람(이름/전화/주소) 컬럼은 매핑되면 보존한다(작업 카드 표시 + 출고 엑셀).
|
||||
- Tracking ID 는 숫자로 읽혀도 문자열로 보존(store.clean_text).
|
||||
- Quantity 가 비면 1, NaN/None 은 빈 문자열로 처리.
|
||||
- 필수 컬럼이 없으면 어떤 컬럼이 없는지 ParseError 로 알린다.
|
||||
@@ -22,10 +24,15 @@ class ParseError(Exception):
|
||||
"""엑셀 파싱 실패. message 는 사용자에게 그대로 보여줄 한국어 메시지."""
|
||||
|
||||
|
||||
def parse_order_export(path: str) -> dict[str, Any]:
|
||||
"""XLSX 경로 → {"parcels": [...], "row_count": int}.
|
||||
# 헤더가 첫 행이 아닐 수 있어 상단에서 헤더 행을 찾을 때 훑는 최대 행 수.
|
||||
_HEADER_SCAN_ROWS = 15
|
||||
|
||||
|
||||
def parse_export(path: str) -> dict[str, Any]:
|
||||
"""출고 XLSX 경로 → {"parcels": [...], "row_count": int}.
|
||||
|
||||
parcels 는 store.group_parcels 결과(1박스=1항목). 실패 시 ParseError.
|
||||
TikTok·Shopee 공용(표준키 매핑이 흡수한다).
|
||||
"""
|
||||
try:
|
||||
from openpyxl import load_workbook # noqa: WPS433 — 지연 import
|
||||
@@ -42,22 +49,20 @@ def parse_order_export(path: str) -> dict[str, Any]:
|
||||
|
||||
try:
|
||||
ws = wb.worksheets[0]
|
||||
rows_iter = ws.iter_rows(values_only=True)
|
||||
try:
|
||||
header = next(rows_iter)
|
||||
except StopIteration:
|
||||
all_rows = list(ws.iter_rows(values_only=True))
|
||||
if not all_rows:
|
||||
raise ParseError("엑셀에 데이터가 없습니다(빈 파일).")
|
||||
|
||||
mapping = store.resolve_columns(header)
|
||||
missing = store.missing_required(mapping)
|
||||
if missing:
|
||||
header_idx, mapping = _find_header(all_rows)
|
||||
if header_idx is None:
|
||||
missing = store.missing_required({})
|
||||
raise ParseError("엑셀에서 필요한 컬럼을 찾지 못했습니다: " + ", ".join(missing))
|
||||
|
||||
norm_rows: list[dict[str, str]] = []
|
||||
for raw in rows_iter:
|
||||
for raw in all_rows[header_idx + 1:]:
|
||||
if raw is None:
|
||||
continue
|
||||
# 표준키만 추출(매핑 안 된 열 = 개인정보 등은 버린다).
|
||||
# 표준키만 추출(매핑 안 된 열은 버린다).
|
||||
record = {
|
||||
key: store.clean_text(raw[idx]) if idx < len(raw) else ""
|
||||
for key, idx in mapping.items()
|
||||
@@ -78,6 +83,24 @@ def parse_order_export(path: str) -> dict[str, Any]:
|
||||
return {"parcels": parcels, "row_count": len(norm_rows)}
|
||||
|
||||
|
||||
# 하위 호환 별칭(기존 호출부/테스트가 쓰던 이름).
|
||||
parse_order_export = parse_export
|
||||
|
||||
|
||||
def _find_header(rows: list) -> tuple[int | None, dict[str, int]]:
|
||||
"""상단 행들을 훑어 필수 컬럼이 잡히는 첫 행을 헤더로 본다.
|
||||
|
||||
반환: (헤더 행 인덱스, {표준키: 열 인덱스}). 못 찾으면 (None, {}).
|
||||
"""
|
||||
for idx, raw in enumerate(rows[:_HEADER_SCAN_ROWS]):
|
||||
if raw is None:
|
||||
continue
|
||||
mapping = store.resolve_columns(raw)
|
||||
if not store.missing_required(mapping):
|
||||
return idx, mapping
|
||||
return None, {}
|
||||
|
||||
|
||||
def _is_description_row(record: dict[str, str]) -> bool:
|
||||
"""헤더 아래 '컬럼 설명' 행 판별.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user