106cf70922
xlsx 엔 받는 사람이 없거나(Shopee) 가려져(TikTok) 라벨 PDF 를 출처로 사용. 업로드 시 Order ID 로 박스와 조인해 채운다(하이브리드). - pdf_parser.py 추가 (pdfplumber) · Shopee SPX 라벨: 2단 중 왼쪽 열만 읽어 Recipient 블록 추출(전화 미표시) · TikTok 라벨: 배송사별 양식 대응 — To <이름> (+60)…(Ninja), Receiver <이름>(NDD) · 주소 영역에 섞인 10자리+ 바코드 숫자 제거(우편번호 5자리 보존) · PDF 없거나 파싱 실패해도 업로드 진행(PII 만 빈 값) - router: 라벨 PDF 파싱 결과를 parcels 에 머지 후 저장 - requirements: pdfplumber>=0.11 (Docker 재빌드 필요) - new.html: PDF 가 받는 사람 출처임을 안내(권장) - .gitignore: docs/samples/ (실제 고객 PII PDF — 커밋 금지) - 문서 갱신 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
7.1 KiB
Markdown
170 lines
7.1 KiB
Markdown
# 말레이시아 배송 모듈 (dispatch) — TikTok·Shopee 출고관리
|
|
|
|
> 초보 물류 직원이 엑셀을 열지 않고 웹 화면만 보고 출고 작업을 하도록 만든 모듈.
|
|
> main-app ERP 에 편입된 **모듈**이다(별도 앱/포트 아님). 인증은 기존 Google
|
|
> OAuth + 권한키 `dispatch` 를 재사용한다.
|
|
|
|
---
|
|
|
|
## 1. 무엇을 하나
|
|
|
|
플랫폼(TikTok·Shopee)에서 매일 받는 파일을 업로드하면, 직원용 **출고 작업
|
|
리스트** 를 웹에서 자동 생성한다(엑셀 파일로 만들 필요 없음). 플랫폼마다 올리는
|
|
파일이 다르며, 업로드 화면이 플랫폼 선택에 따라 바뀐다.
|
|
|
|
**TikTok**
|
|
|
|
| 파일 | 용도 | 업로드 |
|
|
| --- | --- | --- |
|
|
| 02_Shipping_Label_Packing_Slip.pdf | 실제 포장/라벨 부착 원본 | 선택(보관) |
|
|
| 03_TikTok_Order_Export.xlsx | **자동 출고 리스트 기준 데이터** | **필수** |
|
|
|
|
> 01_Picking_List.pdf 는 더 이상 받지 않는다(피킹 요약은 데이터 엑셀로 생성).
|
|
|
|
**Shopee**
|
|
|
|
| 파일 | 용도 | 업로드 |
|
|
| --- | --- | --- |
|
|
| Shopee Seller Centre.pdf | 택배 송장(라벨) 원본 | 선택(보관) |
|
|
| Packing List.Doorstep Delivery.xlsx | **자동 출고 리스트 기준 데이터** | **필수** |
|
|
|
|
작업 기준 키는 **Order ID / Package ID / Tracking ID / Seller SKU / Quantity** 이고,
|
|
**받는 사람 이름·전화·주소**는 출고 작업 카드 표시 + 취합 출고 엑셀 생성을 위해
|
|
박스 단위로 **dispatch_db 에 저장한다**(개인정보 — 접근/보관 정책 검토 대상).
|
|
|
|
### 1박스 묶음 규칙
|
|
|
|
엑셀 1줄 ≠ 1박스. 아래 우선순위로 1박스를 묶는다.
|
|
|
|
1. **Package ID** → 2. **Tracking ID** → 3. **Order ID**
|
|
|
|
같은 박스 안 같은 SKU 는 수량을 합산한다. 결과는 "1박스 = 1카드".
|
|
|
|
---
|
|
|
|
## 2. 화면
|
|
|
|
| 경로 | 화면 |
|
|
| --- | --- |
|
|
| `GET /dispatch/` | 출고 배치 목록 |
|
|
| `GET /dispatch/batches/new` | 업로드(날짜·플랫폼·파일 3개) |
|
|
| `POST /dispatch/batches` | 업로드 + 배치 생성 + 파싱 + 박스 저장 |
|
|
| `GET /dispatch/batches/{id}` | 출고 작업 리스트(1박스=1카드, 상태 토글·필터·검색) |
|
|
| `GET /dispatch/batches/{id}/picking` | SKU별 피킹 요약 |
|
|
| `GET /dispatch/batches/{id}/handover` | Kagayaku 전달 리스트(A4 인쇄용) |
|
|
| `POST /dispatch/parcels/{id}/toggle` | 작업 상태 1개 토글(AJAX, 즉시 저장) |
|
|
|
|
작업 상태(카드 버튼, 누르면 초록색): 상품준비 완료 → 포장완료 → 라벨부착 완료 →
|
|
Kagayaku 전달 완료 → 택배스캔 확인. 모든 변경은 `dispatch_logs` 에 기록된다.
|
|
|
|
---
|
|
|
|
## 3. 설치 / 실행
|
|
|
|
### 3-1. DB 초기화 (superuser 로 1회)
|
|
|
|
```bash
|
|
read -s -p "dispatch_app password: " APP_PWD; echo
|
|
docker exec -i postgres-db psql -U postgres \
|
|
-v app_password="$APP_PWD" \
|
|
< scripts/sql/dispatch_db_init.sql
|
|
```
|
|
|
|
생성물: DB `dispatch_db`, 역할 `dispatch_app`(CRUD only), 테이블
|
|
`dispatch_batches / dispatch_parcels / dispatch_items / dispatch_logs`.
|
|
멱등 스크립트라 여러 번 실행해도 기존 데이터를 지우지 않는다.
|
|
|
|
### 3-2. .env 등록
|
|
|
|
```
|
|
DISPATCH_DB_URL=postgresql://dispatch_app:<APP_PWD>@postgres-db:5432/dispatch_db
|
|
```
|
|
|
|
미설정 시 모듈은 "설정 필요" 안내만 보여주고 앱은 정상 기동한다(JSON 폴백 없음).
|
|
|
|
### 3-3. 재배포
|
|
|
|
```bash
|
|
cd /opt/www/main && docker compose up -d --build web
|
|
```
|
|
|
|
> `git pull` 후 반드시 `--build`. `restart` 만으로는 코드가 안 바뀐다.
|
|
|
|
### 3-4. 권한 부여
|
|
|
|
`/admin` 에서 대상 사용자에게 권한키 `dispatch`(라벨 "말레이시아 배송")를 켠다.
|
|
관리자(admin)는 항상 통과.
|
|
|
|
---
|
|
|
|
## 4. 업로드 원본 보관
|
|
|
|
업로드한 PDF/XLSX 원본은 아래에 저장되고 DB 에는 경로만 기록한다.
|
|
|
|
```
|
|
# TikTok
|
|
$DATA_DIR/dispatch/2026-06-19/<배치slug>_<HHMMSS>/
|
|
02_Shipping_Label_Packing_Slip.pdf
|
|
03_TikTok_Order_Export.xlsx
|
|
# Shopee
|
|
$DATA_DIR/dispatch/2026-06-19/<배치slug>_<HHMMSS>/
|
|
Shopee_Seller_Centre.pdf
|
|
Packing_List.Doorstep_Delivery.xlsx
|
|
```
|
|
|
|
배치 목록의 **다운로드** 버튼은 위 업로드 원본에 더해, 발송 날짜·고객 이름·전화·
|
|
주소·상품명·아이템 코드·주문 수량·오더번호·택배사·송장번호·주문처를 취합한
|
|
**출고 엑셀**을 같은 zip 에 함께 넣는다. 엑셀 파일명: `YYYY.MM.DD(Ddd)_tictoc.xlsx`
|
|
(Shopee 는 끝이 `_shopee`).
|
|
|
|
같은 날짜/플랫폼이라도 batch_name 으로 구분해 **새 배치**로 생성된다(덮어쓰지 않음).
|
|
예: "2026-06-19 TikTok 오전 출고", "2026-06-19 TikTok 오후 출고".
|
|
|
|
---
|
|
|
|
## 5. 파싱 예외 처리
|
|
|
|
- 필수 컬럼이 없으면 **어떤 컬럼이 없는지** 화면에 표시한다.
|
|
- Quantity 가 비면 1 로 처리한다.
|
|
- Tracking ID 가 숫자로 읽혀도 문자열로 보존한다(과학표기/`.0` 제거).
|
|
- NaN/None 은 빈 문자열로 처리한다.
|
|
- 받는 사람 이름/전화/주소 컬럼은 매핑되면 **박스 단위로 보존**한다(작업 카드·엑셀).
|
|
- 헤더가 첫 행이 아닐 수 있어(Shopee 는 제목 행이 위에 붙을 수 있음) 상단 몇 행을
|
|
훑어 필수 컬럼이 잡히는 첫 행을 헤더로 본다.
|
|
|
|
가능한 헤더(앞뒤 공백·대소문자 무시, TikTok·Shopee 별칭은 `store.COLUMN_ALIASES`):
|
|
Order ID(또는 Order SN), Package ID, Tracking ID(또는 Tracking Number),
|
|
Shipping Provider Name(또는 Shipping Option/Courier), Seller SKU(또는 SKU Reference No.),
|
|
Product Name, Quantity, Recipient(받는 사람), Phone(전화), Address(주소).
|
|
|
|
---
|
|
|
|
## 6. 테스트
|
|
|
|
박스 묶기/SKU 합산/컬럼 검증 순수 로직 테스트:
|
|
|
|
```bash
|
|
python -m app.modules.dispatch.tests.test_grouping # 앱 의존성 설치 환경에서
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 기술 메모
|
|
|
|
- 스택: FastAPI · PostgreSQL(psycopg3) · Jinja2 · 기존 ERP CSS(Bootstrap 미사용,
|
|
레포 공통 `erp.css` 재사용) · **openpyxl**(스펙의 pandas 대신, 레포 의존성 경량 유지).
|
|
- 코드: `app/modules/dispatch/` (`store.py` 순수 로직 / `parser.py` 엑셀 파싱 /
|
|
`pdf_parser.py` 라벨 PDF 받는사람 추출 / `export.py` 취합 출고 엑셀 생성 /
|
|
`db.py` DB I/O / `router.py` 라우팅 / `templates/dispatch/` 화면).
|
|
- 데이터 출처(하이브리드):
|
|
- **xlsx** = 박스/SKU/수량/주문/송장. TikTok 은 표준 컬럼, Shopee 는 SKU·수량이
|
|
`product_info` 한 셀에 묶여 있어 `store.parse_shopee_product_info` 로 분해.
|
|
- **라벨 PDF** = 받는 사람 이름/전화/주소. xlsx 엔 없거나 가려져 있어 PDF 가 출처.
|
|
업로드 시 Order ID 로 박스와 조인(`pdf_parser.extract_recipients`, pdfplumber).
|
|
TikTok 라벨은 배송사마다 양식이 달라 `To <이름> (+60)…`(Ninja)·`Receiver <이름>`
|
|
(NDD) 등을 모두 처리. Shopee SPX 라벨은 2단 중 왼쪽 열만 읽는다(전화 미표시).
|
|
PDF 가 없거나 파싱 실패해도 업로드는 진행(해당 PII 만 빈 값).
|
|
- 개인정보 보관: `dispatch_parcels.recipient_name/recipient_phone/recipient_address`.
|
|
기존 DB 에는 마이그레이션 `scripts/sql/dispatch_add_recipient_columns.sql` 로 컬럼을
|
|
추가한다(멱등). 접근/보관 기간 정책을 별도로 검토할 것.
|