[refactor] negodata/backend: 공용 PageParams(page/size/skip) + 상태 enum·모델 정비
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ece24f4067
commit
4587be5844
@ -25,29 +25,14 @@ class _DBTypeMixin:
|
||||
return DBType.MAIN.value
|
||||
|
||||
|
||||
# ERD 공통 컬럼(created/updated/deleted). negodata 도메인 테이블은 단일 MAIN DB 에서 soft-delete 를 쓴다.
|
||||
# ERD 공통 컬럼
|
||||
class MainTableMixin(_DBTypeMixin):
|
||||
created_at = Column(DateTime, nullable=False, server_default=_utc_now_sql())
|
||||
updated_at = Column(DateTime, nullable=False, server_default=_utc_now_sql(), onupdate=_utc_now_sql())
|
||||
deleted = Column(Boolean, nullable=False, server_default=text("false"), default=False)
|
||||
|
||||
|
||||
# 스캐폴드 예시 계정 (negosium 골격 유산)
|
||||
# negodata 인증은 ERD 의 users/companies 를 쓴다(아래). tbl_account 는 스캐폴드
|
||||
# 테스트 호환을 위해 남겨둔다 (negodata_db 의 postgres-init 이 생성).
|
||||
class tbl_account(_DBTypeMixin, MAIN_BASE):
|
||||
__tablename__ = "tbl_account"
|
||||
|
||||
uid = Column(Integer, primary_key=True, autoincrement=True)
|
||||
id = Column(String(45), nullable=False, unique=True)
|
||||
pw = Column(String(255), nullable=False, default="")
|
||||
nickname = Column(String(45), nullable=False, default="")
|
||||
is_blocked = Column(Boolean, nullable=False, default=False)
|
||||
last_login_at = Column(DateTime, nullable=False, server_default=_utc_now_sql())
|
||||
create_at = Column(DateTime, server_default=_utc_now_sql())
|
||||
|
||||
|
||||
# ERD 도메인 모델 (negosium.sql 기준, UUID PK)
|
||||
# ERD 도메인 모델
|
||||
class companies(MainTableMixin, MAIN_BASE):
|
||||
__tablename__ = "companies"
|
||||
|
||||
@ -122,6 +107,31 @@ class suppliers(MainTableMixin, MAIN_BASE):
|
||||
priority = Column(String(10), nullable=True) # True/False 가 아닌 string value 가능
|
||||
|
||||
|
||||
class nego_cards(MainTableMixin, MAIN_BASE):
|
||||
__tablename__ = "nego_cards"
|
||||
|
||||
nego_card_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 등록 유저(o2o 기본 카드는 NULL)
|
||||
name = Column(String(20), nullable=True) # 카드명
|
||||
number = Column(String(10), nullable=True) # 식별번호(카드코드)
|
||||
script = Column(String(255), nullable=True) # 협상 스크립트(평문 미리보기)
|
||||
edit_script = Column(JSONB, nullable=True) # 편집된 스크립트(Slate JSON)
|
||||
|
||||
|
||||
class wild_cards(MainTableMixin, MAIN_BASE):
|
||||
__tablename__ = "wild_cards"
|
||||
|
||||
wild_card_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 등록 유저(o2o 기본 카드는 NULL)
|
||||
name = Column(String(20), nullable=True) # 카드명
|
||||
number = Column(String(10), nullable=True) # 식별번호(카드코드)
|
||||
script = Column(String(255), nullable=True) # 협상 스크립트(평문 미리보기)
|
||||
edit_script = Column(JSONB, nullable=True) # 편집된 스크립트(Slate JSON)
|
||||
condition = Column(String(255), nullable=True) # 사용 조건(트리거)
|
||||
available = Column(Boolean, nullable=False, default=False) # 수동 협상 적용 여부(ACTIVE/INACTIVE 매핑)
|
||||
memo = Column(String(255), nullable=True) # 자유 메모
|
||||
|
||||
|
||||
class quotation_settings(MainTableMixin, MAIN_BASE):
|
||||
__tablename__ = "quotation_settings"
|
||||
|
||||
@ -159,3 +169,39 @@ class quotations(MainTableMixin, MAIN_BASE):
|
||||
preferred_sp_name = Column(String(20), nullable=True)
|
||||
equal_bid_yn = Column(Boolean, nullable=True)
|
||||
equal_bid_data = Column(JSONB, nullable=True)
|
||||
|
||||
|
||||
class sessions(MainTableMixin, MAIN_BASE):
|
||||
__tablename__ = "sessions"
|
||||
|
||||
session_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
quotation_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 소속 견적(quotations.qt_id)
|
||||
item_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 대상 상품(items.item_id)
|
||||
supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 대상 공급사(suppliers.supplier_id)
|
||||
|
||||
qt_number = Column(String(30), nullable=False) # 견적번호 스냅샷
|
||||
qt_round = Column(Integer, nullable=False) # 견적 라운드 스냅샷
|
||||
qt_type = Column(SmallInteger, nullable=False) # QuotationType 스냅샷
|
||||
target_price = Column(BigInteger, nullable=False) # 목표가(원)
|
||||
status = Column(SmallInteger, nullable=False) # SessionStatus 코드
|
||||
bid_price = Column(BigInteger, nullable=True) # 입찰가(원)
|
||||
bid_at = Column(DateTime, nullable=True) # 입찰 시각
|
||||
end_time = Column(DateTime, nullable=False) # 세션 종료 시각
|
||||
reject_reason = Column(String(255), nullable=True)
|
||||
reject_price = Column(BigInteger, nullable=True)
|
||||
reject_delivery_type = Column(SmallInteger, nullable=True) # DeliveryType 코드
|
||||
|
||||
|
||||
class chats(MainTableMixin, MAIN_BASE):
|
||||
__tablename__ = "chats"
|
||||
|
||||
chat_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
session_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 소속 세션(sessions.session_id)
|
||||
card_id = Column(UUID(as_uuid=True), nullable=True) # 사용된 카드(nego_cards.nego_card_id)
|
||||
|
||||
seq = Column(Integer, nullable=False, default=1) # 세션 내 메시지 순번(프로토콜 index)
|
||||
sender = Column(SmallInteger, nullable=False) # ChatSender 코드(1=bot, 2=partner)
|
||||
target_price = Column(BigInteger, nullable=False) # 제시 목표가(원)
|
||||
card_used_yn = Column(Boolean, nullable=True) # 카드 사용 여부
|
||||
indicator_value = Column(Numeric(8, 6), nullable=True)
|
||||
card_type = Column(SmallInteger, nullable=True) # 1=nego_card, 2=wild_card
|
||||
|
||||
@ -38,9 +38,11 @@ class ErrorType(Enum):
|
||||
|
||||
# 상품 관련 에러
|
||||
ITEM_NOT_FOUND = 1300
|
||||
ITEM_CODE_DUPLICATE = auto()
|
||||
|
||||
# 협력사 관련 에러
|
||||
SUPPLIER_NOT_FOUND = 1400
|
||||
SUPPLIER_CODE_DUPLICATE = auto()
|
||||
|
||||
# 견적 관련 에러
|
||||
QUOTATION_NOT_FOUND = 1500
|
||||
@ -48,6 +50,9 @@ class ErrorType(Enum):
|
||||
# 견적 설정 관련 에러
|
||||
QUOTATION_SETTING_NOT_FOUND = 1600
|
||||
|
||||
# 협상카드 관련 에러
|
||||
CARD_NOT_FOUND = 1700
|
||||
|
||||
|
||||
# ErrorType 의 HTTP_* 값과 status_code 를 맞춰 router 단에서 raise 한다.
|
||||
EXCEPTION_INVALID_CLIENT_REQUEST = HTTPException(status_code=ErrorType.HTTP_INVALID_CLIENT_REQUEST.value, detail=ErrorType.HTTP_INVALID_CLIENT_REQUEST.name)
|
||||
@ -102,6 +107,30 @@ class QuotationType(Enum):
|
||||
REQUOTE = 2
|
||||
|
||||
|
||||
class QuotationStatus(Enum):
|
||||
"""quotations.status 코드값(SMALLINT). 프론트 견적상태 뱃지와 매핑된다."""
|
||||
|
||||
CREATED = 1 # 견적생성
|
||||
ACTIVE = 2 # 견적진행중
|
||||
CLOSED = 3 # 견적마감
|
||||
ON_HOLD = 4 # 협상보류
|
||||
|
||||
|
||||
class SessionStatus(Enum):
|
||||
"""negotiation.sessions.status 코드값. 협력사별 협상 세션 진행 상태."""
|
||||
|
||||
NEGOTIATING = 1 # 협상중
|
||||
COMPLETED = 2 # 협상종료
|
||||
REJECTED = 3 # 협상거부
|
||||
|
||||
|
||||
class ChatSender(Enum):
|
||||
"""negotiation.chats.sender 코드값. 채팅 발신 주체."""
|
||||
|
||||
BOT = 1 # 구매대행 봇
|
||||
PARTNER = 2 # 협력사
|
||||
|
||||
|
||||
class DeliveryType(Enum):
|
||||
"""items.delivery_type 코드값. 협상 채팅의 배송형태 선택지와 동일 집합."""
|
||||
|
||||
@ -110,6 +139,13 @@ class DeliveryType(Enum):
|
||||
PICKUP = 3 # 픽업배송
|
||||
|
||||
|
||||
class CardStatus(Enum):
|
||||
"""nego_cards.status 코드값. 와일드카드의 협상 적용 여부(수동 승인). 일반 협상카드는 상시 ACTIVE."""
|
||||
|
||||
ACTIVE = 1
|
||||
INACTIVE = 2
|
||||
|
||||
|
||||
# 도메인 enum 한글 라벨. 프론트 드롭다운 표시는 이 라벨을 쓴다(값=코드).
|
||||
ENUM_LABELS = {
|
||||
UserStatus.ACTIVE: "활성",
|
||||
@ -120,9 +156,20 @@ ENUM_LABELS = {
|
||||
CompanyStatus.INACTIVE: "비활성",
|
||||
QuotationType.RENEGO: "재협상",
|
||||
QuotationType.REQUOTE: "재견적",
|
||||
QuotationStatus.CREATED: "견적생성",
|
||||
QuotationStatus.ACTIVE: "견적진행중",
|
||||
QuotationStatus.CLOSED: "견적마감",
|
||||
QuotationStatus.ON_HOLD: "협상보류",
|
||||
SessionStatus.NEGOTIATING: "협상중",
|
||||
SessionStatus.COMPLETED: "협상종료",
|
||||
SessionStatus.REJECTED: "협상거부",
|
||||
ChatSender.BOT: "봇",
|
||||
ChatSender.PARTNER: "협력사",
|
||||
DeliveryType.PARTNER: "협력사배송",
|
||||
DeliveryType.COURIER: "지정택배배송",
|
||||
DeliveryType.PICKUP: "픽업배송",
|
||||
CardStatus.ACTIVE: "적용",
|
||||
CardStatus.INACTIVE: "대기",
|
||||
}
|
||||
|
||||
# 프론트로 내려주는 도메인 코드 enum 모음. 새 코드 enum 추가 시 여기에 등록한다.
|
||||
@ -131,5 +178,9 @@ DOMAIN_ENUMS = {
|
||||
"user_role": UserRole,
|
||||
"company_status": CompanyStatus,
|
||||
"quotation_type": QuotationType,
|
||||
"quotation_status": QuotationStatus,
|
||||
"session_status": SessionStatus,
|
||||
"chat_sender": ChatSender,
|
||||
"delivery_type": DeliveryType,
|
||||
"card_status": CardStatus,
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from common.enums import ErrorType
|
||||
@ -45,6 +46,24 @@ class Res_WebPacketProtocol(WebPacketProtocol):
|
||||
msg: Optional[str] = None
|
||||
|
||||
|
||||
class Res_PageProtocol(Res_WebPacketProtocol):
|
||||
# 목록 응답 공용 페이지 메타.
|
||||
total: int = 0
|
||||
page: int = 0
|
||||
size: int = 0
|
||||
|
||||
|
||||
class PageParams:
|
||||
# 목록 엔드포인트 공용 쿼리 파라미터. 라우터에서 Depends() 로 주입한다.
|
||||
def __init__(self, page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100)):
|
||||
self.page = page
|
||||
self.size = size
|
||||
|
||||
@property
|
||||
def skip(self) -> int:
|
||||
return (self.page - 1) * self.size
|
||||
|
||||
|
||||
class UserInfo(StructModel):
|
||||
"""JWT subject 로 인코딩되는 유저 식별 정보."""
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user