"""협상지표(협상 성공률) 산출 — 3구간 모델. 선행 chat_server 의 ThreeSegIndicator 를 우리 구현으로 재작성(브랜드/특정사 비포함, CLEANROOM.md). 가격협상(카드선택) 턴에 화면 게이지로 표시할 값을 만든다. anchor < target 전제 (anchor=우선협상 기준가, target=목표 매입가). 구매자 관점이라 제시가가 낮을수록 지표가 높다. - PZ1: 제시가 ≤ anchor → 99 (우선협상 가능 구간) - PZ2: anchor < 제시가 < target → 99→80 선형 보간(19 스텝) - PZ3: 제시가 ≥ target → 79 에서 지수감쇠, 최소 1 반환: (indicator_value 1~99, indicator_range "PZ1"|"PZ2"|"PZ3") 또는 입력이 부적합하면 None. backend 컬럼이 NUMERIC(8,6)(절대값 <100)이라 값은 항상 1~99 범위로 보장한다. """ import math from typing import Optional, Tuple def compute_indicator(anchor_price, current_price, target_price) -> Optional[Tuple[int, str]]: try: anchor = float(anchor_price) current = float(current_price) target = float(target_price) except (TypeError, ValueError): return None if target <= 0 or current <= 0: return None # PZ1 — 우선협상 구간(제시가가 앵커가 이하) if current <= anchor: return 99, "PZ1" # PZ2 — 협상 가능 구간(앵커가 초과 ~ 목표가 미만): 99→80 선형 보간 if anchor < current < target: value = int(math.floor(99 - ((current - anchor) / (target - anchor) * 19))) return max(80, min(99, value)), "PZ2" # PZ3 — 목표가 이상: 79 에서 목표가 정규화 지수감쇠, 최소 1 scale_exponent = int(math.floor(math.log10(target))) k = math.pow(10, -scale_exponent) raw_score = 79 * math.exp(-k * (current - target)) return max(1, int(math.floor(raw_score))), "PZ3"