o2o-negosium-original/schedules/anchoring/README.md
민헌 ba996f47c1 fix(anchoring): 3방향 적대 리뷰 반영 — 테스트 격리·Redis 방어·운영 견고화
모듈·backend·문서 3개 관점의 적대 리뷰에서 확인된 결함 일괄 수정:

[모듈]
- 배치에 company_ids 스코프 옵션 추가 — 통합 테스트가 공유 dev DB 의 실세션을
  소비/마킹하던 문제 해소(테스트는 시드 회사로 한정), 표적 수동 실행 옵션 겸용
- Redis 방어: compose 포트를 127.0.0.1 바인딩(무인증 공개 차단), get_rate 에
  범위([10,200]) 검증 — 오염 캐시값은 미스 취급 후 자가 교정, 미스 백필은 SET NX
  (배치가 방금 쓴 새 값을 구값으로 덮는 write-after-read 경합 방지)
- 배치: Redis ping 후 re-SET(다운 시 셀×timeout 지연 없이 즉시 스킵), 스캔 조인
  ON 절에 quotations/items deleted 필터(철회 거래를 학습에서 배제), 제외 마킹을
  청크별 커밋(레거시 대량 첫 실행의 장시간 단일 트랜잭션 방지)
- main: SIGTERM/SIGINT 핸들러(docker stop 시 정리 로직 보장), --once 부분 실패 시
  종료코드 1(런북/cron 감지 가능)

[backend]
- finalize_session·update_last_offered_price 에 status=IN_PROGRESS 가드 —
  negodata 일괄마감/중복 전송 경합이 종료된 세션을 되살리거나 가격 흔적을
  사후 변경하는 것 차단(파생 판정 결정성 보호)
- 신규 DB 부트스트랩: sessions 3컬럼을 postgres-init/01-schema·04-alter 에도
  반영(backend 가 모듈 DDL 없이 기동) — anchoring 스키마 자체는 모듈 소유 유지
- 낡은 주석 정리(agent_client·quotation_settings 의 구 앵커 산출 서술)

[테스트·문서]
- 신규 테스트: 격주 게이트 골든(ISO 주차), supplier_type NULL, 가격 제시율 0%
  WARN — 모듈 18개·backend 57개 통과
- 문서 정합 감사 20건 반영: 잔존 33,334/노출 문구 제거, §10 SQL 을 실제 코드
  (LEFT JOIN+deleted)와 일치, §11 자동/수동 검증 구분, FastAPI 오기 제거,
  인수인계 reader 시그니처(db 인자), TODO 백로그 5건 기록

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:55:46 +09:00

76 lines
4.5 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.

