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) purchase_price = Column(BigInteger, nullable=True) selling_price = Column(BigInteger, nullable=True) 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=재견적, 3=신규협상, 4=신규견적 (QtType) target_price = Column(BigInteger, nullable=False) # 목표가(원) target_anchoring_price = Column(BigInteger, nullable=True) 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=재견적, 3=신규협상, 4=신규견적 (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) supplier_type = Column(SmallInteger, nullable=True) 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)) 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")) # 소프트 삭제 여부