o2o-negosium-original/negodata/docs/nego-baseline-verification.md
Mina Choi 71ef28110b [feat] negodata·agent·negosium: 협상 기준가 회사별 선택(공급가/매입가) + 공급사 포털 안내 회사설정화
어느 가격이 "공급사에 지불하는 단가"인지는 회사마다 달라, 컬럼을 합치는 대신 회사가 고르게 했다.
판정은 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
2026-08-05 11:47:06 +09:00

15 KiB
Raw Permalink Blame History

협상 기준가 회사별 선택 — 변경 내역과 검증 보고서

작성 2026-08-05. 대상 = IMK 0803 요청 ⑥(유의사항·헬프데스크)·⑦(공급가–매입가 일원화).

핵심은 협상 기준가(인하율 멘트의 분모이자 RL 가격 수용률의 기준가)를 회사가 고르게 한 것이다. 매입해서 되파는 회사는 공급가(items.price)가, 매입만 하는 회사는 매입가(items.purchase_price)가 실제 지불 단가이므로, 컬럼을 합치는 대신 어느 컬럼을 쓸지 회사 설정으로 지정한다.


1. 판정 규칙

1순위  settings.features.nego_baseline_field  ('price' | 'purchase_price')
2순위  price 만 hidden_fields 에 있으면      → purchase_price   (설정 화면 이전 회사 안전망)
기본                                        → price

같은 규칙을 세 앱이 쓴다. 판정식을 바꿀 때는 반드시 세 곳을 같이 고친다.

앱 위치
negodata backend common/nego_baseline.py resolve_baseline_field (정본)
agent negotiation/chat/infra/repository/nego_context_crud.py _resolve_baseline
negosium backend services/chat_service.py chat_init 내 인라인 판정

2. 회사 유형별 설정 방법

어느 가격이 "우리가 공급사에 지불하는 단가"인지는 회사마다 다르다. 그래서 컬럼을 합치지 않고 회사가 고르게 했다. 설정 위치는 전부 negodata → 회사 설정(/settings, 최고관리자 전용).

유형 A — 매입만 하는 회사 (사서 쓰고, 되팔지 않음)

관리하는 가격이 매입가 하나뿐인 회사.

탭 설정
커스텀 필드 → 협상 기준가 매입가 선택
커스텀 필드 → 상품 필드 숨김 상품 단가(공급가)·판매가 체크
용어(라벨) 필요하면 item.purchase_price 를 자기 용어로(예: 구매단가)

결과 — 상품 등록·엑셀 양식에 매입가 칸만 남고, 협상 멘트는 "기존 매입가 대비 N% 인하", 인터넷 최저가 검색 힌트도 매입가로 나간다. 목표가는 인터넷최저가·매입가 후보로 산정된다.

유형 B — 매입해서 되파는 회사 (유통·구매대행)

공급사에서 사서(매입가) 고객사에 넘기는(공급가) 회사. 기본값이라 아무것도 안 해도 된다.

탭 설정
커스텀 필드 → 협상 기준가 상품 단가 선택 (미설정 시 기본값)
커스텀 필드 → 상품 필드 숨김 안 씀
용어(라벨) 필요하면 item.price 를 자기 용어로(IMK 는 "공급가")

결과 — 공급가가 협상 출발점, 매입가는 재견적 목표가 후보로 계속 쓰인다.

공통 주의

  • 기준가로 고른 필드는 숨기지 말 것. 숨기면 신규 상품 등록 화면에 그 칸이 없어 값이 비고, 인하율 멘트가 통째로 사라진다. 설정 화면이 그 조합을 고르면 경고를 띄운다.
  • 인터넷 최저가는 숨길 수 없다. 신규 견적의 유일한 목표가 후보라 숨김 목록에서 제외했다.
  • 협상 이력이 쌓인 뒤에는 바꾸지 말 것. 기준가는 RL 가격 수용률의 분모라 학습 상태 인덱스에 들어간다. 바꾸면 Q테이블에 두 기준이 섞이고 되돌려도 복구되지 않는다. 회사 온보딩 때 정한다.
  • 용어를 바꾸면 협상 멘트 호칭도 같이 바뀐다. 용어 탭의 item.price/item.purchase_price 라벨이 그대로 공급사에게 나가는 문장에 쓰인다(조사는 받침에 맞춰 자동 보정).

