[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:
Mina Choi 2026-06-18 11:00:15 +09:00
parent ece24f4067
commit 4587be5844
3 changed files with 133 additions and 17 deletions

View File

@ -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

View File

@ -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,
}

View File

@ -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 로 인코딩되는 유저 식별 정보."""