import uuid from sqlalchemy.orm import declarative_base from sqlalchemy import Column, Integer, SmallInteger, BigInteger, Numeric, Float, String, Boolean, DateTime from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.sql import text from common.enums import DBType, UserStatus, UserRole, CompanyStatus # 모든 ORM 모델의 베이스. insert 시 isinstance 체크에도 사용된다. MAIN_BASE = declarative_base() # 공통 mixin # DB 계약(_DBTypeMixin)과 ERD 공통 컬럼(MainTableMixin)을 분리해 둔다. def _utc_now_sql(): return text("(now() AT TIME ZONE 'utc')") class _DBTypeMixin: """모델이 자신이 속한 논리 DB 를 알려준다 (람다 실행 시 DBType 으로 세션 선택).""" @staticmethod def DBType(): return DBType.MAIN.value # 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) # ERD 도메인 모델 class companies(MainTableMixin, MAIN_BASE): __tablename__ = "companies" __table_args__ = {"schema": "company"} company_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String(100), nullable=False) business_number = Column(String(30), nullable=True) code = Column(Integer, nullable=True) # 내부 인덱스용 representative_name = Column(String(50), nullable=True) email = Column(String(255), nullable=True) contact_number = Column(String(20), nullable=True) website_url = Column(String(255), nullable=True) industry = Column(SmallInteger, nullable=True) # 업종 코드 (스키마 SMALLINT) status = Column(SmallInteger, nullable=False, default=CompanyStatus.ACTIVE.value) # CompanyStatus class users(MainTableMixin, MAIN_BASE): __tablename__ = "users" __table_args__ = {"schema": "company"} user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) company_id = Column(UUID(as_uuid=True), nullable=False, index=True) id = Column(String(20), nullable=False, unique=True, index=True) # 로그인 아이디 password = Column(String(255), nullable=False) # bcrypt 해시 (ERD VARCHAR(30)→255 확장) name = Column(String(50), nullable=True) email = Column(String(255), nullable=True) contact_number = Column(String(20), nullable=True) last_accessed_at = Column(DateTime, nullable=False, server_default=_utc_now_sql()) status = Column(SmallInteger, nullable=False, default=UserStatus.ACTIVE.value) role = Column(SmallInteger, nullable=False, default=UserRole.USER.value) class items(MainTableMixin, MAIN_BASE): __tablename__ = "items" __table_args__ = {"schema": "partner"} item_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) company_id = Column(UUID(as_uuid=True), nullable=False, index=True) user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 등록한 유저 name = Column(String(100), nullable=False) code = Column(String(30), nullable=True) category = Column(String(255), nullable=True) category_type = Column(Integer, nullable=False, default=1) # 카테고리 탐색용 자동 증가 숫자 image_url = Column(String(255), nullable=True) model_name = Column(String(100), nullable=True) spec = Column(String(255), nullable=True) manufacturer = Column(String(50), nullable=True) made_in = Column(String(100), nullable=True) price = Column(BigInteger, nullable=True) # 금액(원), 스키마 BIGINT internet_lowest_price_yn = Column(Boolean, nullable=False, default=False) # 최저가 솔루션 원자성 보존용 moq = Column(String(50), nullable=True) # 최소 주문 수량 lead_time = Column(SmallInteger, nullable=True) # 주문 후 배송 도착까지 시간 quantity_unit = Column(String(50), nullable=True) # 단위 라벨(자유입력): EA/BOX/SET/ROLL ... delivery_type = Column(SmallInteger, nullable=True) # 배송 유형 코드 vat_yn = Column(Boolean, nullable=True) delivery_fee_yn = Column(Boolean, nullable=True) class suppliers(MainTableMixin, MAIN_BASE): __tablename__ = "suppliers" __table_args__ = {"schema": "partner"} supplier_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) company_id = Column(UUID(as_uuid=True), nullable=False, index=True) user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 등록한 유저 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) # ERD 오타(manger) 교정 priority = Column(String(10), nullable=True) # True/False 가 아닌 string value 가능 class nego_cards(MainTableMixin, MAIN_BASE): __tablename__ = "nego_cards" __table_args__ = {"schema": "card"} 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" __table_args__ = {"schema": "card"} 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 versions(MainTableMixin, MAIN_BASE): __tablename__ = "versions" __table_args__ = {"schema": "card"} version_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 버전 생성 유저 code = Column(Integer, nullable=False, default=0) # 빠른 조회용 name = Column(String(10), nullable=False) # 버전명 class version_nego_cards(MainTableMixin, MAIN_BASE): __tablename__ = "version_nego_cards" __table_args__ = {"schema": "card"} vnc_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) version_id = Column(UUID(as_uuid=True), nullable=False, index=True) # versions.version_id nego_card_id = Column(UUID(as_uuid=True), nullable=False, index=True) # nego_cards.nego_card_id class version_wild_cards(MainTableMixin, MAIN_BASE): __tablename__ = "version_wild_cards" __table_args__ = {"schema": "card"} vwc_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) version_id = Column(UUID(as_uuid=True), nullable=False, index=True) # versions.version_id wild_card_id = Column(UUID(as_uuid=True), nullable=False, index=True) # wild_cards.wild_card_id class quotation_settings(MainTableMixin, MAIN_BASE): __tablename__ = "quotation_settings" __table_args__ = {"schema": "quotation"} qt_setting_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 설정 소유 유저 target_margin_rate = Column(Numeric(8, 6), nullable=False) anchoring_value = Column(Numeric(8, 6), nullable=False, default=0.01) card_count = Column(Integer, nullable=False, default=3) # 한 협상 내 협상카드 사용 횟수 class quotations(MainTableMixin, MAIN_BASE): __tablename__ = "quotations" __table_args__ = {"schema": "quotation"} qt_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id = Column(UUID(as_uuid=True), nullable=False, index=True) qt_setting_id = Column(UUID(as_uuid=True), nullable=False, index=True) version_id = Column(UUID(as_uuid=True), nullable=False, index=True) name = Column(String(50), nullable=False) number = Column(String(30), nullable=False) type = Column(SmallInteger, nullable=False) # QuotationType: 1=renego(1:1) / 2=requote(1:N) round = Column(Integer, nullable=False, default=1) # 재견적 진행 시 증가 status = Column(SmallInteger, nullable=False) # 진행 상태 코드 start_time = Column(DateTime, nullable=False) end_time = Column(DateTime, 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) iteration = Column(Integer, nullable=False, default=0) preferred_sp_yn = Column(Boolean, nullable=True) preferred_sp_id = Column(UUID(as_uuid=True), nullable=True) 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" __table_args__ = {"schema": "negotiation"} 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" __table_args__ = {"schema": "negotiation"} 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 meta = Column(JSONB, nullable=True) # 말풍선 표현 데이터(script/step/client_step/input_mode/input_options/chat_end)