버그 수정중 .

This commit is contained in:
hbyang 2026-06-18 18:59:06 +09:00
parent 088c57f415
commit cb3adf6d2e
5 changed files with 41 additions and 11 deletions

View File

@ -6,6 +6,7 @@ Chat_server 의 step 체계(서비스안내→담당자확인→협상품목안
DB/정책(카드선택·학습)은 여기 두지 않는다 — ChatService(async)가 StepView 의 신호를 보고 처리한다. DB/정책(카드선택·학습)은 여기 두지 않는다 — ChatService(async)가 StepView 의 신호를 보고 처리한다.
""" """
import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@ -15,6 +16,22 @@ MAX_ROUNDS = 3
_PRICE_MODES = ("price",) _PRICE_MODES = ("price",)
_CHOICE_MODES = ("yes_no", "confirm", "delivery_type") _CHOICE_MODES = ("yes_no", "confirm", "delivery_type")
# 프론트는 표시용 문자열로 가격을 보낸다(예: "530,000원"). 천단위 콤마·통화기호("원")·공백 등
# 숫자 외 문자를 제거하고 파싱한다. (콤마만 지우면 "원" 때문에 float() 가 실패해 가격 입력이
# 영영 저장되지 않고 같은 step 에 머무는 버그가 났었다.)
_PRICE_CLEAN_RE = re.compile(r"[^\d.]")
def _parse_price(user_input: Any) -> Optional[float]:
cleaned = _PRICE_CLEAN_RE.sub("", str(user_input if user_input is not None else ""))
if not cleaned or cleaned == ".":
return None
try:
price = float(cleaned)
except ValueError:
return None
return price if price > 0 else None
@dataclass @dataclass
class ChatSession: class ChatSession:
@ -62,9 +79,8 @@ class ChatEngine:
mode = node.get("next_input_mode", "null") mode = node.get("next_input_mode", "null")
if mode in _PRICE_MODES: if mode in _PRICE_MODES:
try: price = _parse_price(user_input)
price = float(str(user_input).replace(",", "")) if price is None:
except (TypeError, ValueError):
return self._error(session, "가격을 숫자로 입력해 주세요.") return self._error(session, "가격을 숫자로 입력해 주세요.")
session.context["input_price"] = price session.context["input_price"] = price
session.context["round"] = session.context.get("round", 0) + 1 session.context["round"] = session.context.get("round", 0) + 1

View File

@ -147,7 +147,7 @@ class ChatService:
async def _seed_opening(self, sess) -> Optional[ChatMessage]: async def _seed_opening(self, sess) -> Optional[ChatMessage]:
"""오프닝(턴0) 봇 메시지를 agent 로 생성하고 seq=1 로 저장한다. 동시 진입 충돌은 무시(유니크가 방어).""" """오프닝(턴0) 봇 메시지를 agent 로 생성하고 seq=1 로 저장한다. 동시 진입 충돌은 무시(유니크가 방어)."""
ctx = self._agent_context(sess, turn=0) ctx = await self._agent_context(sess, turn=0)
turn = await self.agent.chat(session_id=str(sess.session_id), user_input=None, ctx=ctx) turn = await self.agent.chat(session_id=str(sess.session_id), user_input=None, ctx=ctx)
if not turn.ok: if not turn.ok:
return None return None
@ -231,7 +231,7 @@ class ChatService:
return res return res
# agent 위임 (한 턴). 실패 시 선점한 유저 메시지를 롤백 → 재시도 가능. # agent 위임 (한 턴). 실패 시 선점한 유저 메시지를 롤백 → 재시도 가능.
ctx = self._agent_context(sess, turn=turn_no) ctx = await self._agent_context(sess, turn=turn_no)
turn = await self.agent.chat(session_id=str(sess.session_id), user_input=user_input, ctx=ctx) turn = await self.agent.chat(session_id=str(sess.session_id), user_input=user_input, ctx=ctx)
if not turn.ok: if not turn.ok:
await DB_SESSION_MNG.execute_lambda_run( await DB_SESSION_MNG.execute_lambda_run(
@ -270,10 +270,16 @@ class ChatService:
return res return res
# ---- 빌더 / 매퍼 ---------------------------------------------------- # ---- 빌더 / 매퍼 ----------------------------------------------------
def _agent_context(self, sess, turn: int) -> AgentChatContext: async def _agent_context(self, sess, turn: int) -> AgentChatContext:
# 핸드오프 #2/#5: X-Tenant-ID 는 견적(갑) 회사 company_id 여야 한다. # 핸드오프 #2/#5: X-Tenant-ID 는 견적(갑) 회사 company_id 여야 한다.
# 현재 quotation.user_id 만 보유 → 정확한 company_id 해석(company.users 조회)은 agent 연동 시 보완. # 상품(partner.items)의 소유 회사가 갑(buyer)이므로 item.company_id 로 해석한다.
tenant_id = "" # mock 은 무시. 실제 연동 시 quotation 의 buyer company_id 로 채운다. tenant_id = "" # 해석 실패 시 빈 값(agent 가 400) — 로깅으로 추적
err_type, item = await DB_SESSION_MNG.execute_lambda(
items.DBType(), DBWRType.DB_READ.value,
lambda s: self.chat_crud.get_item_by_id(s, sess.item_id),
)
if err_type == ErrorType.SUCCESS and item is not None and item.company_id:
tenant_id = str(item.company_id)
rq_type = "재협상" if sess.qt_type == 1 else "재견적" rq_type = "재협상" if sess.qt_type == 1 else "재견적"
anchor = int(sess.target_price * 0.99) if sess.target_price else 0 anchor = int(sess.target_price * 0.99) if sess.target_price else 0
return AgentChatContext( return AgentChatContext(

View File

@ -21,6 +21,7 @@ services:
environment: environment:
APP_ENV: local APP_ENV: local
DB_HOST: host.docker.internal # 컨테이너→호스트 DB (config.local.toml의 127.0.0.1 override) DB_HOST: host.docker.internal # 컨테이너→호스트 DB (config.local.toml의 127.0.0.1 override)
AGENT_BASE_URL: http://host.docker.internal:9500 # 컨테이너→호스트 agent (로컬 uvicorn :9500)
ports: ports:
- "9300:9300" - "9300:9300"
# 컨테이너에서 호스트의 DB 로 접근 (config.docker.toml 의 host.docker.internal) # 컨테이너에서 호스트의 DB 로 접근 (config.docker.toml 의 host.docker.internal)

View File

@ -45,7 +45,10 @@ export function useChatController(sessionId: string) {
const setInitData = useChatInitStore((s) => s.setInitData) const setInitData = useChatInitStore((s) => s.setInitData)
const initQuery = useChatInitQuery(sessionId) const initQuery = useChatInitQuery(sessionId)
const messagesQuery = useChatMessagesQuery(sessionId) const messagesQuery = useChatMessagesQuery(sessionId)
const sendMutation = useChatSendMutation(sessionId) // mutate 는 참조가 안정적이다. useMutation 반환 객체 전체를 아래 effect deps 에 넣으면
// mutate 호출→pending 리렌더마다 객체 identity 가 바뀌어 effect 가 teardown(reset())→재바인딩되고,
// staleTime:Infinity 인 메시지 쿼리는 재조회되지 않아 스토어가 빈 채로 남아 채팅이 사라진다.
const { mutate: sendMutate } = useChatSendMutation(sessionId)
// init 메타 → 스토어 // init 메타 → 스토어
useEffect(() => { useEffect(() => {
@ -68,7 +71,7 @@ export function useChatController(sessionId: string) {
store.setIsLoading(true) store.setIsLoading(true)
store.setPriceErrorMessage('') store.setPriceErrorMessage('')
sendMutation.mutate( sendMutate(
{ user_input: text, user_input_type: inputType === 'text' ? null : inputType }, { user_input: text, user_input_type: inputType === 'text' ? null : inputType },
{ {
onSuccess: (data) => { onSuccess: (data) => {
@ -104,7 +107,7 @@ export function useChatController(sessionId: string) {
return () => { return () => {
useChatStore.getState().reset() useChatStore.getState().reset()
} }
}, [sessionId, sendMutation, navigate]) }, [sessionId, sendMutate, navigate])
return { return {
isInitLoading: initQuery.isLoading || messagesQuery.isLoading, isInitLoading: initQuery.isLoading || messagesQuery.isLoading,

View File

@ -140,6 +140,7 @@ class wild_cards(MainTableMixin, MAIN_BASE):
class versions(MainTableMixin, MAIN_BASE): class versions(MainTableMixin, MAIN_BASE):
__tablename__ = "versions" __tablename__ = "versions"
__table_args__ = {"schema": "card"}
version_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) version_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 버전 생성 유저 user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 버전 생성 유저
@ -149,6 +150,7 @@ class versions(MainTableMixin, MAIN_BASE):
class version_nego_cards(MainTableMixin, MAIN_BASE): class version_nego_cards(MainTableMixin, MAIN_BASE):
__tablename__ = "version_nego_cards" __tablename__ = "version_nego_cards"
__table_args__ = {"schema": "card"}
vnc_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 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 version_id = Column(UUID(as_uuid=True), nullable=False, index=True) # versions.version_id
@ -157,6 +159,7 @@ class version_nego_cards(MainTableMixin, MAIN_BASE):
class version_wild_cards(MainTableMixin, MAIN_BASE): class version_wild_cards(MainTableMixin, MAIN_BASE):
__tablename__ = "version_wild_cards" __tablename__ = "version_wild_cards"
__table_args__ = {"schema": "card"}
vwc_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) 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 version_id = Column(UUID(as_uuid=True), nullable=False, index=True) # versions.version_id
@ -240,3 +243,4 @@ class chats(MainTableMixin, MAIN_BASE):
card_used_yn = Column(Boolean, nullable=True) # 카드 사용 여부 card_used_yn = Column(Boolean, nullable=True) # 카드 사용 여부
indicator_value = Column(Numeric(8, 6), nullable=True) indicator_value = Column(Numeric(8, 6), nullable=True)
card_type = Column(SmallInteger, nullable=True) # 1=nego_card, 2=wild_card 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)