o2o-negosium-original/backend/common/database/model/models.py
Mina Choi 103c80046e [feat] negodata: 신규 견적유형 + 목표가·앵커링 산정 + 협력사유형 + 협상카드 용도
- 견적유형 4종: QuotationType 에 신규협상(3)·신규견적(4) 추가(1·2 재 고정)
- 목표가 산정: items(매입가·판매가·인터넷최저가) + quotations.md_price +
  quotation_settings.internet_average_fee 로 _calc_target_price(KTC 이식, 신규/재 분리),
  세션 target_price·target_anchoring_price 저장
- 협력사유형: quotations.supplier_type(SupplierType) + 직전유형 조회 API(/supplier/{id}/last-type)
- 협상카드 용도: nego_cards/wild_cards.usage_type(CardUsageType: 공통/신규견적전용/재견적전용)
- 스키마(01-schema.sql)·ORM·protocol·service·crud·프론트(생성타입/폼) 일괄, /backend enums·models 미러

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:09:14 +09:00

226 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, Integer, String, Boolean, DateTime, SmallInteger, BigInteger, Numeric
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.sql import text
from common.enums import DBType
# 모든 ORM 모델의 베이스. insert 시 isinstance 체크에도 사용된다.
MAIN_BASE = declarative_base()
class supplier_users(MAIN_BASE):
# 이 프로젝트의 기본 유저. 실제 테이블은 negosium_db 의 supplier 스키마(supplier_users).
@staticmethod
def DBType():
return DBType.USER.value
__tablename__ = "supplier_users"
__table_args__ = {"schema": "supplier"}
# gen_random_uuid() 는 pgcrypto 확장 기준. 코드값(status/role/type)은 SMALLINT 정수 코드(앱 enum 매핑).
su_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 유저 식별자(PK)
supplier_id = Column(UUID(as_uuid=True), nullable=False) # 소속 공급사(partner.suppliers.supplier_id)
id = Column(String(20), nullable=False) # 로그인 ID
password = Column(String(255), nullable=False) # 해시된 비밀번호이어야 함
name = Column(String(50), nullable=True) # 이름
email = Column(String(255), nullable=True) # 이메일
contact_number = Column(String(20), nullable=True) # 연락처
last_accessed_at = Column(DateTime(timezone=True), nullable=False) # 마지막 접속 시각
status = Column(SmallInteger, nullable=False, server_default=text("1")) # 상태: 1=active, 2=inactive
role = Column(SmallInteger, nullable=False, server_default=text("1")) # 권한: 1=user, 2=manager
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
class suppliers(MAIN_BASE):
# partner.suppliers (공급사 회사). 공급사명(name) 조회용. partner 도메인이라 DBType 은 PARTNER.
@staticmethod
def DBType():
return DBType.PARTNER.value
__tablename__ = "suppliers"
__table_args__ = {"schema": "partner"}
supplier_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 공급사 식별자(PK)
company_id = Column(UUID(as_uuid=True), nullable=False) # 소속 회사(company.companies.company_id)
user_id = Column(UUID(as_uuid=True), nullable=False) # 등록 유저(company.users.user_id)
name = Column(String(100), nullable=False) # 공급사명
code = Column(String(20), nullable=True) # 공급사 코드
manager_name = Column(String(50), nullable=True) # 담당자명
manager_email = Column(String(255), nullable=True) # 담당자 이메일
manager_contact_number = Column(String(20), nullable=True) # 담당자 연락처
priority = Column(String(10), nullable=True) # 우선순위 (고객사별 문자열 값 가능)
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
class items(MAIN_BASE):
# partner.items (상품).
@staticmethod
def DBType():
return DBType.PARTNER.value
__tablename__ = "items"
__table_args__ = {"schema": "partner"}
item_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 상품 식별자(PK)
company_id = Column(UUID(as_uuid=True), nullable=False) # 소속 회사(company.companies.company_id)
user_id = Column(UUID(as_uuid=True), nullable=False) # 등록 유저(company.users.user_id)
name = Column(String(100), nullable=False) # 상품명
code = Column(String(30), nullable=True) # 상품 코드
price = Column(BigInteger, nullable=True) # 가격(원)
category = Column(String(255), nullable=True) # 카테고리
image_url = Column(String(255), nullable=True) # 이미지 URL
model_name = Column(String(100), nullable=True) # 모델명
spec = Column(String(255), nullable=True) # 규격
moq = Column(String(50), nullable=True) # 최소 주문 수량
lead_time = Column(SmallInteger, nullable=True) # 배송 리드타임
manufacturer = Column(String(50), nullable=True) # 제조사
made_in = Column(String(100), nullable=True) # 원산지
quantity_unit = Column(SmallInteger, nullable=True) # 취급 단위 (코드, 앱 enum 매핑)
delivery_type = Column(SmallInteger, nullable=True) # 배송 유형 (코드, 앱 enum 매핑)
vat_yn = Column(Boolean, nullable=True) # 부가세 포함 여부
delivery_fee_yn = Column(Boolean, nullable=True) # 배송비 포함 여부
internet_lowest_price_yn = Column(Boolean, nullable=False, server_default=text("false")) # 최저가 솔루션 보조 컬럼
internet_lowest_price = Column(BigInteger, nullable=True) # 인터넷 최저가 실값(원). 목표가 산정용(KTC: ×(1−수수료))
purchase_price = Column(BigInteger, nullable=True) # 매입가(원). 목표가 후보(그대로). KTC items.purchase_price
selling_price = Column(BigInteger, nullable=True) # 판매가(원). 목표가 후보(×(1−마진율)), 유통형만. KTC items.selling_price
category_type = Column(Integer, nullable=False, server_default=text("1")) # 카테고리 조회용 자동 증가 숫자
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
class sessions(MAIN_BASE):
# negotiation.sessions (협상 세션).
@staticmethod
def DBType():
return DBType.NEGOTIATION.value
__tablename__ = "sessions"
__table_args__ = {"schema": "negotiation"}
session_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 협상 세션 식별자(PK)
quotation_id = Column(UUID(as_uuid=True), nullable=False) # 소속 견적(quotation.quotations.qt_id)
item_id = Column(UUID(as_uuid=True), nullable=False) # 대상 상품(partner.items.item_id)
supplier_id = Column(UUID(as_uuid=True), nullable=False) # 대상 공급사(partner.suppliers.supplier_id)
qt_number = Column(String(30), nullable=False) # 견적번호(스냅샷)
qt_round = Column(Integer, nullable=False) # 견적 라운드(스냅샷)
qt_type = Column(SmallInteger, nullable=False) # 견적 유형: 1=재협상, 2=재견적 (QtType)
target_price = Column(BigInteger, nullable=False) # 목표가(원)
target_anchoring_price = Column(BigInteger, nullable=True) # 앵커링가(원)=floor(목표가×(1−anchoring_value)). KTC sessions.target_anchoring_price
status = Column(SmallInteger, nullable=False) # 진행 상태 (SessionStatus 코드)
bid_price = Column(BigInteger, nullable=True) # 입찰가(원)
bid_at = Column(DateTime(timezone=True), nullable=True) # 입찰 시각
end_time = Column(DateTime(timezone=True), nullable=False) # 세션 종료(마감) 시각
reject_reason = Column(String(255), nullable=True) # 거절 사유
reject_price = Column(BigInteger, nullable=True) # 거절 시 제시가(원)
reject_delivery_type = Column(SmallInteger, nullable=True) # 거절 시 배송 유형 (코드)
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
class quotations(MAIN_BASE):
# quotation.quotations (견적).
@staticmethod
def DBType():
return DBType.QUOTATION.value
__tablename__ = "quotations"
__table_args__ = {"schema": "quotation"}
qt_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 견적 식별자(PK)
user_id = Column(UUID(as_uuid=True), nullable=False) # 생성 유저(company.users.user_id)
qt_setting_id = Column(UUID(as_uuid=True), nullable=False) # 견적 설정(quotation.quotation_settings.qt_setting_id)
version_id = Column(UUID(as_uuid=True), nullable=False) # 버전(card.versions.version_id)
name = Column(String(50), nullable=False) # 견적명
number = Column(String(30), nullable=False) # 견적번호
type = Column(SmallInteger, nullable=False) # 견적 유형: 1=재협상, 2=재견적 (QtType)
round = Column(Integer, nullable=False, server_default=text("1")) # 재견적 회차
status = Column(SmallInteger, nullable=False) # 진행 상태 (QuotationStatus 코드)
start_time = Column(DateTime(timezone=True), nullable=False) # 견적 시작 시각
end_time = Column(DateTime(timezone=True), nullable=False) # 견적 종료(마감) 시각
manager_name = Column(String(50), nullable=True) # 담당자명
manager_email = Column(String(255), nullable=True) # 담당자 이메일
manager_contact_number = Column(String(20), nullable=True) # 담당자 연락처
memo = Column(String(100), nullable=True) # 메모
md_price = Column(BigInteger, nullable=True) # MD 제시가(원). 목표가 산정 최우선값
supplier_type = Column(SmallInteger, nullable=True) # 협력사 유형(SupplierType). 재견적 1:1 → 견적에 기록
iteration = Column(Integer, nullable=False, server_default=text("0")) # 반복 횟수
preferred_sp_yn = Column(Boolean, nullable=True) # 선호 공급사 지정 여부
preferred_sp_id = Column(UUID(as_uuid=True), nullable=True) # 선호 공급사(partner.suppliers.supplier_id)
preferred_sp_name = Column(String(20), nullable=True) # 선호 공급사명(스냅샷)
equal_bid_yn = Column(Boolean, nullable=True) # 동일가 입찰 발생 여부
equal_bid_data = Column(JSONB, nullable=True) # 동일가 입찰 상세(JSON)
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
class quotation_settings(MAIN_BASE):
# quotation.quotation_settings (견적 설정). 앵커링값(anchoring_value) 조회용 — agent RL state 입력.
@staticmethod
def DBType():
return DBType.QUOTATION.value
__tablename__ = "quotation_settings"
__table_args__ = {"schema": "quotation"}
qt_setting_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 견적 설정 식별자(PK)
user_id = Column(UUID(as_uuid=True), nullable=False) # 생성 유저(company.users.user_id)
target_margin_rate = Column(Numeric(8, 6), nullable=False) # 목표 마진율
anchoring_value = Column(Numeric(8, 6), nullable=False, server_default=text("0.01")) # 앵커링 값(비율) — anchor=round(target*(1-value))
internet_average_fee = Column(Numeric(8, 6), nullable=False, server_default=text("0.078")) # 인터넷 평균 수수료율. 목표가=인터넷최저가×(1−값). KTC QuotationPrice.internet_average_fee(Float)
card_count = Column(Integer, nullable=False, server_default=text("3")) # 협상 내 협상카드 사용 횟수
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
class chats(MAIN_BASE):
# negotiation.chats (협상 채팅 메시지 로그). session 1 : N chats. (session_id, seq) 유니크.
@staticmethod
def DBType():
return DBType.NEGOTIATION.value
__tablename__ = "chats"
__table_args__ = {"schema": "negotiation"}
chat_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 채팅 식별자(PK)
session_id = Column(UUID(as_uuid=True), nullable=False) # 소속 세션(negotiation.sessions.session_id)
card_id = Column(UUID(as_uuid=True), nullable=True) # 사용된 카드(card.nego_cards/wild_cards)
seq = Column(Integer, nullable=False, server_default=text("1")) # 세션 내 메시지 순번
sender = Column(SmallInteger, nullable=False) # 발신자 (ChatSender: 1=BOT, 2=USER)
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
meta = Column(JSONB, nullable=True) # 말풍선 표현 데이터(script/step/client_step/input_mode/input_options/chat_end)
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
class supplier_user_tokens(MAIN_BASE):
# 유저 인증 토큰. supplier_users 1 : N tokens.
@staticmethod
def DBType():
return DBType.USER.value
__tablename__ = "supplier_user_tokens"
__table_args__ = {"schema": "supplier"}
sut_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 토큰 식별자(PK)
su_id = Column(UUID(as_uuid=True), nullable=False) # 소유 유저(supplier.supplier_users.su_id)
type = Column(SmallInteger, nullable=False) # 토큰 종류 (코드, 앱 enum 매핑)
token = Column(JSONB, nullable=False) # 토큰 본문(JSON)
issued_at = Column(DateTime(timezone=True), nullable=False) # 발급 시각
expired_at = Column(DateTime(timezone=True), nullable=False) # 만료 시각
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부