diff --git a/app/modules/dispatch/db.py b/app/modules/dispatch/db.py index 87d2056..8b75722 100644 --- a/app/modules/dispatch/db.py +++ b/app/modules/dispatch/db.py @@ -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( diff --git a/app/modules/dispatch/router.py b/app/modules/dispatch/router.py index 5283dad..5a863a6 100644 --- a/app/modules/dispatch/router.py +++ b/app/modules/dispatch/router.py @@ -503,6 +503,26 @@ async def parcel_toggle( return JSONResponse({"ok": True, **result}) +@router.post("/batches/{batch_id:int}/bulk") +async def parcels_bulk( + request: Request, + batch_id: int, + field: str = Form(...), + user: dict[str, Any] = Depends(_require_user), +) -> JSONResponse: + """배치 내 모든 박스의 작업 상태 1개를 완료(TRUE)로 일괄 설정. JSON 반환.""" + st = _store(request) + if st is None: + raise HTTPException(status_code=503, detail="dispatch_db 미설정") + try: + count = st.bulk_set_status( + batch_id=batch_id, field=field, value=True, worker_name=user.get("email", "") + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + return JSONResponse({"ok": True, "field": field, "count": count}) + + @router.get("/api/batches/{batch_id:int}/parcels") async def api_parcels( request: Request, batch_id: int, _: dict[str, Any] = Depends(_require_user) diff --git a/app/modules/dispatch/templates/dispatch/detail.html b/app/modules/dispatch/templates/dispatch/detail.html index 2c0aee1..6baf7ed 100644 --- a/app/modules/dispatch/templates/dispatch/detail.html +++ b/app/modules/dispatch/templates/dispatch/detail.html @@ -4,7 +4,9 @@