from datetime import datetime from uuid import UUID from fastapi import APIRouter, Depends, Query from common.models.gmodel import PageParams, UserInfo from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse from services.quotation_service import QuotationService from .protocol import ( Req_AwardQuotation, Req_CreateQuotation, Req_RegenerateQuotation, Res_CreateQuotation, Res_DeleteQuotation, Res_LastSupplierType, Res_NotifySessions, Res_Quotation, Res_QuotationCards, Res_QuotationList, Res_QuotationResult, Res_QuotationSessions, Res_QuotationStatus, Res_SessionChat, Res_TargetBreakdown, ) # 라우터(컨트롤러). 인증(Depends(IsValidAccessToken))으로 UserInfo 를 받아 company_id 로 스코프(작성자 회사 기준). # 라우팅 주의: 정적/하위 경로(/list, /create, /{qt_id}/status ...)를 /{qt_id} 보다 먼저 선언해야 # /{qt_id} 가 /list 등을 가로채지 않는다. router = APIRouter(prefix="/v1/quotation", tags=["Quotation"], responses={404: {"description": "Not found"}}) # ----- 실제 엔드포인트 (DB) ----- @router.get(path="/list", response_model=Res_QuotationList, summary="견적 목록") async def list_quotations( service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken), search: str | None = Query(None, description="견적명/견적번호 검색"), status: str | None = Query(None, description="상태 필터(정확히 일치)"), type: str | None = Query(None, description="유형 필터(정확히 일치)"), start_from: datetime | None = Query(None, description="시작일시 이후(ISO)"), start_to: datetime | None = Query(None, description="시작일시 이전(ISO)"), mine: bool = Query(False, description="내 견적만 보기(작성자=로그인 유저)"), pg: PageParams = Depends(), ): owner = user_info.user_id if mine else None return RemoveNoneResponse(await service.list_quotations(user_info.company_id, owner, search, status, type, start_from, start_to, pg)) @router.post(path="/create", response_model=Res_CreateQuotation, summary="견적 생성") async def create_quotation( req: Req_CreateQuotation, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken) ): return RemoveNoneResponse(await service.create_quotation(user_info.user_id, req)) @router.post(path="/stop/{qt_id}", response_model=Res_Quotation, summary="견적 마감") async def stop_quotation(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.stop_quotation(str(qt_id), user_info.company_id, user_info.user_id, user_info.role)) @router.post(path="/award/{qt_id}", response_model=Res_Quotation, summary="개찰 견적 직접 낙찰") async def award_quotation( qt_id: UUID, req: Req_AwardQuotation, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken) ): # 권한(본인 견적만/OWNER 예외)은 서비스에서 판정하도록 호출자 user_id·role 을 넘긴다. return RemoveNoneResponse( await service.award_quotation(str(qt_id), user_info.company_id, user_info.user_id, user_info.role, req.winner_supplier_id) ) @router.post(path="/regenerate/{qt_id}", response_model=Res_CreateQuotation, summary="견적 재생성(다음 라운드)") async def regenerate_quotation( qt_id: UUID, req: Req_RegenerateQuotation, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken) ): return RemoveNoneResponse(await service.regenerate_quotation(str(qt_id), user_info.company_id, req.supplier_ids, user_info.user_id, user_info.role)) # ----- 견적 상세 (FK로 연결된 하위 데이터 / 일부는 모델 미존재로 스텁) ----- @router.get(path="/{qt_id}/status", response_model=Res_QuotationStatus, summary="견적 상태 조회") async def get_quotation_status(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.get_status(str(qt_id), user_info.company_id)) @router.get(path="/{qt_id}/sessions", response_model=Res_QuotationSessions, summary="참여현황") async def get_quotation_sessions(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.list_sessions(str(qt_id), user_info.company_id)) @router.post(path="/{qt_id}/notify", response_model=Res_NotifySessions, summary="협상 초청 메일 발송") async def notify_quotation(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.notify_sessions(str(qt_id), user_info.company_id, user_info.user_id, user_info.role)) @router.get(path="/session/{session_id}/chat", response_model=Res_SessionChat, summary="채팅 상세") async def get_session_chat(session_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.list_chats(str(session_id), user_info.company_id)) @router.get(path="/session/{session_id}/target-breakdown", response_model=Res_TargetBreakdown, summary="세션 목표가 산정내역") async def get_target_breakdown(session_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.get_target_breakdown(str(session_id), user_info.company_id)) @router.post(path="/session/{session_id}/notify", response_model=Res_NotifySessions, summary="세션 초청 메일 재발송") async def notify_session(session_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.notify_session(str(session_id), user_info.company_id, user_info.user_id, user_info.role)) @router.get(path="/{qt_id}/result", response_model=Res_QuotationResult, summary="낙찰 결과") async def get_quotation_result(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.get_result(str(qt_id), user_info.company_id)) @router.get(path="/{qt_id}/cards", response_model=Res_QuotationCards, summary="견적 사용 카드") async def get_quotation_cards(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.list_cards(str(qt_id), user_info.company_id)) @router.delete(path="/delete/{qt_id}", response_model=Res_DeleteQuotation, summary="견적 삭제") async def delete_quotation(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.delete_quotation(str(qt_id), user_info.company_id, user_info.user_id, user_info.role)) @router.get(path="/supplier/{supplier_id}/last-type", response_model=Res_LastSupplierType, summary="협력사 직전 견적 유형") async def get_supplier_last_type(supplier_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.get_last_supplier_type(str(supplier_id), user_info.company_id)) # ----- 단건 조회 (정적/하위 경로 뒤에 선언) ----- @router.get(path="/{qt_id}", response_model=Res_Quotation, summary="견적 조회") async def get_quotation(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.get_quotation(str(qt_id), user_info.company_id))