- 마감사유 quotations.close_reason(SMALLINT, 8종) 신설 — 낙찰/재협상3종/유찰4종 구분, 재생성 한도 카운팅. quotation_settings 에 mid_action·over_action·regen_limit(3구간 가격정책) 추가. models.py·enums.py·01-schema.sql·crud·service·테스트 정합. - 프론트 노출: CloseReason/PriceGateAction 라벨·배지, 목록 마감결과 컬럼, 견적세팅 모달 3구간. orval 생성모델 갱신. - 견적상세 결과밴드: 목표가→낙찰가→절감 stat 트리오를 단가조정 흐름 스펙트럼(목표가·최저·최고 투찰가)으로 교체. lowestBid/highestBid 는 bid_price 파생(DB 컬럼 아님), 최초가 컬럼 없어 시작앵커=최고 투찰가로 대체. 라벨 방향 값순 번갈아 배치로 겹침 방지. - 라벨 정정: "단독 낙찰"→"낙찰"(낙찰은 단수), "목표 대비"→"목표가 대비". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
129 lines
5.2 KiB
Python
129 lines
5.2 KiB
Python
import uuid
|
|
|
|
from fastapi import Depends
|
|
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.database.model.models import quotation_settings
|
|
from common.enums import DBWRType, ErrorType
|
|
from crud.quotation_setting_crud import IQuotationSettingCRUD, QuotationSettingCRUD
|
|
from router.v1.quotation_setting.protocol import (
|
|
QuotationSettingData,
|
|
Req_CreateQuotationSetting,
|
|
Req_UpdateQuotationSetting,
|
|
Res_DeleteQuotationSetting,
|
|
Res_QuotationSetting,
|
|
Res_QuotationSettingList,
|
|
)
|
|
|
|
|
|
class QuotationSettingService:
|
|
"""견적 설정 비즈니스 로직. user_id 로 소유권을 확인한다(유저별 설정)."""
|
|
|
|
def __init__(self, qs_crud: IQuotationSettingCRUD = Depends(QuotationSettingCRUD)):
|
|
self.qs_crud = qs_crud
|
|
|
|
async def _fetch_owned(self, user_uuid: uuid.UUID, qt_setting_id: uuid.UUID):
|
|
"""설정 조회 + 소유권 확인. (ErrorType, setting|None) 반환."""
|
|
err_type, setting = await DB_SESSION_MNG.execute_lambda(
|
|
quotation_settings.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.qs_crud.get_by_id(s, qt_setting_id),
|
|
)
|
|
if err_type != ErrorType.SUCCESS or setting is None:
|
|
return ErrorType.QUOTATION_SETTING_NOT_FOUND, None
|
|
if setting.user_id != user_uuid:
|
|
return ErrorType.QUOTATION_SETTING_NOT_FOUND, None
|
|
return ErrorType.SUCCESS, setting
|
|
|
|
async def list_settings(self, user_id: str) -> Res_QuotationSettingList:
|
|
res = Res_QuotationSettingList()
|
|
user_uuid = uuid.UUID(user_id)
|
|
|
|
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
|
|
quotation_settings.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.qs_crud.list_by_user(s, user_uuid),
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
res.settings = [QuotationSettingData.model_validate(r) for r in rows]
|
|
res.total = total
|
|
return res
|
|
|
|
async def get_setting(self, user_id: str, qt_setting_id: str) -> Res_QuotationSetting:
|
|
res = Res_QuotationSetting()
|
|
err_type, setting = await self._fetch_owned(uuid.UUID(user_id), uuid.UUID(qt_setting_id))
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
res.setting = QuotationSettingData.model_validate(setting)
|
|
return res
|
|
|
|
async def create_setting(self, user_id: str, req: Req_CreateQuotationSetting) -> Res_QuotationSetting:
|
|
res = Res_QuotationSetting()
|
|
setting = quotation_settings(
|
|
user_id=uuid.UUID(user_id),
|
|
target_margin_rate=req.target_margin_rate,
|
|
anchoring_value=req.anchoring_value,
|
|
card_count=req.card_count,
|
|
mid_action=req.mid_action.value,
|
|
over_action=req.over_action.value,
|
|
regen_limit=req.regen_limit,
|
|
)
|
|
err_type = await DB_SESSION_MNG.execute_lambda_run(
|
|
[quotation_settings.DBType()],
|
|
[lambda s: self.qs_crud.add_setting(s, setting)],
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
# 서버 기본값(created_at/updated_at)은 insert 후 객체에 실리지 않으므로 재조회한다.
|
|
return await self.get_setting(user_id, str(setting.qt_setting_id))
|
|
|
|
async def update_setting(self, user_id: str, qt_setting_id: str, req: Req_UpdateQuotationSetting) -> Res_QuotationSetting:
|
|
res = Res_QuotationSetting()
|
|
user_uuid = uuid.UUID(user_id)
|
|
setting_uuid = uuid.UUID(qt_setting_id)
|
|
data = req.model_dump(exclude_unset=True)
|
|
# PriceGateAction enum → SMALLINT 코드값으로 변환(모델 컬럼이 int)
|
|
for k in ("mid_action", "over_action"):
|
|
v = data.get(k)
|
|
if v is not None and hasattr(v, "value"):
|
|
data[k] = v.value
|
|
|
|
# 소유권 확인
|
|
err_type, _ = await self._fetch_owned(user_uuid, setting_uuid)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
err_type = await DB_SESSION_MNG.execute_lambda_run(
|
|
[quotation_settings.DBType()],
|
|
[lambda s: self.qs_crud.update_setting(s, setting_uuid, data)],
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
# 갱신 후 재조회
|
|
return await self.get_setting(user_id, qt_setting_id)
|
|
|
|
async def delete_setting(self, user_id: str, qt_setting_id: str) -> Res_DeleteQuotationSetting:
|
|
res = Res_DeleteQuotationSetting()
|
|
user_uuid = uuid.UUID(user_id)
|
|
setting_uuid = uuid.UUID(qt_setting_id)
|
|
|
|
err_type, _ = await self._fetch_owned(user_uuid, setting_uuid)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
err_type = await DB_SESSION_MNG.execute_lambda_run(
|
|
[quotation_settings.DBType()],
|
|
[lambda s: self.qs_crud.soft_delete(s, setting_uuid)],
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|