- 견적유형 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>
226 lines
21 KiB
Python
226 lines
21 KiB
Python
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")) # 소프트 삭제 여부
|