종합 피드백에서 남긴 마지막 개선 3종: - --once --dry-run: 판정·예상 조정(조정예정 라인)·제외 예정 건수를 로그로만 보고 DB/Redis 를 일절 변경하지 않는 예행 연습 — 첫 운영 실행(레거시 전량 판정·마킹) 전에 규모를 눈으로 확인하는 안전장치. status=dry_run, 종료코드 0 - 박제 정합 감시: 스캔 시 정수식 tp×(1000−rate)//1000 과 박제 anchor 를 대조, 불일치 시 WARN + 요약에 snapshot_mismatch — negodata 이식 오류(float 잔재· 칸 해석 오류)를 적용 첫 주에 자동 감지. 전환기(rate 미박제)엔 자동 스킵 - 전환기 점프 절차화: 인수인계 적용 순서에 "negodata 적용 직전 current_rates 분포 확인 → 점프 감수/이력 리셋 정책 결정" 단계 삽입 + TODO 등재 테스트 2종 추가(dry-run 무변경·정합 WARN) — 모듈 20개·backend 57개 통과. 리허설 완료: dry-run 예상과 실제 실행 결과 일치 확인(제외 3·조정 1칸 10‰→30‰). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
77 lines
4.6 KiB
Markdown
77 lines
4.6 KiB
Markdown
# 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 --dry-run # 예행 연습(DB/Redis 무변경)
|
||
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` 하나뿐.
|