From bcb87de35ec07217f689f2002e9b9776e81f6043 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Wed, 22 Jul 2026 16:10:22 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20backend:=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EC=99=80=EC=9D=BC=EB=93=9C=EC=B9=B4=EB=93=9C=20card=5Fid=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=E2=80=94=20wild=20=EC=8A=A4=ED=85=9D?= =?UTF-8?q?=EC=9D=B4=EB=A9=B4=20wild=5Fcards=20=EC=A1=B0=ED=9A=8C=C2=B7car?= =?UTF-8?q?d=5Ftype=3D2(nego=EB=8A=94=20card=5Ftype=3D1,=20step=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B5=AC=EB=B6=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/common/database/model/models.py | 14 +++++++++++ backend/crud/chat_crud.py | 18 +++++++++++++- backend/services/chat_service.py | 33 ++++++++++++++++--------- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/backend/common/database/model/models.py b/backend/common/database/model/models.py index 6418d31..ccf03f9 100644 --- a/backend/common/database/model/models.py +++ b/backend/common/database/model/models.py @@ -213,6 +213,20 @@ class nego_cards(MAIN_BASE): deleted = Column(Boolean, nullable=False, server_default=text("false")) +class wild_cards(MAIN_BASE): + # card.wild_cards (와일드카드). backend 는 번호→UUID 변환(chats.card_id 저장)만 위해 최소 컬럼 미러. + @staticmethod + def DBType(): + return DBType.NEGOTIATION.value # 같은 negosium_db + + __tablename__ = "wild_cards" + __table_args__ = {"schema": "card"} + + wild_card_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) + number = Column(String(10), nullable=True) + deleted = Column(Boolean, nullable=False, server_default=text("false")) + + class chats(MAIN_BASE): # negotiation.chats (협상 채팅 메시지 로그). session 1 : N chats. (session_id, seq) 유니크. @staticmethod diff --git a/backend/crud/chat_crud.py b/backend/crud/chat_crud.py index 6e36228..32deb8c 100644 --- a/backend/crud/chat_crud.py +++ b/backend/crud/chat_crud.py @@ -6,7 +6,7 @@ from sqlalchemy import asc, desc, select, update from sqlalchemy.ext.asyncio import AsyncSession from common.database.db_session_manager import DB_SESSION_MNG -from common.database.model.models import chats, items, sessions, nego_cards +from common.database.model.models import chats, items, sessions, nego_cards, wild_cards from common.enums import ErrorType, SessionStatus from common.logger import LOG @@ -51,6 +51,10 @@ class IChatCRUD(ABC): async def get_nego_card_id_by_number(self, cdb: AsyncSession, number: str): pass + @abstractmethod + async def get_wild_card_id_by_number(self, cdb: AsyncSession, number: str): + pass + class ChatCRUD(IChatCRUD): async def list_by_session(self, cdb: AsyncSession, session_id) -> Tuple[ErrorType, list]: @@ -128,6 +132,18 @@ class ChatCRUD(IChatCRUD): LOG.e_no_callstack(ex) return None + async def get_wild_card_id_by_number(self, cdb: AsyncSession, number: str): + # 와일드카드 번호(agent turn.card_id, wild_card_dynamic) → wild_card_id(UUID). 없으면 None. + try: + query = select(wild_cards.wild_card_id).where(wild_cards.number == number, wild_cards.deleted == False).limit(1) # noqa: E712 + err_type, row_list = await DB_SESSION_MNG.execute(cdb, query, f"get_wild_card_id_by_number({number}) failed.") + if err_type != ErrorType.SUCCESS or not row_list: + return None + return row_list[0] + except Exception as ex: + LOG.e_no_callstack(ex) + return None + async def finalize_session( self, cdb: AsyncSession, session_id, status: int, bid_price: Optional[int] = None, reject_reason: Optional[str] = None, reject_price: Optional[int] = None, diff --git a/backend/services/chat_service.py b/backend/services/chat_service.py index d434f0d..0dabae4 100644 --- a/backend/services/chat_service.py +++ b/backend/services/chat_service.py @@ -103,7 +103,7 @@ class ChatService: ) @staticmethod - def _build_bot_chat(sess, seq: int, turn, bot_chat_type: Optional[str] = None, summary: Optional[dict] = None, nego_card_uuid=None) -> chats: + def _build_bot_chat(sess, seq: int, turn, bot_chat_type: Optional[str] = None, summary: Optional[dict] = None, card_uuid=None, card_type=None) -> chats: # bot_chat_type/summary 도 meta 에 영속화 → 히스토리 복원 시 폼 재현. indicator_value 는 전용 컬럼에도 적재. # nego_card_uuid: turn.card_id(번호)를 UUID 로 변환한 값(nego 카드). 있으면 chats.card_id/card_type/card_used_yn 컬럼에 적재 # → negodata 가 이 컬럼으로 카드 사용/효과를 조인한다. (wild 카드는 agent 가 card_id 미제공 — 별도 작업) @@ -112,9 +112,9 @@ class ChatService: sender=ChatSender.BOT.value, target_price=int(sess.target_price or 0), indicator_value=turn.indicator_value, - card_id=nego_card_uuid, - card_type=1 if nego_card_uuid else None, # CardType: 1=nego (wild=2 는 별도) - card_used_yn=True if nego_card_uuid else None, + card_id=card_uuid, + card_type=card_type if card_uuid else None, # CardType: 1=nego, 2=wild + card_used_yn=True if card_uuid else None, meta={ "script": turn.script, "step": turn.step, "client_step": turn.client_step, "input_mode": turn.input_mode, "input_options": turn.input_options, @@ -377,16 +377,27 @@ class ChatService: # 유저 미입력 가격 타결 케이스 — 마지막 유저 제시가와 다를 수 있다). summary = await self._build_summary(sess, quote, item, final_price, turn.settled_price or last_price) - # nego 카드 번호(turn.card_id) → UUID 변환. 카드 사용 로그(chats.card_id/type/used)를 negodata 조인용으로 남긴다. - nego_card_uuid = None + # 카드 번호(turn.card_id) → UUID 변환. step 으로 nego/wild 갈라 각 테이블 조회(번호가 겹칠 수 있어 종류로 구분). + # 카드 사용 로그(chats.card_id/type/used)를 negodata 조인용으로 남긴다. (1% 인하 시스템 카드는 agent 가 card_id 미제공) + card_uuid = None + card_type = None if turn.card_id: - nego_card_uuid = await DB_SESSION_MNG.execute_lambda( - chats.DBType(), DBWRType.DB_READ.value, - lambda s: self.chat_crud.get_nego_card_id_by_number(s, str(turn.card_id)), - ) + is_wild = bool(turn.step and turn.step.startswith("wild")) + if is_wild: + card_uuid = await DB_SESSION_MNG.execute_lambda( + chats.DBType(), DBWRType.DB_READ.value, + lambda s: self.chat_crud.get_wild_card_id_by_number(s, str(turn.card_id)), + ) + card_type = 2 + else: + card_uuid = await DB_SESSION_MNG.execute_lambda( + chats.DBType(), DBWRType.DB_READ.value, + lambda s: self.chat_crud.get_nego_card_id_by_number(s, str(turn.card_id)), + ) + card_type = 1 # 봇 메시지 + 종료 시 확정(성공=DONE+입찰가 / 실패=REJECTED+거부사유·제시가). 한 트랜잭션. - bot_msg = self._build_bot_chat(sess, seq=max_seq + 2, turn=turn, bot_chat_type=bot_chat_type, summary=summary, nego_card_uuid=nego_card_uuid) + bot_msg = self._build_bot_chat(sess, seq=max_seq + 2, turn=turn, bot_chat_type=bot_chat_type, summary=summary, card_uuid=card_uuid, card_type=card_type) funcs = [lambda s: self.chat_crud.insert_message(s, bot_msg)] # 가격 입력 턴 → 마지막 제시가를 봇 메시지 저장과 같은 트랜잭션으로 갱신. # 앵커링 표본 판정의 "가격 흔적"(가격을 써낸 협상만 집계 — 중간 이탈해도 실패로 측정 가능).