현재 IMK 설정

features.nego_baseline_field = "purchase_price"      → 매입가 기준 (유형 A)
hidden_fields = [made_in, delivery_fee_yn, selling_price, vat_yn, price]
labels        = { "item.price": "공급가", … }

3. 수정한 곳

agent (협상 엔진)

파일 내용
negotiation/chat/infra/repository/nego_context_crud.py _ITEMS 에 purchase_price·company_id 추가, _COMPANIES 테이블 신설. get_item_price → get_item_baseline 로 교체 — items⋈companies 한 쿼리로 (기준가, 호칭, 회사 용어사전) 반환. _resolve_baseline 판정 함수
negotiation/chat/service/negotiation_context_loader.py NegotiationDbContext 에 item_price_label·labels 추가 (세션 시작 시 박제)
services/chat_service.py 세션 컨텍스트에 item_price_label·labels 적재. 데모 경로 폴백 _DEFAULT_ITEM_PRICE_LABEL = "상품 단가"
negotiation/chat/service/chat_engine.py discount_phrase 3분기의 "공급가" 하드코딩 → 컨텍스트 호칭. _SCRIPT_LABELS 용어 토큰, _josa/_has_batchim 조사 자동 보정. input_options 도 변수 치환을 타게 수정(안 고쳤으면 {label_delivery_type_1} 토큰이 사용자에게 노출)
negotiation/chat/service/script_naturalizer.py docstring 문구
tenants/_base/resources/scripts_renegotiation.json "협력사 간"·"협력사 포털"·"협력사 관리 시스템" → {label_supplier}
tenants/_base/resources/scripts_requote.json 위 + "신규 공급사를 선정"·"공급사 선정에 반영"·"배송 형태를 선택"·배송 보기 3개
tenants/_base/resources/scripts_cards.json "목표 매입가" ×2 → {label_target_price}, "다른 협력사들의" → {label_supplier}. 4번 카드의 {discount_rate}% 수치 인용 제거(기준가 없을 때 "인하율 약 0.0%는 의미 있는 진전" 모순 방지)
tenants/_base/resources/scripts_wildcard.json "목표 매입가는" → {label_target_price}
tests/test_context_loader.py 더블·단언을 새 시그니처로

negosium backend (공급사 포털 API)

파일 내용
services/chat_service.py 협상 화면 item_price 를 기준가 규칙으로. 요약의 배송형태 라벨을 DeliveryType.label_of() 하드코딩 대신 회사 용어 우선
services/auth_service.py me()·session_branding() 에 헬프데스크·유의사항 전달
router/v1/auth/protocol.py Res_Me.guide_notices, Res_SessionBranding.helpdesk

negodata backend

파일 내용
common/nego_baseline.py 신규. 기준가 판정 정본 (resolve_baseline_field / resolve_baseline_price)
services/lps_sync_service.py 인터넷 최저가 검색의 가격 힌트를 items.price 고정 → 기준가 규칙. 호출부가 설정을 안 넘기면 상품의 소속 회사 설정을 직접 조회

negodata front (어드민)

파일 내용
features/settings/catalog.ts features.nego_baseline_field 타입·선택지, branding.helpdesk, guide_notices, DEFAULT_GUIDE_NOTICES. 용어 카탈로그에 target_price·supplier 추가. 숨김 가능 목록에서 internet_lowest_price 제외(신규 견적의 유일한 목표가 후보)
features/settings/SettingsView.tsx 공급사 포털 안내 탭 신설(협상 유의사항·헬프데스크). 커스텀 필드 탭 최상단에 협상 기준가 라디오(NegoBaselinePicker) — 선택지마다 실제로 나갈 문장 미리보기 + 학습 데이터 경고. LineListEditor. 저장·JSON 병합 경로에 features·guide_notices 반영
features/products/components/ProductFormSheet.tsx 신규 등록 기본값에서 개발용 더미 제거 — code: PROD-BAT-###·price: 1,000,000·minPrice: 800,000·origin: 대한민국·moq: 10 EA·leadTime: 14 → 빈 값/0. 선택형(단위·배송형태·부가세)만 유지

