feat(anchoring): 조회용 뷰 2종 신설 — records 테이블은 검토 후 기각 (TODO 2)
요구(회사별 앵커링 값 업데이트 리스트업 + 이전 값 판별)는 rate_adjustments 한 행에 anchor_rate_before→after 가 박제되어 이미 충족 — 신규 테이블은 동일 정보의 사본만 만들므로 기각하고, 조회를 제품화하는 파생 뷰로 해결: - anchoring.rate_history: 값 변경 이력 리스트업(이전→새 값, delta_permille, success_rate, created_at) - anchoring.current_rates: 칸별 현재값(최신 조정 행 — 없는 칸 = 시작값 10‰) 뷰는 상태가 없어 오염·재구축 이슈 자체가 없고 append-only 보호 대상 아님. 통합 테스트에 뷰 검증 추가(이력 before/after·성공률, 현재값). 운영 문서 §8 쿼리를 뷰 기반으로 단순화, TODO 과제 2 종결(대시보드 페이징 요구 시 스냅샷 테이블 승격 재검토 명시). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e1940c69aa
commit
a132dac57a
@ -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.
|
||||
추후 대시보드에서 "전체 칸 나열(무조정 칸 포함)·페이징" 요구가 생기면 그때 스냅샷 테이블로 승격을 재검토한다.
|
||||
|
||||
@ -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 가 이미 생성 시 채우는 기존 컬럼 — 앵커가 박제로 그대로 활용(신규 컬럼 아님).
|
||||
|
||||
@ -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 = '<uuid>'
|
||||
ORDER BY id;
|
||||
ORDER BY adjustment_id;
|
||||
|
||||
-- ①-b 어떤 회사의 칸별 "현재값" 한눈에 (여기 없는 칸 = 시작값 1%)
|
||||
SELECT * FROM anchoring.current_rates
|
||||
WHERE company_id = '<uuid>';
|
||||
|
||||
-- ② 특정 조정(adj_id)의 근거가 된 협상들
|
||||
SELECT s.session_id, s.status, s.target_anchoring_price, s.last_offered_price, s.bid_price
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user