o2o-negosium-original/schedules/anchoring
민헌 c0d6723fe4 feat(anchoring): 설정 파일 환경 분리 — APP_ENV 로 config.{local,dev,prod}.toml 선택
- config.py: APP_ENV(기본 local) 기반 config.{APP_ENV}.toml 로드 — backend/negodata 와 동일 관례
- dev/prod 는 파일 부재 시 기동 즉시 중단(FileNotFoundError) — 오타·미배치 상태로
  코드 기본값(로컬 DB)에 붙는 무증상 사고 방지. local 만 파일 없이 기본값 허용(개발 편의)
- main 기동 로그에 APP_ENV·설정 파일 명시
- 루트 compose: anchoring 서비스에 APP_ENV=local + config.local.toml 마운트
- config.toml.example 을 3환경 공용 템플릿으로 갱신, .gitignore 에 config.*.toml
  (env별 실파일은 gitignore — dev/prod 는 배포 시 CHANGE_ME 채움)
- TODO.md 삭제(주요 과제 전부 종결) + README·개발용.md 참조 정리
- 검증: 모듈 20 테스트 통과, APP_ENV=staging fail-fast(exit 1)·local dry-run 기동 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:41:42 +09:00
..
docs feat(anchoring): 설정 파일 환경 분리 — APP_ENV 로 config.{local,dev,prod}.toml 선택 2026-07-06 11:41:42 +09:00
migrations refactor(anchoring): DDL·compose 루트 통합 — postgres-init/05 신설, 모듈 schema.sql·docker-compose 제거 2026-07-06 11:37:13 +09:00
src/anchoring feat(anchoring): 설정 파일 환경 분리 — APP_ENV 로 config.{local,dev,prod}.toml 선택 2026-07-06 11:41:42 +09:00
tests refactor(anchoring): DDL·compose 루트 통합 — postgres-init/05 신설, 모듈 schema.sql·docker-compose 제거 2026-07-06 11:37:13 +09:00
.dockerignore fix(anchoring): 이미지에 config.toml 미포함 — 런타임 마운트 전환·.dockerignore 신설 2026-07-06 11:16:45 +09:00
.gitignore feat(anchoring): 설정 파일 환경 분리 — APP_ENV 로 config.{local,dev,prod}.toml 선택 2026-07-06 11:41:42 +09:00
config.toml.example feat(anchoring): 설정 파일 환경 분리 — APP_ENV 로 config.{local,dev,prod}.toml 선택 2026-07-06 11:41:42 +09:00
Dockerfile feat(anchoring): 설정 파일 환경 분리 — APP_ENV 로 config.{local,dev,prod}.toml 선택 2026-07-06 11:41:42 +09:00
pytest.ini feat(anchoring): 앵커링 값 자동 조정 자립 모듈 신설 (v1.2) 2026-07-02 16:29:46 +09:00
README.md feat(anchoring): 설정 파일 환경 분리 — APP_ENV 로 config.{local,dev,prod}.toml 선택 2026-07-06 11:41:42 +09:00
requirements.txt feat(anchoring): 운영 로그 개선 — run_id·칸별 조정 상세·회사별 요약 2026-07-02 16:46:16 +09:00

anchoring — 앵커링 값 자동 조정 배치 (자립 모듈)

회사 × 협력사유형(1유통/2제조/3총판) × 가격구간(자릿수 계단식 사다리, 46칸 — 예: 3만 원대)별 앵커링 값(‰)을 격주 토 00:00 KST 배치로 협상 성공률에 따라 자동 조정한다. schedules/anchoring 아래에서 완전 독립으로 동작 — backend 코드를 import 하지 않는다.

규범 문서: docs/개발용.md (정책: docs/기획용.md, 흐름 해설: docs/워크플로우.md, 타 팀 적용: docs/인수인계.md) 처음 오신 분 / 운영 담당자 → docs/운영및유지보수.md 부터 보세요 (설치·실행·로그 읽기·트러블슈팅).

경계

구분 대상
소유(쓰기) anchoring.adjustments(append-only 조정 이력), sessions.used_by_adjustment_id(소비 마킹 — 이 컬럼만), Redis anchor:* 키
읽기 전용 negotiation.sessions(박제 컬럼), quotation.quotations.supplier_type, partner.items.company_id
소비자 negodata 가 reader 이식판으로 세션 생성 시 앵커가 박제 — 적용 완료(2026-07-04), 이식판은 Redis 미사용(current_values 뷰 직조회, 인수인계 §1)

구조

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           # 현재 anchoring_value 조회: 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 없으면 자동 스킵)
migrations/           # 기존 DB 이름 개편용 rename 마이그레이션 (DDL 자체는 postgres-init/05)

실행

# 0) DDL 적용 — 루트 postgres-init 로 이관됨 (신규 DB: 01~04 이후 05)
psql -h 127.0.0.1 -U postgres -d negosium_db -f ../../postgres-init/05-anchoring-schema.sql

# 로컬(가상환경)
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp config.toml.example config.local.toml     # DB/Redis 채우기 (APP_ENV 로 local/dev/prod 선택, 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          # 스케줄러 상주

# 도커 — 루트 docker-compose.yml 에 통합됨(anchoring + anchoring-redis). config.local.toml 은
# 이미지에 안 들어가고 마운트되므로 up 전에 파일이 먼저 있어야 한다 (레포 루트에서 실행)
(cd ../.. && docker compose up -d --build anchoring anchoring-redis)

# 테스트
PYTHONPATH=src .venv/bin/python -m pytest tests/ -q

로그 확인

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.adjustments 행과 교차 확인한다.
  • 칸 실패가 있으면 종료 요약이 WARNING 으로 승격된다 — "WARN 이상 알람" 룰에 걸린다.
  • 로그 로테이션은 루트 compose 에 설정됨(10MB × 5). 영구 감사 추적은 로그가 아니라 DB(조정 이력 ↔ 세션 마킹)가 담당.

운영 런북

  • 미스파이어: 토 00:00 에 서비스가 내려가 있었고 1시간(misfire_grace) 초과로 그 회차가 스킵됐다면, 재기동 후 --once 1회 실행으로 즉시 캐치업(격주 게이트만 무시, 정책 파라미터 불변).
  • Redis 유실/재기동: 캐시는 파생값 — 매 실행(매주, 게이트 무관) 시작 시 조정 보유 칸 전체를 re-SET 하고 TTL 7일이 보조하므로 자가 회복된다. 수동 복구가 필요하면 --once.
  • 가격 제시율 0% WARN: backend 의 last_offer_price 기록 배선 유실 신호(학습 무증상 동결) — 즉시 점검.
  • 박제 정합 불일치 WARN: negodata 의 앵커 산출 이식 오류 의심(정수식 ≠ 박제 anchor) — docs/인수인계.md §1.3 점검 요청.
  • 조정 이력은 append-only — UPDATE/DELETE 금지. 배치가 sessions 에 쓰는 컬럼은 used_by_adjustment_id 하나뿐.