negosium front (공급사 포털 화면)

파일 내용
apis/auth/auth.type.ts Branding.helpdesk, MeResponse.guide_notices, AuthUser.guideNotices
features/auth/hooks/usePreLoginBranding.ts 로그인 전 헬프데스크 수신
features/chat/components/menu/Contact.tsx 하드코딩 연락처 → 회사 설정, 미등록이면 섹션 숨김
features/chat/components/popup/GuideContent.tsx 유의사항 불릿 5개 하드코딩 → 회사 설정(미설정 시 기본 문구). VAT·배송비 불릿 삭제(IMK ⑥ 요청)
pages/LoginPage.tsx 하드코딩 연락처 → 회사 설정

4. 경우의 수 검증 — 12조합 × 3경로

상품 = 산업용 베어링 6204 (공급가 8,900 / 매입가 7,200), 공급사 제시가 8,000. agent·negosium backend·negodata backend 를 각각 실제로 호출해 측정.

기준가 설정 숨김 포털 표시 LPS 가격 힌트 협상 멘트
미설정 없음 8,900 price=8900 기존 상품 단가 대비 약 10.1% 인하
미설정 price 7,200 purchase_price=7200 기존 매입가(7200원)보다 약 11.1% 높은
미설정 purchase_price 8,900 price=8900 기존 상품 단가 대비 약 10.1% 인하
미설정 둘 다 8,900 price=8900 기존 상품 단가 대비 약 10.1% 인하
=price 없음 8,900 price=8900 기존 상품 단가 대비 약 10.1% 인하
=price price 8,900 price=8900 기존 상품 단가 대비 약 10.1% 인하
=price purchase_price 8,900 price=8900 기존 상품 단가 대비 약 10.1% 인하
=price 둘 다 8,900 price=8900 기존 상품 단가 대비 약 10.1% 인하
=purchase 없음 7,200 purchase_price=7200 기존 매입가(7200원)보다 약 11.1% 높은
=purchase price 7,200 purchase_price=7200 기존 매입가(7200원)보다 약 11.1% 높은
=purchase purchase_price 7,200 purchase_price=7200 기존 매입가(7200원)보다 약 11.1% 높은
=purchase 둘 다 7,200 purchase_price=7200 기존 매입가(7200원)보다 약 11.1% 높은

12/12 세 경로가 같은 값을 쓴다. 어긋나는 조합 없음.

  • 명시 설정이 항상 이긴다 — =price 인데 price 를 숨겨도 8,900
  • 숨김 폴백은 price 만 숨겼을 때만 발동
  • 둘 다 숨겨도 price 로 폴백해 협상이 안 깨진다

용어·조사 검증

조건 출력
용어 미설정 + price 기존 상품 단가 대비 약 3.2% 인하된 금액입니다
item.price="공급가" 기존 공급가 대비 …
item.purchase_price="기준매입단가" 기존 기준매입단가 대비 약 4.2% 인하된 금액입니다
item.purchase_price="기준값"(받침) + 제시가=기준가 기존 기준값과 동일한 수준의 금액입니다
기준가 컬럼 NULL 인하율 문장 생략(0원 대비 계산 안 나감)

용어를 전부 바꾼 회사(supplier=공급업체, target_price=목표단가)로 전 흐름 실행:

"본 서비스는 아이마켓코리아와 공급업체 간 물품 공급 가격 협상을 위한 것으로…"
"본 안내는 공급업체 포털에 등록된 담당자에게 발송되었습니다."
"아이마켓코리아는 아래 상품에 대해 신규 공급업체를 선정하고 있으며…"
배송형태선택 → options: ['직납', 'IMK물류(배송)', 'IMK물류(집배송)']

목표가 산정 (숨김 축, 기준가 설정과 무관)

인터넷최저가 8,500 / 매입가 7,200 / 판매가 9,800 · 수수료 7.8% · 네고율 12%

숨김 신규 재견적
없음 7,840 6,340
price 7,840 6,340
purchase_price 7,840 7,840 (가장 싼 후보가 빠짐)
둘 다 7,840 7,840

