어느 가격이 "공급사에 지불하는 단가"인지는 회사마다 달라, 컬럼을 합치는 대신 회사가 고르게 했다.
판정은 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
252 lines
15 KiB
Markdown
252 lines
15 KiB
Markdown
# 협상 기준가 회사별 선택 — 변경 내역과 검증 보고서
|
||
|
||
작성 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% 높은`.
|