협상 불가 사유를 내면 500 이 나고, 거부 폼은 목록·채팅이 따로 놀았으며,
제출한 내용을 다시 볼 방법이 없었다. 결렬 건은 협력사가 낸 거부가를 계약가로
간주해 낙찰시켜 절감 통계가 음수로 뒤집힐 수 있었고, 견적 상세는 판정 가격이
세 곳에 흩어져 대화 탭에선 아예 보이지 않았다.
agent
- 결렬 종료 로깅 크래시 수정 — _log 를 action_id 기반으로 되돌리고 선택 근거
(Q·UCB·방문수)는 decision/policy 가 있을 때만 채운다. 종료 행은 카드 선택이
없고 policy.update 뒤라 값을 넣으면 학습 화면 집계가 오염된다
negosium
- 거부 폼을 목록·채팅 공용 컴포넌트 하나로 통일(사유 3종 + 공급 희망가·의견 선택)
- 거부 사유 열람 — 목록에 '거부 사유 보기'(부가정보 보기와 같은 규격), 채팅
재진입 시 대화 끝에 거부 내역 카드. 목록·채팅 init 응답에 reject_reason·reject_price 추가
- 자유 입력 거부("협상 포기합니다")가 사유 NULL 로 저장되던 문제 수정 — 폼 마커가
없으면 원문을 사유로 쓰고, 문장 속 숫자를 희망가로 오인하지 않는다
- koreanNumber 를 전역 lib 으로 이동(공용 폼이 쓴다)
negodata
- 직접 낙찰에 계약가 입력 — 결렬·미응찰 건을 오프라인으로 다시 협상한 결과를
담당자가 확정해 넣는다. 후보는 초청 협력사 전부(가격 미제출도 포함),
계약가는 sessions.custom.offline_award 에 근거·작성자·시각과 함께 남긴다
- 통계 계약가 = 담당자 확정가 우선, 없으면 투찰가. 거부가를 계약가로 치던 파생 제거.
KPI 에 오프라인 반영 건수 추가
- 견적 상세 리모델링 — 가격 레일(앵커링가/투찰현황 · 목표가 · 타결 상한가 · 결과가)을
시트에 고정해 접힘·탭 전환에도 남기고, 스펙트럼에 타결 판정선과 구간색 추가.
라벨은 폭을 실측해 두 레인으로 배치(겹침 불가). 상품·마감시각 등 전 행 동일 컬럼 제거,
협상현황에 부가정보 노출, 1:1 은 협력사·세션상태를 결과 밴드로 올림
테스트: negosium 58 · negodata 110 통과. 프론트 빌드/린트 통과.
원인 두 겹.
1) 절충 계열 가드가 타결 상한만 봐서, 중간가((앵커+목표)/2)처럼 구조상 목표가를 넘지 못하는
값은 항상 통과했다. 앵커가 목표가에 붙으면 그 값이 곧 목표가라 목표가로 제시된다.
→ 파생 제안값이 목표가 이상이면 미발동(compute_offer). 목표가 카드는 영향 없음.
2) 제안가 확정 시 계산에 쓴 당사 직전가를 남기지 않고 그 자리를 제안가로 덮은 뒤, 멘트 렌더가
갱신된 값으로 재계산해 "당사 A 와 귀사 B 의 절반은 A" 자기모순이 났다. 직전 커밋은 갱신을
렌더 뒤로 미뤘으나 와일드카드는 렌더 지점이 더 뒤라 그대로 남아 있었다.
→ Offer/record_offer 로 금액과 재료(당사 직전가·협력사 제시가)를 함께 박제하고, vars_for 는
재계산 대신 그 기록을 읽는다. 렌더 순서에 의존하지 않으므로 직전 커밋의 순서 조정은 제거.
→ 산식은 tactics.OFFER_VARIABLES 한 곳만 남기고 chat_engine 의 복사본 삭제.
곁들여: 카드 설정에서 '적용 대기'로 꺼둔 와일드카드가 견적에 담겨 있으면 그대로 발동하던 것을
차단(wild_cards.available 조건). 테이블 정의에 빠져 있던 컬럼도 보강.
검증: 단위/E2E 35건, 실협상 8시나리오(멘트 토큰 노출·절반 산술 정합·카드 중복·제안가 역행·
상한 초과 자동 검사) 통과.
목표가를 1원이라도 넘으면 결렬되던 탓에, 기존 단가보다 인하됐는데도 결렬되는 케이스가 있었다
(EST-202607-973E: 기존 17,500 / 목표 16,980 / 최종 17,300). 견적 생성 시 세션에 박제해 두던
done_ceiling_price(목표가×(1+타결상한율), 세팅 기본 +5%)를 협상 엔진이 실제로 읽게 배선했다.
- tactics: settle_ceiling() 신설 — 타결선 판정을 한 곳으로. 박제가 없는 옛 세션·데모는 목표가 폴백
- 카드 제안가 유효조건의 상한도 목표가 → 타결 상한가 (받아줄 수 있는 금액까지는 부를 수 있어야 함)
- _render 가드레일이 목표가 초과 성공을 결렬로 되돌리고 있어 같이 상한 기준으로 교정 —
타결 판정만 고치면 이 가드에서 다시 뒤집혀, 배선했는데도 결렬로 떨어졌다
- crud/loader/세션 컨텍스트에 done_ceiling_price 적재
검증(목표가 956,580 · 상한 1,004,410): 950,000·1,000,000·1,004,410 타결 / 1,004,500·1,010,000 결렬.
agent 테스트 178건 통과.
어느 가격이 "공급사에 지불하는 단가"인지는 회사마다 달라, 컬럼을 합치는 대신 회사가 고르게 했다.
판정은 settings.features.nego_baseline_field 1순위, 미설정 회사는 price 만 숨겼으면 purchase_price 폴백.
판정식 정본은 negodata/backend/common/nego_baseline.py (agent·negosium backend 가 같은 규칙 미러).
- negodata front: 회사 설정에 협상 기준가 라디오(선택지마다 실제 나갈 문장 미리보기 + 학습 경고),
'공급사 포털 안내' 탭 신설(협상 유의사항·헬프데스크 연락처), 용어 카탈로그에 target_price·supplier 추가.
인터넷 최저가는 숨김 대상에서 제외(신규 견적의 유일한 목표가 후보).
상품 등록 기본값에서 개발용 더미 제거(price 1,000,000·PROD-BAT-###·800,000·대한민국·10 EA·14).
- agent: get_item_price → get_item_baseline (기준가·호칭·회사 용어사전을 한 쿼리로),
"기존 공급가 대비" 하드코딩을 회사 용어로 치환 + 받침 기준 조사 자동 보정,
협상 스크립트 4종의 협력사·목표가·배송형태 용어를 {label_*} 토큰화, input_options 도 변수 치환 적용.
- negosium: 협상 화면 기준 단가·배송형태 라벨을 회사 설정 기준으로, 유의사항 본문과 헬프데스크 연락처를
하드코딩에서 회사 설정으로(미등록 시 영역 숨김). 유의사항의 VAT·배송비 문구 삭제(IMK 0803 ⑥).
- negodata backend: LPS 검색 가격 힌트를 items.price 고정에서 기준가 규칙으로.
검증: 기준가 설정 3 × 숨김 4 = 12조합에서 협상 멘트·포털 표시·LPS 힌트가 전부 일치.
agent 테스트 176건 통과. 보고서 negodata/docs/nego-baseline-verification.md
1) 표시가≠투찰가 (배포 서버 협상 결과 오류)
- 카운터 제시 중 멘트의 절충/중간 변수(middle_price·target_mid_price)가 vars_for 재계산으로
compute_counter 의 target 클램프·prev_customer 갱신과 어긋나, 화면엔 1,740,000 인데 실제로는
1,700,000 으로 타결되던 문제 → vars_for 에서 세 변수(counter/middle/target_mid)를 pending 으로 고정
- WC-03 최후통첩 멘트가 제시 금액을 안 보여줘 수락 시 화면에 없던 target 으로 타결되던 문제 →
멘트에 {target_price} 명시(seed init-data.sql). 운영 DB 는 별도 UPDATE 필요
2) 카드 사용 횟수 3회 초과
- quotation_settings.card_count(협상카드 사용 횟수 상한, 기본 3)가 어디서도 강제되지 않던 죽은 설정 →
에이전트가 sessions.qt_setting_id 로 card_count 를 조회해 협상카드 재생 수를 min(선택수, card_count)로 캡
(session.action_space_size 는 소진 판정 전용 — Q-table 은 카탈로그 전체로 별도 고정)
- 회귀 테스트: 표시가==타결가(test_card_tactics·test_p7_chat), card_count 로드(test_context_loader). 전체 153 pass
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- agent: 인터넷 최저가(items.internet_lowest_price)를 협상 파이프라인에 연결
(nego_context_crud 조회 → context loader → session.context → chat_engine.vars_for).
값 있을 때만 {internet_lowest_price}/internet_min_price 치환, 미수집(0)이면 원형 유지(허위 시장가 방지)
- negodata: 카드 에디터 변수 카탈로그에 '인터넷 최저가' 추가
- negodata: 견적 생성 카드 선택 게이팅 — 조건전략 미작성({customer_condition} 잔존)·
최저가 미수집 상품의 {internet_lowest_price} 인용 카드는 선택 불가(상품 변경 시 자동 해제 포함)
- negodata: 상품 엑셀 업로드의 협력사 목록 size 1000→100
(PageParams le=100 초과 시 422 → 목록 비어 전 행 '미등록 협력사' 오탐)
- seed: WC-01 의 데이터 소스 없는 {customer_reference} 제거 → 고정 문구로 대체
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
카드 카탈로그(negodata)가 Q-table action space 를 정의하는 정본이 되고, 카드 변경이
config 수정·학습 손실 없이 agent 에 자동 반영되는 고리를 완성.
- action space 정리: 카탈로그 전체(NGC-001~011, 11장) 고정, 견적별 선택은 축소가 아니라
available_mask(_selection_mask) 로 처리 — action_id↔카드 대응을 견적마다 일정하게 유지해
Q-table 학습 일관성 보장. 구 인덱스 방식(selected[action_id]) 폐기.
- ① 카탈로그 DB 정본화: action_mapping.type=db 면 registry 가 card.nego_cards(user_id NULL,
number 순) 조회로 action_to_card 동적 구성(파일은 폴백). port/adapter(card_catalog_*).
_base=type:db. → negodata 카드 추가/삭제 시 config 수정 불필요.
- ② 차원 변경 학습 보존 마이그레이션: migrate_active_version_dim — 겹치는 셀 복사
(append/truncate 안전) + 새 카드 fresh. model_store.load 가 차원 불일치 시 호출.
- ③ reload 엔드포인트: /v1/catalog-refresh(테넌트) · /v1/catalog-refresh-all(전역, 화이트리스트).
- ④ 브랜드: company_profile_repo — 자동 온보딩 고객사(company_id UUID)는
company.companies.name 으로 {company_name} 채움. 데모 테넌트는 파일 유지.
- 크로스서비스: negodata card_service 가 공용 nego 카드 변경 시 agent_notify 로 전역 리로드 알림
(best-effort, is_test skip). config 에 agent_base_url.
- 하니스 episodes 400→600(action 11 수렴). 테스트 갱신·추가로 agent 98/98.
알려진 갭(후속): per-company 카탈로그 스코프(회사 카드도 action space 포함), 카탈로그 중간
삭제 시 카드번호 기반 마이그레이션.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
표현 아키텍처: agent 스크립트는 의미(텍스트)만 소유, 표현(굵기·색)은 프론트 소유.
Slate 는 negodata 에디터 내부에만 두고, 전송/저장은 마커 문자열 한 벌(구버전의 리치텍스트 이중관리 폐기).
- 트랙 A (negodata): serializeToMarker 추가 — Slate 마크(bold/underline/color)를 **/__/{{토큰}} 로
인코딩해 nego_cards.script 저장. edit_script(Slate 원본)는 재편집 전용. 고정 3색 → 시맨틱 토큰(강조/안내).
- 트랙 B (agent): 카드 멘트 DB 소스 — ICardScriptRepository/CardScriptDbRepository(port+adapter),
ScriptRepository.resolve_card_script 가 cards.source_type=backoffice_db 면 card.nego_cards.script 우선,
없으면 파일 폴백. action_id→card_id→nego_cards.number 매칭.
- 트랙 C (양 프론트): renderEmphasis 재귀 파서 — **굵게**·__밑줄__·{{강조|빨강}}·{{안내|파랑}} 중첩 렌더.
색은 시맨틱 토큰→디자인 토큰 클래스(다크모드 안전). negodata tokens.css 에 --info 신설. CardTable 미리보기 적용.
- supplier_items 연동: 유통코드=supplier_items.supply_type(→quotations.supplier_type 폴백),
파트너유형=상품별 매핑 협력사 수(→세션 이력 폴백).
- 가격 수용률: 기존 공급가(item_price) 기준 양보율로 정정 — 첫 라운드부터 실값(첫 제시가 기준 0 아님).
테스트: agent 86/86, 공급사 frontend·negodata front tsc 통과.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 동적 가중치 W: 라운드 감쇠 → 상태 5차원 가중합 clip(Σwᵢ·Sᵢ, 0.2, 0.8) (식 12~13, 기존 w1~w5 연결)
- 종료보상에 (1−W) 적용: R = W×R_price + (1−W)×R_end − λ×round (식 8)
- R_price 3단계: P<anchor 시 1+β·(anchor−P)/anchor 초과달성 보너스 추가 (식 9~11, beta 의미 재정의)
- price zone 경계는 명세(T)와 달리 anchor 유지(우선협상 규칙이 실제 의사결정 경계) — 사유 docstring 명시
- state_calculator/config 의 낡은 반대 컨벤션(anchor≥target) 주석 정정
- RewardCalculator(RewardConfig, StateConfig) 시그니처 변경 + 호출부 5곳 갱신, 테스트 기대값 정정 (76/76 PASS)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agent 가격협상 무한 루프 수정 + 선행 chat_server 의 가격협상 연출(카드
스크립트 + 협상지표 게이지) 복원, 버짓 확인 입력가 표시 버그, 요약카드
공급사 담당자/날짜 표시 정비.
- agent: 가격협상 루프를 라운드 상한(MAX_ROUNDS)으로 종료(카드선택 실패해도
종료 보장 — 기존엔 카드소진에만 의존해 무한 입력). chat_server iteration>=3 동작 복원.
- agent: 가격협상 턴에 선택 카드 스크립트(scripts_cards.json)를 script 로 렌더 +
협상지표(indicator.py, 1~99/PZ) 산출해 indicator_value/bot_chat_type 전달.
Res_Chat 에 indicator/bot_chat_type, Req_Chat 에 item_price 추가.
- agent: 가격협상_확인 멘트에 할인율(discount_rate) 표시,
가격협상_확인_버짓이 target 이 아니라 입력가(input_price)를 표시하도록 수정.
- backend: item_price(기존 공급가) 를 agent 로 전달. 요약카드 공급사 담당자
정보를 supplier_users(빈 이메일) 대신 partner.suppliers.manager_* 에서 조회,
ChatSummary.supplier_manager_phone 추가.
- frontend: 요약카드 협상 개시/종료 시각을 "YYYY년 MM월 dd일 HH시 MM분 SS초"(KST)
로, 공급 계약 만료일(+1년) ISO 파싱, 우선협상 대상자에 공급사 연락처/이메일 표시.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>