price 는 원래 목표가 후보가 아니라 숨겨도 무영향.

포털 API 실측 (GET /v1/negotiation/sessions/{id}/chat/init)

global 계정 로그인 후 4조합 확인 — 위 표의 포털 열이 그 결과. 회사 용어 12개, 배송형태 라벨(직납) 정상 전달.

자동 테스트

agent 176건 전부 통과. (테스트가 learning 스키마를 TRUNCATE 하므로 백업 후 실행·복원)


5. negodata / negosium 영향 — 문제 있는 곳

negodata (어드민)

화면 설정에 따라 달라지는 것 문제
회사 설정 협상 기준가 라디오·상품 필드 숨김·용어·포털 안내 탭 없음
상품 목록·등록·엑셀 양식 숨긴 필드가 세 곳에서 동시에 빠짐 없음
엑셀 업로드 숨긴 열은 파일에 있어도 읽지 않고 무시(양식 생성과 파싱이 같은 컬럼 정의 공유) 없음
견적 생성 목표가 후보에서 숨긴 가격 제외. 후보가 없으면 프론트가 생성 차단 없음
인터넷 최저가 검색 가격 힌트가 기준가 규칙을 따름 없음
통계 목표가·낙찰가·앵커가 기반이라 무관 없음

negosium (공급사 포털·챗)

화면 설정에 따라 달라지는 것 문제
협상 챗 멘트 "기존 OO 대비 N% 인하" 의 분모와 호칭 없음
협상 화면 상단 기준 단가 표시값 없음
협상 카드 멘트 회사 용어({label_supplier}·{label_target_price} 등) 치환 없음
배송형태 선택지 회사 용어로 치환(IMK: 직납·IMK물류) 없음
유의사항 팝업 회사 설정 항목, 미설정 시 기본 문구 없음
헬프데스크 회사 설정 연락처, 미등록 시 영역 숨김 없음
로그인 화면 로그인 전에도 회사 헬프데스크 노출 없음

안 바뀌는 곳

목표가 산정(기준가 설정과 무관) · 앵커링가 · 앵커링 학습(축 = 회사·협력사유형·가격대) · 통계 전부 · 진행 중인 협상(세션 시작 시 기준가를 박제하므로 라운드 중간에 안 바뀜).

유일하게 되돌릴 수 없는 것

RL 가격 수용률 — 학습 상태 인덱스에 들어가므로, 협상 이력이 쌓인 뒤 기준가를 바꾸면 Q테이블에 두 기준이 섞이고 설정을 되돌려도 복구되지 않는다. 온보딩 때 정한다.


6. 확인하지 않은 것

  • 설정 화면 렌더를 눈으로 보지 않았다. tsc·eslint 통과. 다만 관리자가 그 화면에서 저장한 값이 DB 에 정상 반영된 것으로 렌더·저장 경로는 실증됐다(features·hidden_fields·guide_notices).
  • negodata backend 테스트 미실행. 컨테이너에 pytest 미설치이고 negosium_test_db 가 없어 dev DB 를 truncate 할 위험이 있어 돌리지 않았다. 다만 이번에 바꾼 common/nego_baseline.py(신규)· lps_sync_service.py 를 호출하는 기존 테스트는 없다(test_scheduler.py 가 잡 이름만 확인).
  • global 계정 비밀번호를 dev DB 에서 1234 로 리셋했다.

오해였던 것 (기록)

  • 검증 중 learning 스키마 행이 늘어 "테스트 협상이 학습을 오염시켰다"고 봤으나, 실제로는 pytest 산출물이었다. 학습 데이터의 company_id 는 테스트마다 만든 랜덤 UUID 42개이고 IMK 스코프는 0건이다.
  • "IMK 매입가에 원가가 들어 있어 협상 멘트가 전부 인상으로 나온다"고 봤으나, 테스트 제시가를 매입가보다 높게 넣어서 생긴 착시였다. 매입가 이하를 제시하면 정상적으로 인하율이 나온다: 7,000 → 2.8% 인하 · 6,500 → 9.7% 인하 · 8,000 → 11.1% 높은.