feat(dispatch): 일괄 완료 버튼(모두 라벨부착/모두 Kagayaku 전달) + 검색바 축소

- 출고 작업 툴바 우측에 일괄 버튼 2개. 배치 내 전 박스 상태를 완료로 일괄 설정
- db.bulk_set_status: 변경된 박스만 로그(set 기반 INSERT), 화이트리스트 검증
- 라우터 POST /batches/{id}/bulk
- 검색 입력칸 폭 축소(flex 0 1 240px)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 15:29:55 +09:00
parent ef4658fb1d
commit a1dd9bed4a
3 changed files with 84 additions and 1 deletions
+36
View File
@@ -250,6 +250,42 @@ class DispatchStore:
)
return {"parcel_id": parcel_id, "field": field, "value": new_value}
def bulk_set_status(
self, *, batch_id: int, field: str, value: bool = True, worker_name: str = ""
) -> int:
"""배치 내 모든 박스의 작업 상태 1개를 value 로 일괄 설정. 변경된 박스만 로그.
반환: 실제로 바뀐 박스 수(이미 value 인 박스는 건드리지 않음).
field 는 store.STATUS_FIELDS 화이트리스트 검증(SQL 식별자 안전).
"""
if field not in store.STATUS_FIELDS:
raise ValueError(f"허용되지 않는 작업 상태: {field}")
with self._pool.connection() as conn:
with conn.transaction():
rows = conn.execute(
# field 는 화이트리스트라 인젝션 위험 없음.
f"UPDATE dispatch_parcels SET {field} = %s "
f"WHERE batch_id = %s AND {field} = %s RETURNING id",
(value, batch_id, not value),
).fetchall()
ids = [r["id"] for r in rows]
if ids:
conn.execute(
"""
INSERT INTO dispatch_logs
(parcel_id, action, old_value, new_value, worker_name)
SELECT unnest(%s::bigint[]), %s, %s, %s, %s
""",
(
ids,
field,
str(not value).lower(),
str(value).lower(),
(worker_name or "").lower().strip(),
),
)
return len(ids)
def parcel_batch_id(self, *, parcel_id: int) -> int | None:
with self._pool.connection() as conn:
row = conn.execute(