몰별 성공/실패 표시
lps_db.price_history 에는 몰별 정보가 다 있는데(naver_lowest·coupang_lowest·by_mall),
동기화가 final_source(이긴 몰) 하나만 남기고 나머지를 버려서 API 로는
"네이버는 어땠는지"를 알 수 없었다. 실제로 네이버는 빈손이고 쿠팡만 성공하는
케이스가 기본값처럼 나오는 중이라 화면에 드러낼 필요가 있다.
→ price_history.by_mall(JSONB)을 partner.item_internet_lowest_prices 로 그대로
미러링한다(열린 스키마 — 오픈마켓 폴백이 늘어도 스키마 변경 불필요).
- crud: 읽기 계약에 by_mall 추가 + SELECT 포함
- model: item_internet_lowest_prices.by_mall
- service: 언팩·저장
- protocol: LowestPriceEntry.by_mall
- init.sql: 테이블 정의 + 하단 보정 ALTER(기존 DB 반영용)
강제 재검색
POST /v1/item/{id}/lowest-price?force=true → LPS 네거티브 캐시 우회.
사용자가 '다시 검색'을 누른 경우에만 true.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
349 lines
20 KiB
Python
349 lines
20 KiB
Python
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, SupplierUserStatus, SupplierUserRole
|
|
|
|
# 모든 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(timezone=True), nullable=False, server_default=_utc_now_sql())
|
|
updated_at = Column(DateTime(timezone=True), 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
|
|
# 회사별 커스터마이징 설정. branding(CI)/labels(용어)/features(동작)/item_fields·supplier_fields(커스텀필드 정의)
|
|
settings = Column(JSONB, nullable=True)
|
|
|
|
|
|
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(timezone=True), 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 notifications(MainTableMixin, MAIN_BASE):
|
|
__tablename__ = "notifications"
|
|
__table_args__ = {"schema": "company"}
|
|
|
|
notification_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 수신자(users.user_id) = 견적 작성자
|
|
type = Column(SmallInteger, nullable=False) # NotificationType: 1=success(낙찰) 2=regenerated(재생성) 3=failure(결렬)
|
|
ref_qt_id = Column(UUID(as_uuid=True), nullable=True) # 관련 견적(quotations.qt_id)
|
|
ref_session_id = Column(UUID(as_uuid=True), nullable=True) # 관련 세션(sessions.session_id)
|
|
data = Column(JSONB, nullable=True) # 렌더 스냅샷(유형별)
|
|
read_at = Column(DateTime(timezone=True), nullable=True) # 읽은 시각(NULL=안읽음)
|
|
|
|
|
|
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) # 최저가 솔루션 원자성 보존용
|
|
internet_lowest_price = Column(BigInteger, nullable=True)
|
|
purchase_price = Column(BigInteger, nullable=True)
|
|
selling_price = Column(BigInteger, nullable=True)
|
|
|
|
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)
|
|
# 회사 커스텀필드 값. 정의(키/라벨/타입)는 companies.settings.item_fields, 여기는 {key: value}만
|
|
custom = Column(JSONB, nullable=True)
|
|
|
|
|
|
class item_internet_lowest_prices(MainTableMixin, MAIN_BASE):
|
|
"""인터넷 최저가 수집 이력(시도 단위, 성공/실패 모두 기록).
|
|
확정 대표값은 items.internet_lowest_price 에 박제된다(N건 이력 → 1건 대표 패턴).
|
|
LPS 동기화 배치(services/lps_sync_service)가 lps_db.price_history 를 읽어 채운다."""
|
|
|
|
__tablename__ = "item_internet_lowest_prices"
|
|
__table_args__ = {"schema": "partner"}
|
|
|
|
lp_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
item_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
|
|
|
lp_price = Column(BigInteger, nullable=True) # 수집한 최저가(원). 실패면 NULL
|
|
website = Column(SmallInteger, nullable=False) # LowestPriceWebsite 코드
|
|
success_yn = Column(Boolean, nullable=False)
|
|
fail_reason = Column(String(100), nullable=True) # 실패 사유(예: not_found)
|
|
ai_model = Column(SmallInteger, nullable=True) # (예약) AI 모델 코드 — LPS 계약엔 미포함
|
|
crawl_duration_ms = Column(Integer, nullable=True) # (예약) 수집 소요 — LPS 계약엔 미포함
|
|
lp_name = Column(String(300), nullable=True) # 찾은 상품명(판매 페이지 기준) — 근거 검증용
|
|
lp_url = Column(String, nullable=True) # 찾은 판매 페이지 링크(TEXT) — 근거 검증용
|
|
# 몰별 최저가 스냅샷 — lps_db.price_history.by_mall 을 그대로 미러링한다(열린 스키마).
|
|
# [{source, mall_name, price, shipping_fee, shipping_type, name, detail_url}, ...] 가격 오름차순.
|
|
# 매칭된 몰만 들어오므로, 특정 몰이 없으면 그 몰은 빈손이었다는 뜻이다(네이버 실패/쿠팡만 성공 구분).
|
|
by_mall = Column(JSONB, nullable=True)
|
|
crawl_end_time = Column(DateTime(timezone=True), nullable=False) # 수집 완료 시각(=price_history.created_at, 워터마크 기준)
|
|
|
|
|
|
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) 교정
|
|
total_revenue = Column(BigInteger, nullable=True) # 총매출액
|
|
# 회사 커스텀필드 값. 정의는 companies.settings.supplier_fields, 여기는 {key: value}만
|
|
custom = Column(JSONB, nullable=True)
|
|
|
|
|
|
class supplier_users(MainTableMixin, MAIN_BASE):
|
|
__tablename__ = "supplier_users"
|
|
__table_args__ = {"schema": "supplier"}
|
|
|
|
su_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True) # partner.suppliers (no-FK)
|
|
id = Column(String(20), nullable=False, index=True) # 로그인 아이디
|
|
password = Column(String(255), nullable=False) # bcrypt 해시
|
|
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, server_default=_utc_now_sql())
|
|
status = Column(SmallInteger, nullable=False, default=SupplierUserStatus.ACTIVE.value)
|
|
role = Column(SmallInteger, nullable=False, default=SupplierUserRole.MANAGER.value)
|
|
hide_service_info = Column(Boolean, nullable=False, server_default=text("false"), default=False)
|
|
|
|
|
|
# 채팅 계정의 로그인 토큰(루트 backend 소유) — negodata 는 비번재설정/비활성 시 기존 로그인을 끊기 위한 삭제만 한다.
|
|
class supplier_user_tokens(MainTableMixin, MAIN_BASE):
|
|
__tablename__ = "supplier_user_tokens"
|
|
__table_args__ = {"schema": "supplier"}
|
|
|
|
sut_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
su_id = Column(UUID(as_uuid=True), nullable=False, index=True) # supplier_users.su_id
|
|
type = Column(SmallInteger, nullable=False)
|
|
token = Column(JSONB, nullable=False)
|
|
issued_at = Column(DateTime(timezone=True), nullable=False)
|
|
expired_at = Column(DateTime(timezone=True), nullable=False)
|
|
|
|
|
|
class supplier_items(MainTableMixin, MAIN_BASE):
|
|
__tablename__ = "supplier_items"
|
|
# (supplier_id, item_id) 유일성은 soft-delete 인지 부분 유니크 인덱스(uq_supplier_items, WHERE deleted=FALSE)로 DB에서 보장.
|
|
__table_args__ = {"schema": "partner"}
|
|
|
|
supplier_item_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True) # suppliers.supplier_id
|
|
item_id = Column(UUID(as_uuid=True), nullable=False, index=True) # items.item_id
|
|
# SupplierType: 이 협력사가 이 상품을 공급하는 방식(0=없음/1=유통/2=제조/3=총판).
|
|
supply_type = Column(SmallInteger, nullable=False, server_default=text("0"), default=0)
|
|
|
|
|
|
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)
|
|
usage_type = Column(SmallInteger, nullable=False, default=1)
|
|
|
|
|
|
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)
|
|
usage_type = Column(SmallInteger, nullable=False, default=1)
|
|
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) # 목표 마진율(목표가 산정에 사용)
|
|
card_count = Column(Integer, nullable=False, default=3)
|
|
# 낙찰 가격정책(mid/over/regen)은 견적 단위로 이관, 앵커링은 칸 rate(anchoring v1.2)로 대체 → 세팅 컬럼 제거됨.
|
|
|
|
|
|
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) / 3=new_nego(1:1) / 4=new_quote(1:N)
|
|
round = Column(Integer, nullable=False, default=1) # 재견적 진행 시 증가
|
|
status = Column(SmallInteger, nullable=False) # 진행 상태 코드
|
|
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)
|
|
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)
|
|
close_reason = Column(SmallInteger, nullable=True) # CloseReason 코드. 마감 시 사유 기록(재생성 한도 카운팅·유찰 사유 구분). 미마감이면 NULL
|
|
|
|
# 낙찰 기준(가격게이트) — 견적 단위. 마감 판정(close_and_decide)이 이 행값을 읽는다. 기준 미달이면 개찰(낙찰자 미정 마감).
|
|
# 1:1 협상: over 는 항상 OPEN(목표 초과=개찰), mid 만 앵커/목표 택1. 1:N 경매: mid=over=AWARD 강제(무조건 최저가 낙찰).
|
|
mid_action = Column(SmallInteger, nullable=False, server_default=text("1"), default=1) # PriceGateAction: 앵커링가<투찰가≤목표가 처리(1=낙찰/2=개찰)
|
|
over_action = Column(SmallInteger, nullable=False, server_default=text("1"), default=1) # PriceGateAction: 목표가<투찰가 처리(1=낙찰/2=개찰)
|
|
|
|
|
|
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) # 목표가(원)
|
|
anchoring_price = Column(BigInteger, nullable=True) # 앵커링가(원) — 생성 시 박제, 이후 수정 금지(앵커링 배치 판정 기준)
|
|
anchoring_value = Column(SmallInteger, nullable=True) # 제안 당시 앵커링 값(정수 ‰) 박제 — 위와 동일 규칙. 주의: quotation_settings.anchoring_value(구 float 비율)와 무관. 나머지 앵커링 컬럼(last_offer_price 등)은 backend/배치 소유라 매핑 안 함
|
|
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) # DeliveryType 코드
|
|
email_sent_at = Column(DateTime(timezone=True), nullable=True) # 협상 초청 메일 발송 시각(NULL=미발송)
|
|
custom = Column(JSONB, nullable=True) # 협상완료 부가정보 값 {key: value} (정의는 companies.settings.session_fields)
|
|
|
|
|
|
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)
|