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 && ( + + )} + )} +