- 생성(_calc_target_price)과 표시(get_target_breakdown)가 _candidates 헬퍼를 공유 → 프론트 재계산 제거, 저장된 목표가와 항상 일치 - TargetPriceModal: useGetTargetBreakdown 으로 후보·채택·앵커링가 '표시만' - QuotationType.is_new() 한 곳에서 신규유형 판단(생성·산정 공통) - enum 라벨 단일출처(lib/enumLabels.ts): 협력사유형/카드 사용구분/유저상태 members·CardFormSheet 는 거기서 re-export Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
122 lines
6.5 KiB
Python
122 lines
6.5 KiB
Python
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_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 를 받는다.
|
|
# quotations 테이블에 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)"),
|
|
pg: PageParams = Depends(),
|
|
):
|
|
return RemoveNoneResponse(await service.list_quotations(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)))
|
|
|
|
|
|
@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), req.supplier_ids))
|
|
|
|
|
|
# ----- 견적 상세 (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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
@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)))
|
|
|
|
|
|
# ----- 단건 조회 (정적/하위 경로 뒤에 선언) -----
|
|
@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)))
|