용어 체계: 값=anchoring_value(정수‰)·가격=anchoring_price·조정=adjustment·구간=price_range·표본=sample - DB: rate_adjustments→anchoring.adjustments (id→adjustment_id, price_bracket_index→price_range_index, nego_count→sample_count, anchor_rate_before/after→anchoring_value_before/after, consumed_session_ids→used_session_ids) - sessions: target_anchoring_price→anchoring_price, anchor_rate_permille→anchoring_value, last_offered_price→last_offer_price, anchoring_adjustment_id→used_by_adjustment_id - 뷰: rate_history/current_rates→value_history/current_values, delta_permille→value_change - 코드: calc_price_range_index·calc_anchoring_price·evaluate_samples·get_current_value· get_latest_adjusted_value·get_current_anchoring_value·fetch_current_values·get_base_anchoring_value· Adjustment(ORM)·update_last_offer_price, 상수 ANCHORING_VALUE_MIN/MAX·ADJUSTMENT_STEP· PRICE_RANGE_COUNT/INDEX_MAX, 배치 로그 키 bracket=→price_range= - API: negodata protocol 필드 target_anchoring_price→anchoring_price (front 생성 모델·컴포넌트 동반) - 기존 DB 마이그레이션 신설: schedules/anchoring/migrations/20260706_rename_anchoring.sql (멱등 DO 블록 — 테이블·컬럼·뷰·인덱스·PK 제약. 코드 배포와 동시 적용 필요) - postgres-init 01·04, 문서 6종 동기화 - 실배포 전 수정 포함: main.py argparse 화(--dry-run 단독·오타 플래그 기동 전 차단), 박제 정합식 calc_anchoring_price 재사용, clamped 지표가 실제 포화만 집계(경계값 유지 제외) 주의: sessions.anchoring_value(정수‰)와 quotation_settings.anchoring_value(구 float 비율)는 같은 이름·다른 단위 — 구 컬럼은 미변경. 검증: 모듈 20·negodata 50·backend 57 테스트 통과, front tsc·vite build 통과, 로컬 DB 마이그레이션 적용 후 배치 dry-run·상주 기동·양 서버 부팅 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
56 lines
2.7 KiB
Python
56 lines
2.7 KiB
Python
"""현재 앵커링 값 조회(읽기 경로) — negodata 이식판.
|
|
|
|
원본 reader(schedules/anchoring)는 Redis 캐시를 먼저 보지만, 이식판은 DB 직조회 한 문장만 쓴다
|
|
(2026-07-03 단순화 결정 — 조회가 견적 생성 시에만 일어나 캐시가 불필요, Redis 의존 제거).
|
|
칸별 최신 조정 rate 는 모듈 소유 뷰 `anchoring.current_values` 가 제공하고,
|
|
조정 이력이 없는 칸은 결과에 없으므로 호출측이 정적 테이블 시작값으로 폴백한다.
|
|
|
|
견적 생성이 앵커 조회 때문에 실패해서는 안 된다(인수인계.md §1 규칙 6) — anchoring 스키마
|
|
미적용 환경을 포함해 어떤 실패도 밖으로 던지지 않고 빈 결과(전량 정적 폴백) + WARN 으로 처리한다.
|
|
"""
|
|
from sqlalchemy import column, select, table
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from common.anchoring.constants import ANCHORING_VALUE_MAX, ANCHORING_VALUE_MIN, SAMPLEABLE_SUPPLIER_TYPES
|
|
from common.logger import LOG
|
|
|
|
# 모듈 소유 DDL(schedules/anchoring/schema.sql)의 조회용 뷰 — negodata 는 ORM 모델 없이 읽기만 한다.
|
|
_current_values = table(
|
|
"current_values",
|
|
column("company_id"),
|
|
column("price_range_index"),
|
|
column("anchoring_value"),
|
|
column("supplier_type"),
|
|
schema="anchoring",
|
|
)
|
|
|
|
|
|
async def fetch_current_values(db: AsyncSession, company_ids: list, supplier_type: int) -> dict:
|
|
"""칸별 현재 앵커링 값 일괄 조회. {(company_id, price_range_index): rate‰} 반환.
|
|
|
|
supplier_type 은 견적 단위로 하나뿐이라 키에 넣지 않는다.
|
|
조정 이력이 없는 칸은 결과에 없다(호출측 정적 폴백). 조회 실패 시 빈 dict."""
|
|
if not company_ids or supplier_type not in SAMPLEABLE_SUPPLIER_TYPES:
|
|
return {}
|
|
try:
|
|
stmt = select(
|
|
_current_values.c.company_id,
|
|
_current_values.c.price_range_index,
|
|
_current_values.c.anchoring_value,
|
|
).where(
|
|
_current_values.c.company_id.in_(company_ids),
|
|
_current_values.c.supplier_type == supplier_type,
|
|
)
|
|
rows = (await db.execute(stmt)).all()
|
|
except Exception as ex:
|
|
LOG.w(f"[앵커링] current_values 조회 실패 — 전량 정적 테이블 폴백: {ex}")
|
|
return {}
|
|
|
|
out = {}
|
|
for company_id, price_range_index, rate in rows:
|
|
if not ANCHORING_VALUE_MIN <= rate <= ANCHORING_VALUE_MAX: # 범위 밖 값은 오염 방어 — 버리고 정적 폴백
|
|
LOG.w(f"[앵커링] rate 범위 밖 — 무시(정적 폴백): company={company_id} bracket={price_range_index} rate={rate}")
|
|
continue
|
|
out[(company_id, price_range_index)] = rate
|
|
return out
|