From a1dd9bed4a691da1e41ec106136846dd0f78f709 Mon Sep 17 00:00:00 2001 From: king Date: Mon, 22 Jun 2026 15:29:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(dispatch):=20=EC=9D=BC=EA=B4=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=B2=84=ED=8A=BC(=EB=AA=A8=EB=91=90=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=EB=B6=80=EC=B0=A9/=EB=AA=A8=EB=91=90=20Kagay?= =?UTF-8?q?aku=20=EC=A0=84=EB=8B=AC)=20+=20=EA=B2=80=EC=83=89=EB=B0=94=20?= =?UTF-8?q?=EC=B6=95=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 출고 작업 툴바 우측에 일괄 버튼 2개. 배치 내 전 박스 상태를 완료로 일괄 설정 - db.bulk_set_status: 변경된 박스만 로그(set 기반 INSERT), 화이트리스트 검증 - 라우터 POST /batches/{id}/bulk - 검색 입력칸 폭 축소(flex 0 1 240px) Co-Authored-By: Claude Opus 4.8 --- app/modules/dispatch/db.py | 36 +++++++++++++++++++ app/modules/dispatch/router.py | 20 +++++++++++ .../dispatch/templates/dispatch/detail.html | 29 ++++++++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) 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 @@