diff --git a/schedules/anchoring/TODO.md b/schedules/anchoring/TODO.md index 54451e8..4a5d876 100644 --- a/schedules/anchoring/TODO.md +++ b/schedules/anchoring/TODO.md @@ -14,30 +14,14 @@ 산식 = `bisect_right`. 조정 이력 0건 시점에 적용해 마이그레이션 없음. 상세: `docs/개발용.md` §2·§4.1. -## 2. `anchoring_records` — 회사별 앵커링 값 전체 조회 테이블 +## ~~2. `anchoring_records` — 회사별 앵커링 값 전체 조회 테이블~~ ✅ 뷰로 종결 (2026-07-02) -### 배경 +**신규 테이블 없이 조회용 뷰 2개로 해결.** 요구(회사별 값 업데이트 리스트업 + 이전 값 판별)는 +`rate_adjustments` 한 행에 before→after 가 박제되어 있어 이미 충족 — 테이블 추가는 사본만 만든다고 +판단해 기각하고, 조회를 제품화하는 뷰를 추가했다: -현재 `anchoring.rate_adjustments` 는 **"조정 사건" 로그**다 — 값이 바뀐 칸만 행이 생긴다. -"A사의 모든 칸(또는 운영 중 사용된 칸)의 앵커링 값이 지금/과거에 얼마였나"를 한 번에 -조회하려면, 조정 이력 + 정적 테이블 시작값을 합성해야 해서 조회(대시보드·관리 화면)에 -불편하다. 회사별로 운영되며 업데이트된 모든 앵커링 값을 바로 조회할 수 있는 테이블이 필요하다. +- `anchoring.rate_history` — 회사별 값 변경 이력(이전→새 값, 변화폭, 성공률, 시각) +- `anchoring.current_rates` — 칸별 현재값(없는 칸 = 시작값 10‰) -### 설계 시 결정해야 할 것 (구현이 크게 바뀔 수 있음) - -- [ ] **성격 확정**: ① 칸별 "현재값" 스냅샷(UPSERT — 조회 최적) ② 값 변경 시계열의 조회 - 친화 사본(append) ③ 둘 다 — 무엇이 필요한지 조회 요구사항(대시보드/API 화면)부터 정의 -- [ ] **rate_adjustments 와의 역할 분리**: records 는 **파생(조회용) 테이블**로 두고 - 진실 원천은 rate_adjustments 유지가 안전 — records 는 언제든 이력에서 재구축 가능해야 - 하며, append-only 보호 대상(§12)에는 포함하지 않는다 -- [ ] **쓰기 주체·시점**: 배치가 조정 트랜잭션에 함께 기록할지(정합 우선), 커밋 후 - best-effort 로 기록할지(조정 경로 무손대 우선 — 재구축 가능하므로 후자도 충분) -- [ ] **무조정 칸 표현**: 시작값(10‰) 그대로인 칸을 행으로 둘지(회사 온보딩 시 46행 선생성 — - 사다리 개편으로 부담 미미), 조회 시 정적 테이블과 합성할지 -- [x] ~~과제 1(구간 개편)과 순서 조율~~ — 구간 개편 완료(46칸 사다리), records 는 새 인덱스 기준으로 설계하면 됨 - -### 참고 - -- 임시로는 현행 SQL 조회로 동일 정보를 얻을 수 있다: `docs/운영및유지보수.md` §8 - (값 변천사 = ①, 현재값 = 칸별 최신 행 + 없으면 시작값 10‰) -- 스펙 반영 위치: `docs/개발용.md` §6(스키마)·§8(배치 절차에 기록 단계 추가) +상세: `docs/개발용.md` §6.3, 사용법: `docs/운영및유지보수.md` §8. +추후 대시보드에서 "전체 칸 나열(무조정 칸 포함)·페이징" 요구가 생기면 그때 스냅샷 테이블로 승격을 재검토한다. diff --git a/schedules/anchoring/docs/개발용.md b/schedules/anchoring/docs/개발용.md index 5000152..82c86e1 100644 --- a/schedules/anchoring/docs/개발용.md +++ b/schedules/anchoring/docs/개발용.md @@ -341,6 +341,18 @@ CREATE INDEX IF NOT EXISTS idx_sessions_anchoring_pending WHERE anchoring_adjustment_id IS NULL AND deleted = false; ``` +### 6.3 조회용 뷰 (파생 — 상태 없음) + +회사별 값 변경 추적·현재값 조회는 신규 테이블 없이 **뷰**로 제공한다(진실 원천은 rate_adjustments 그대로): + +```sql +anchoring.rate_history -- 값 변경 이력 리스트업: 이전 값(anchor_rate_before)→새 값 + delta_permille·success_rate·created_at +anchoring.current_rates -- 칸별 현재값(최신 조정 행). 여기 없는 칸의 현재값 = 정적 테이블 시작값(10‰) +``` + +- 뷰는 파생이므로 append-only 보호 대상(§12)이 아니며, 필요 시 자유롭게 재정의할 수 있다. +- "records 신규 테이블" 안은 검토 후 기각 — 요구(회사별 업데이트 이력 + 이전 값 판별)가 rate_adjustments 한 행(before→after 박제)으로 이미 충족되어, 테이블 추가는 동일 정보의 사본만 만든다. + 주의사항: - `sessions.target_anchoring_price`는 negodata 가 이미 생성 시 채우는 기존 컬럼 — 앵커가 박제로 그대로 활용(신규 컬럼 아님). diff --git a/schedules/anchoring/docs/운영및유지보수.md b/schedules/anchoring/docs/운영및유지보수.md index f4490b4..96887e8 100644 --- a/schedules/anchoring/docs/운영및유지보수.md +++ b/schedules/anchoring/docs/운영및유지보수.md @@ -190,12 +190,14 @@ docker logs anchoring | tail -20 # 최근 상태 로그는 로테이션되지만 **DB 이력은 영구**입니다. "왜 이 값이 됐는가"는 항상 DB로 답할 수 있습니다. ```sql --- ① 어떤 회사의 값 변천사 (시간순) -SELECT id, created_at, supplier_type, price_bracket_index, - nego_count, success_count, anchor_rate_before, anchor_rate_after -FROM anchoring.rate_adjustments +-- ① 어떤 회사의 값 변천사 (시간순) — 이전 값→새 값·변화폭·성공률까지 한 줄에 +SELECT * FROM anchoring.rate_history WHERE company_id = '' -ORDER BY id; +ORDER BY adjustment_id; + +-- ①-b 어떤 회사의 칸별 "현재값" 한눈에 (여기 없는 칸 = 시작값 1%) +SELECT * FROM anchoring.current_rates +WHERE company_id = ''; -- ② 특정 조정(adj_id)의 근거가 된 협상들 SELECT s.session_id, s.status, s.target_anchoring_price, s.last_offered_price, s.bid_price diff --git a/schedules/anchoring/schema.sql b/schedules/anchoring/schema.sql index 3d03020..f203e82 100644 --- a/schedules/anchoring/schema.sql +++ b/schedules/anchoring/schema.sql @@ -37,3 +37,34 @@ ALTER TABLE negotiation.sessions CREATE INDEX IF NOT EXISTS idx_sessions_anchoring_pending ON negotiation.sessions (qt_type, status) WHERE anchoring_adjustment_id IS NULL AND deleted = false; + +-- ============================================================ +-- 조회용 뷰 (파생 — 상태 없음, 진실 원천은 rate_adjustments) +-- ============================================================ + +-- 회사별 앵커링 값 변경 이력 리스트업: "언제, 어떤 칸이, 몇 건 중 몇 건 성공으로, 몇 ‰에서 몇 ‰로" +CREATE OR REPLACE VIEW anchoring.rate_history AS +SELECT id AS adjustment_id, + company_id, + supplier_type, -- 1유통/2제조/3총판 + price_bracket_index, -- 0..45 자릿수 사다리 + anchor_rate_before, -- 이전 값(‰) + anchor_rate_after, -- 새 값(‰) + anchor_rate_after - anchor_rate_before AS delta_permille, + nego_count, + success_count, + round(success_count::numeric / nego_count, 3) AS success_rate, + created_at +FROM anchoring.rate_adjustments; + +-- 칸별 현재값: 칸의 최신 조정 행. 여기 없는 칸의 현재값 = 정적 테이블 시작값(10‰) +CREATE OR REPLACE VIEW anchoring.current_rates AS +SELECT DISTINCT ON (company_id, supplier_type, price_bracket_index) + company_id, + supplier_type, + price_bracket_index, + anchor_rate_after AS anchor_rate_permille, + id AS last_adjustment_id, + created_at AS last_adjusted_at +FROM anchoring.rate_adjustments +ORDER BY company_id, supplier_type, price_bracket_index, id DESC; diff --git a/schedules/anchoring/tests/test_batch.py b/schedules/anchoring/tests/test_batch.py index 2d5bfcd..c47dccc 100644 --- a/schedules/anchoring/tests/test_batch.py +++ b/schedules/anchoring/tests/test_batch.py @@ -78,6 +78,18 @@ async def test_full_cycle_and_idempotency(seeder, caplog): async with adb.session_scope() as db: assert len(await _adjustments(db, seeder)) == 1 + # 조회용 뷰 — rate_history(이전→새 값 리스트업) / current_rates(칸별 현재값) + hist = (await db.execute(text( + "SELECT anchor_rate_before, anchor_rate_after, delta_permille, success_rate " + "FROM anchoring.rate_history WHERE company_id = :c"), {"c": seeder.company_id})).one() + assert (hist.anchor_rate_before, hist.anchor_rate_after, hist.delta_permille) == (10, 30, 20) + assert float(hist.success_rate) == 0.615 + cur = (await db.execute(text( + "SELECT anchor_rate_permille FROM anchoring.current_rates " + "WHERE company_id = :c AND supplier_type = 1 AND price_bracket_index = :b"), + {"c": seeder.company_id, "b": BRACKET})).scalar_one() + assert cur == 30 + # ── §11.5: 이월(7건 스킵 → 누적 13건 단일 평가) ────────── async def test_carryover(seeder):