From 5520e3c2b651ad2066799272397307c0093d8693 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Thu, 30 Jul 2026 13:20:42 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20negodata:=20=EA=B2=AC=EC=A0=81=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C(=EA=B0=9C=EB=B0=9C=EC=9E=90=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9)=C2=B7=EC=97=B0=EA=B2=B0=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=BA=90=EC=8A=A4=EC=BC=80=EC=9D=B4=EB=93=9C=20sof?= =?UTF-8?q?t-delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 견적관리 체크박스 선택 + 툴바 '선택 삭제'(상품·협력사 패턴), 상세 시트 단건 '삭제' - 백엔드 soft_delete 를 연결 협상 세션·대화까지 한 트랜잭션 캐스케이드 - 체크박스·선택삭제·상세삭제 모두 개발자(role 3)에게만 노출 --- negodata/backend/crud/quotation_crud.py | 17 +++++- .../components/QuotationDetailSheet/index.tsx | 18 +++++- .../quotations/components/QuotationTable.tsx | 6 +- .../quotations/hooks/useQuotations.ts | 14 +++++ negodata/front/src/pages/quotation.tsx | 60 ++++++++++++++++++- 5 files changed, 110 insertions(+), 5 deletions(-) diff --git a/negodata/backend/crud/quotation_crud.py b/negodata/backend/crud/quotation_crud.py index a851edb..0606bd5 100644 --- a/negodata/backend/crud/quotation_crud.py +++ b/negodata/backend/crud/quotation_crud.py @@ -77,6 +77,7 @@ class IQuotationCRUD(ABC): @abstractmethod async def soft_delete(self, cdb: AsyncSession, qt_id) -> ErrorType: + """견적과 연결된 하위 데이터(세션·대화)를 함께 소프트 삭제.""" pass @abstractmethod @@ -547,9 +548,21 @@ class QuotationCRUD(IQuotationCRUD): return ErrorType.DB_RUN_FAILED async def soft_delete(self, cdb: AsyncSession, qt_id) -> ErrorType: + """견적 + 연결된 하위 데이터(협상 세션·대화)를 한 트랜잭션으로 소프트 삭제한다. + 대화(chats)→세션(sessions)→견적(quotations) 순서로 deleted=True. 한 스텝이라도 실패하면 execute_lambda_run 이 롤백한다.""" try: - query = update(quotations).where(quotations.qt_id == qt_id).values(deleted=True, updated_at=GTime.UTC()) - return await DB_SESSION_MNG.add(cdb, query) + now = GTime.UTC() + # 이 견적에 매달린 세션들 — chats 삭제 범위를 잡는 서브쿼리(quotation_id 기준, deleted 무관). + session_ids = select(sessions.session_id).where(sessions.quotation_id == qt_id) + for query in ( + update(chats).where(chats.session_id.in_(session_ids)).values(deleted=True, updated_at=now), + update(sessions).where(sessions.quotation_id == qt_id).values(deleted=True, updated_at=now), + update(quotations).where(quotations.qt_id == qt_id).values(deleted=True, updated_at=now), + ): + err_type = await DB_SESSION_MNG.add(cdb, query) + if err_type != ErrorType.SUCCESS: + return err_type + return ErrorType.SUCCESS except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED diff --git a/negodata/front/src/features/quotations/components/QuotationDetailSheet/index.tsx b/negodata/front/src/features/quotations/components/QuotationDetailSheet/index.tsx index dc060dd..c002578 100644 --- a/negodata/front/src/features/quotations/components/QuotationDetailSheet/index.tsx +++ b/negodata/front/src/features/quotations/components/QuotationDetailSheet/index.tsx @@ -1,8 +1,9 @@ import { useState } from 'react'; import { useNavigate } from 'react-router'; import { createPortal } from 'react-dom'; -import { CheckCircle2, ChevronLeft, X, UserCheck, MessageSquare, Layers, RefreshCw } from 'lucide-react'; +import { CheckCircle2, ChevronLeft, X, UserCheck, MessageSquare, Layers, RefreshCw, Trash2 } from 'lucide-react'; import { Typography } from '@/components/ui/typography'; +import { Button } from '@/components/ui/button'; import { useGetQuotationSessions, useGetSessionChat, @@ -47,6 +48,8 @@ type QuotationDetailSheetProps = { onNotify: (qtId: string) => Promise; /** 협상 초청 메일 — 세션(공급사) 단위 재발송. */ onNotifySession: (sessionId: string, qtId: string) => Promise; + /** 넘기면 헤더에 삭제 버튼 노출(개발자 전용 — 페이지에서 권한 판정 후 주입). 삭제 성공 시 시트를 닫는다. */ + onDelete?: (id: string, name: string) => void; onClose: () => void; }; @@ -58,6 +61,7 @@ export function QuotationDetailSheet({ onRegenerate, onNotify, onNotifySession, + onDelete, onClose, }: QuotationDetailSheetProps) { const [activeTab, setActiveTab] = useState('status'); @@ -215,6 +219,18 @@ export function QuotationDetailSheet({ ); })()} + {/* 견적 삭제 — 개발자만(onDelete 주입 시). 연결된 협상 세션·대화까지 함께 소프트 삭제 후 시트 닫힘. */} + {onDelete && ( + + )} + )} +