o2o-negosium-original/negodata/docs/imk-0803-requests.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

112 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# IMK 0803 가격협상 요청 정리
원본: `0803_가격협상 우선 적용 및 논의 정리.xlsx` (12건). 시트의 `우선/논의` 구분 대신
**스펙이 확정돼 바로 착수 가능한 것 / 결정이 있어야 착수 가능한 것**으로 다시 갈랐다.
시트와 다른 3건 — ⑪은 수정으로, ③은 논의로, ④는 코드가 아니라 회사설정으로.
---
## 수정 (스펙 확정)
### ④ 부가세 입력칸 — 코드 문제 아님
`vat_yn` 은 상품폼(`ProductFormSheet.tsx`)·업로드 양식(`ExcelUploadModal.tsx`) 둘 다 이미 있다.
IMK 회사설정이 `hidden_fields = [made_in, delivery_fee_yn, selling_price, vat_yn]` 라 화면·양식에서
같이 빠진 것. 설정에서 체크 해제하면 끝.
### ⑥ 유의사항 문구 / 헬프데스크
- 유의사항의 VAT·배송비 문구는 `GuideContent.tsx` 에서 상품별 동적 표기로 이미 바뀐 상태 → 요청대로 삭제.
- 헬프데스크 연락처 실사용처 3곳, 전부 placeholder였음:
`frontend/src/features/chat/components/menu/Contact.tsx`,
`frontend/src/features/chat/components/popup/GuideContent.tsx`,
`frontend/src/pages/LoginPage.tsx`. 초청 메일엔 없음.
- **반영**: `companies.settings.branding.helpdesk: string[]` 로 회사별 설정화. 아래 "헬프데스크 배선" 참조.
### ⑦ 공급가–매입가 일원화
목표가 후보엔 이미 공급가가 없다(internet·purchase·selling — `pricing.py`). 잔여 2건:
1. 화면의 공급가(`item.price`, IMK 라벨 "공급가") 노출 제거
2. 신규 견적이 아직 인터넷최저가만 씀 → 매입가 후보 추가
⚠️ **`items.price` 를 hidden 으로 감추면 안 된다.** `hidden_fields` 는 목록·등록폼·엑셀양식 3곳을
동시에 감추므로 신규 상품의 `price` 가 NULL 로 쌓이고, 협상 멘트의 인하율이 통째로 빠진다.
`items.price` 소비처 → `items-price-is-nego-baseline` 메모 참조.
### ⑫ 시장가 산식 `(상품가+배송비)/1.1`
인터넷 최저가는 몰 판매가(VAT 포함·배송비 별도)로 수집되는데 우리 매입가·공급사 견적가는 VAT 별도라
축이 어긋난 채 비교 중. 배송비를 더해 실구매 총액을 만들고 1.1로 나눠 VAT를 벗긴다(약 9% 낮아짐).
미결 2개:
- 배송비를 대부분 모른다(네이버 쇼핑 API 미제공, 실측 39/39 null). 0으로 칠지 / 미상이면 환산 스킵할지
- 적용 범위 — 카드 멘트 인용값만인지, 목표가 후보(`internet × (1수수료율)`)에도 거는지.
후자면 인터넷 기준 목표가가 9% 내려간다.
### ⑪ 인터넷최저가 ≥ 목표가 → 시장가 카드 차단 (절반 완료)
견적생성 시 선택 게이팅은 반영됨(`useCardGating.tsx`). 잔여 = 협상 진행 중 런타임 차단.
### ⑤ 상품 일괄등록 유효성 오류
재현 케이스(어느 필드가 오탐인지) 확보 후 수정. 엑셀 원본을 받는 게 빠름.
### ② 목표가 초과 낙찰 허용
상한 그릇은 들어감(`sessions.done_ceiling_price`, 커밋 91b8dc77).
**협상 엔진이 아직 안 읽는다**`agent/`·루트 `backend/` 어디에도 `done_ceiling` 참조 없음.
타결 판정에 배선하면 EST-202607-973E 케이스 해소.
조건: 기존 단가 > 견적가 > 목표가 > 앵커링가면 타결. 여기서 "기존 단가" = `items.price`.
---
## 논의 (결정 필요)
### ③ 중간값 로직
②의 짝. "목표가 초과 제시 안 함" 상한을 푸는 건 맞는데 **대신 무엇을 상한으로 쓸지**
`done_ceiling_price`(목표가×(1+율))인지 기존 단가인지.
IMK 예시(목표 39,800 / 필요 40,200)는 +1.0% 수준이고 현 기본율은 +5%.
### ⑧ 세팅 횟수만큼 카드 소진
목표가에 이미 근접했는데도 남은 카드를 다 태울지. 라운드가 늘면 결렬 위험·시간도 는다.
"최소 사용 횟수 보장" vs "조기 타결 우선" 중 택.
### ⑨ 앵커 제안가 단조성
후속 카드 제안가가 앞 카드보다 낮아지는 건(16,980 → 16,810) 카드마다 고정 인하율을 쓰기 때문.
제안가를 카드가 아니라 **라운드에 종속**시켜 단조 상향으로 바꾸는 구조 변경 → 이 중 유일하게 범위가 큼.
### ⑩ 와일드카드 + 최종제안 중복
종결 국면은 현재 **필수 관문**이다(`chat_engine.py` `check_iteration_limit`):
1. `closing_played` false → `force_closing` → 종결 전용 와일드카드 또는 폴백 최후통첩(목표가 제시)
2. `closing_played` true → 제시가 ≤ 목표가면 타결, 초과면 결렬
스킵하면 마지막 우리 카운터가 안 나가고 공급사 마지막 제시가로 즉시 판정 → **결렬률 상승**.
구현 시 `closing_played = True` 는 반드시 세워야 무한루프를 피한다.
단 중복 방지는 이미 양쪽에 걸려 있다 — 진입 단계(`chat_engine.py`)도 종결 단계(`chat_service.py`)도
`is_played` 로 쓴 카드를 건너뛴다. 코드상 같은 카드가 2회 나올 수 없으므로 IMK가 본 것이
진짜 동일 카드인지 확인이 먼저(dev DB엔 EST-202607-5947 없음 — 운영 데이터).
### ⑬ SG별 앵커링
**SG = `items.category`** (IMK 라벨 "SG명"). 현재 SG는 앵커링에도 목표가 산정에도 안 들어간다.
앵커링 축은 3개 — `(company_id, supplier_type, price_range_index)`:
| 축 | 값 |
|---|---|
| `company_id` | 회사 |
| `supplier_type` | 1 유통 / 2 제조 / 3 총판 — 개별 협력사가 아니라 **유형** |
| `price_range_index` | 목표가 기준 46개 자릿수 구간 |
SG 축을 추가하면 셀이 46×3×SG수로 쪼개져 셀당 표본 10건(`SAMPLE_THRESHOLD`)을 못 채우고
평가가 스킵·이월되어 **학습이 사실상 멈춘다**. 이게 실제 결정 포인트.
---
## 헬프데스크 배선 (⑥ 반영분)
`companies.settings.branding.helpdesk: string[]` — 한 줄 = 담당자 한 명, 자유 문자열.
| 경로 | 파일 |
|---|---|
| 편집 UI | `negodata/front/src/features/settings/SettingsView.tsx` (브랜딩 탭) |
| 타입 | `negodata/front/src/features/settings/catalog.ts` |
| 로그인 후 전달 | `backend/services/auth_service.py` `me()``branding` dict 통째로 내려 자동 포함 |
| 로그인 전 전달 | `backend/services/auth_service.py` `session_branding()` + `protocol.py` `Res_SessionBranding` |
| 표시 | `frontend/``Contact.tsx` · `GuideContent.tsx` · `LoginPage.tsx` |
값이 비면 각 표시부는 연락처 줄을 렌더하지 않는다(placeholder 노출 금지).