o2o-negosium-original/backend/common/database/model/models.py
민헌 3c8923c075 feat(chat): 협상 유의사항 안내 팝업 2종 이식 + 팝업 숨김 저장 API
KT-NEGOWIZ fe_v2 의 서비스 안내 팝업을 우리 구조로 이식.

- frontend: GuideContent(공용 본문, VAT/배송비는 chat init 메타로 동적 표시)
  + ServiceInfoPopup(채팅 진입 시 자동 — 안내 보지 않기/오늘 하루 보지 않기)
  + ServiceGuidePopup(메뉴 유의사항·이용 가이드 버튼 수동 — X/배경/ESC 닫기)
  + useServiceInfoPopup 훅(서버 상태 + localStorage 오늘 하루)
  + apis/auth 에 popupStatus 쿼리·hidePopup 뮤테이션, 헬프데스크 연락처 반영,
    한국어 단어 잘림 방지(break-keep)
- backend: GET /v1/auth/popup/status, POST /v1/auth/popup/hide
  (authenticate 공통 인증 + supplier_users.hide_service_info 영구 저장), 테스트 4건
- db: supplier_users.hide_service_info 컬럼 — 00-init 테이블 정의 갱신,
  기존 DB 보정은 alters/2026-07-07-supplier-users-hide-service-info.sql 별도 파일
  (기준선 이후 보정 ALTER 는 alters/ 파일로 관리하도록 규칙 변경)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:41:46 +09:00

229 lines
22 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
hide_service_info = Column(Boolean, nullable=False, server_default=text("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')"), onupdate=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, UPDATE 시 자동 갱신)
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')"), onupdate=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, UPDATE 시 자동 갱신)
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')"), onupdate=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, UPDATE 시 자동 갱신)
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) # 목표가(원)
anchoring_price = Column(BigInteger, nullable=True) # 앵커링가(원) — 생성 시 박제(negodata), 사후 수정 금지
anchoring_value = Column(SmallInteger, nullable=True) # 제안 당시 앵커링 값(‰) 박제 — 사후 수정 금지
last_offer_price = Column(BigInteger, nullable=True) # 협력사 마지막 제시가(원) — 가격 입력마다 갱신, 종료 후 불변. 앵커링 표본 판정의 "가격 흔적"
used_by_adjustment_id = Column(BigInteger, nullable=True) # 앵커링 배치 소비 마킹(NULL=미처리 0=제외 >0=조정 id) — schedules/anchoring 전용
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')"), onupdate=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, UPDATE 시 자동 갱신)
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')"), onupdate=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, UPDATE 시 자동 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
class quotation_settings(MAIN_BASE):
# quotation.quotation_settings (견적 설정). 견적 설정 스냅샷 — anchoring_value 는 구(舊) 앵커 산출용으로 채팅 경로에서는 더 이상 사용하지 않음(앵커는 sessions.anchoring_price 박제값).
@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")) # 앵커링 값(비율) — 구 방식 앵커 비율(현행 앵커 산출에는 미사용 — schedules/anchoring 참조)
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')"), onupdate=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, UPDATE 시 자동 갱신)
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')"), onupdate=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, UPDATE 시 자동 갱신)
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')"), onupdate=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, UPDATE 시 자동 갱신)
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부