# anchoring — 앵커링 값 자동 조정 배치 (자립 모듈)
회사 × 협력사유형(1유통/2제조/3총판) × 가격구간(자릿수 계단식 사다리, 46칸 — 예: 3만 원대)별 앵커링 값(‰)을
**격주 토 00:00 KST** 배치로 협상 성공률에 따라 자동 조정한다.
`schedules/anchoring` 아래에서 **완전 독립**으로 동작 — backend 코드를 import 하지 않는다.
> 규범 문서: **`docs/개발용.md`** (정책: `docs/기획용.md`, 흐름 해설: `docs/워크플로우.md`, 타 팀 적용: `docs/인수인계.md`)
> **처음 오신 분 / 운영 담당자** → **`docs/운영및유지보수.md`** 부터 보세요 (설치·실행·로그 읽기·트러블슈팅).
> 과제 이력·백로그 → **`TODO.md`** (주요 과제는 전부 종결)
## 경계
| 구분 | 대상 |
|---|---|
| 소유(쓰기) | `anchoring.rate_adjustments`(append-only 조정 이력), `sessions.anchoring_adjustment_id`(소비 마킹 — 이 컬럼만), Redis `anchor:*` 키 |
| 읽기 전용 | `negotiation.sessions`(박제 컬럼), `quotation.quotations.supplier_type`, `partner.items.company_id` |
| 소비자 | negodata 가 `reader.get_anchor_rate` 이식 + 이 Redis 를 참조해 세션 생성 시 앵커가 박제 (인수인계) |
## 구조
```
schema.sql # 모듈 소유 DDL (rate_adjustments + sessions 3컬럼) — psql 수동 적용
src/anchoring/
constants.py # 상수·enum (δ={1:20, 2:10, 3:15} — 제조/총판 스왑 주의)
resources/anchoring_base.json # 정적 기본 테이블(46칸 사다리, 전부 10‰) — 불변, 시작값의 유일한 소스
base_table.py # 로드+검증(실패 시 기동 중단)
service.py # 순수 계산 (구간·앵커가·판정·평가) — negodata 이식 대상
reader.py # 현재 rate 조회: Redis → 조정 이력 → 정적 테이블 — negodata 이식 대상
redis_client.py # TTL 7일, socket timeout 0.3s, 장애 시 DB 폴백
batch.py # 격주 평가: 캐시 re-SET → 스캔·파생 판정 → 조정 INSERT+마킹(한 트랜잭션, rowcount 롤백)
scheduler.py # 매주 토 00:00 트리거 (격주 게이트는 잡 내부 ISO 주차 홀짝)
main.py # 엔트리 (상주 / --once)
tests/ # 골든 벡터(test_core) + DB 통합(test_batch — 로컬 Postgres 없으면 자동 스킵)
```
## 실행
```bash
# 0) DDL 적용 (신규 DB: postgres-init/01~04 이후)
psql -h 127.0.0.1 -U postgres -d negosium_db -f schema.sql
# 로컬(가상환경)
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp config.toml.example config.toml # DB/Redis 채우기 (env 로 대체 가능)
PYTHONPATH=src .venv/bin/python -m anchoring.main --once # 수동 1회(격주 게이트 무시)
PYTHONPATH=src .venv/bin/python -m anchoring.main # 스케줄러 상주
# 도커(자립 compose: redis 동봉)
docker compose up -d --build
# 테스트
PYTHONPATH=src .venv/bin/python -m pytest tests/ -q
```
## 로그 확인
```bash
docker logs anchoring | grep "batch 20260705" # 특정 회차 전체
docker logs anchoring | grep "company=<uuid>" # 특정 회사만 (조정·회사요약 라인)
docker logs anchoring | grep -E "WARNING|ERROR" # 이상 신호만
```
- 타임스탬프는 항상 KST. 회차마다 `조정 company=... n=13 성공=8 10‰→30‰ adj_id=26`(칸별 상세)과
`회사요약 company=...`(테넌트별 집계) 라인이 남고, `adj_id` 로 `anchoring.rate_adjustments` 행과 교차 확인한다.
- 칸 실패가 있으면 종료 요약이 WARNING 으로 승격된다 — "WARN 이상 알람" 룰에 걸린다.
- 로그 로테이션은 compose 에 설정됨(10MB × 5). 영구 감사 추적은 로그가 아니라 DB(조정 이력 ↔ 세션 마킹)가 담당.
## 운영 런북
- **미스파이어**: 토 00:00 에 서비스가 내려가 있었고 1시간(misfire_grace) 초과로 그 회차가 스킵됐다면,
재기동 후 `--once` 1회 실행으로 즉시 캐치업(격주 게이트만 무시, 정책 파라미터 불변).
- **Redis 유실/재기동**: 캐시는 파생값 — 매 실행(매주, 게이트 무관) 시작 시 조정 보유 칸 전체를 re-SET 하고
TTL 7일이 보조하므로 자가 회복된다. 수동 복구가 필요하면 `--once`.
- **가격 제시율 0% WARN**: backend 의 `last_offered_price` 기록 배선 유실 신호(학습 무증상 동결) — 즉시 점검.
- 조정 이력은 append-only — UPDATE/DELETE 금지. 배치가 sessions 에 쓰는 컬럼은 `anchoring_adjustment_id` 하나뿐.