매주 같은 시간에 큐레이터가 한 번 더 똑똑해지는 컨셉으로 - 회고 컨텍스트(weekly_review + 자동 채점 잡) - 4계층 위계(코어/보너스/확장/풀, 5~20세트) - 결정 카드 단일 화면(브리핑 탭 재구성) - 분석 탭은 자료실로 강등 - 월요일 09:00 큐레이션 + 텔레그램 푸시
359 lines
17 KiB
Markdown
359 lines
17 KiB
Markdown
# Lotto Curator Evolution — Design Spec
|
||
|
||
- 일자: 2026-05-11
|
||
- 범위: `web-ui` (브리핑 탭 재구성), `web-backend/lotto` (스키마·잡), `web-backend/agent-office` (큐레이터·텔레그램)
|
||
- 컨셉 한 줄: **매주 같은 시간에 큐레이터가 한 번 더 똑똑해진다**
|
||
|
||
## 1. 동기와 문제
|
||
|
||
현재 `/lotto`는 3탭(브리핑·분석·구매)으로 구성되어 정보가 풍부하지만, 사용자가 5천~1만원 어치를 즐기며 구매하기에 다음 페인이 있다.
|
||
|
||
- 분석·통계·브리핑이 모두 *결정용 화면*처럼 노출되어 정보 과다.
|
||
- 큐레이터가 매주 5세트를 추천하지만, 5세트의 *역할*과 *왜 이 분배인지*가 와닿지 않는다.
|
||
- 큐레이터·시스템에 시간축이 없다. 매주 동일 알고리즘을 새로 도는 느낌.
|
||
- 1만원어치 구매 시 5세트로는 부족하다. 추가 게임에 대한 설계가 없다.
|
||
|
||
## 2. 컨셉
|
||
|
||
다음 두 축으로 강화한다.
|
||
|
||
- **서사적 진화**: 큐레이터가 매주 *지난 주를 회고*하고 이번 주 전략으로 이어간다. 자기 추천 결과 + 사용자 실제 구매 결과를 둘 다 회고 데이터로 사용한다.
|
||
- **포트폴리오 명료성**: 5게임이 단순 5장이 아니라 안정/균형/공격 분배가 그 주 데이터에 따라 동적으로 바뀌고, 그 이유가 한 줄로 와닿는다. 5~20세트로 위계적으로 확장된다.
|
||
|
||
## 3. 주간 사이클
|
||
|
||
```
|
||
토 20:35 추첨
|
||
│
|
||
일 03:00 추첨결과 sync (기존)
|
||
↓
|
||
채점 잡 (신규) → weekly_review INSERT
|
||
lotto_purchase auto_graded UPDATE
|
||
│
|
||
월 09:00 큐레이션 트리거 (lotto_agent.on_schedule)
|
||
├─ build_retrospective(target_draw)
|
||
├─ collect_candidates(N=30)
|
||
├─ build_context (+retrospective)
|
||
├─ Claude 호출 (회고+계층 규칙)
|
||
└─ briefings INSERT (4계층 picks)
|
||
│
|
||
월 09:05 텔레그램 헤드라인 푸시
|
||
│
|
||
월~토 사용자: 사이트 결정 카드 → 모드 선택(5/10/15/20) → 1탭 구매 기록
|
||
│
|
||
토 20:35 추첨 → 다음 사이클
|
||
```
|
||
|
||
cron 시간(일 03:00 / 월 09:00)은 운영하며 조정 가능한 기본값.
|
||
|
||
## 4. 결정 카드 (브리핑 탭 메인)
|
||
|
||
브리핑 탭을 단일 `DecisionCard`로 재구성한다. 정보 위계는 위→아래로:
|
||
|
||
1. **헤더** — 회차 + 한 줄 헤드라인 + 신뢰도(0~100, 큐레이터 자기 평가)
|
||
2. **회고 박스** (▸ 보라색 라벨) — 지난 주 너 + 큐레이터 한 줄 회고. *시간축*의 핵심.
|
||
3. **헤드라인 + 3줄** — 이번 주 전망 + 근거 3줄(기존 narrative 유지).
|
||
4. **분배 칩** — 선택 모드까지의 안정/균형/공격 합산 + "왜 이 분배인지" 한 줄.
|
||
5. **모드 토글** — 4단계 칩(코어 5 / +보너스 5 / +확장 5 / +풀 5).
|
||
6. **계층 섹션 × 4** — 각 계층마다 타이틀 + 사유 한 줄 + 5장 PickCard. 코어는 항상 펼침, 그 외는 모드에 따라.
|
||
7. **하단 액션** — "이대로 N세트 구매했음" 한 클릭 → 자동 기록.
|
||
|
||
### 4계층 위계
|
||
|
||
| 계층 | 누적 게임 | 비용 | 큐레이터의 의도 |
|
||
|---|---|---|---|
|
||
| 코어(필수) | 5 | 5천 | 안정 2 / 균형 2 / 공격 1, 그 주 주축 |
|
||
| + 보너스 | 10 | 1만 | 코어 분배의 공백 보완 |
|
||
| + 확장 | 15 | 1.5만 | 코어·보너스에 없던 시각(합계 극단·콜드 누적·4주 미등장) |
|
||
| + 풀 | 20 | 2만 | 한 번도 누르지 않은 패턴(연속·동끝·5수 균등) |
|
||
|
||
각 5세트는 *큐레이터가 의도한 한 묶음*이며, 늘어날수록 *서사가 더해지는 구조*. 마지막 모드 선택은 브라우저 `localStorage` 에 `lotto.tier_mode` 키로 저장하여 다음 주 진입 시 디폴트로 사용한다(서버 저장 X — 사용자 디바이스 단위 기억).
|
||
|
||
### 분석 탭은 "Deep Dive" 자료실로 강등
|
||
|
||
- 라벨 변경: `📊 분석·통계` → `📚 자료실 / Deep Dive`
|
||
- 첫 진입 시 모든 패널 접힘
|
||
- 기존 패널 모두 보존 (CombinedRecommendPanel, ReportPanel, 시뮬레이션, 통계, 빈도, PersonalAnalysisPanel, 수동 추천, 히스토리)
|
||
- PerformanceBanner는 결정 카드 헤더와 역할 중복 없도록 자료실에만 둠
|
||
|
||
## 5. 데이터 모델
|
||
|
||
### 신규 테이블 — `weekly_review`
|
||
|
||
```sql
|
||
CREATE TABLE weekly_review (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
draw_no INTEGER NOT NULL UNIQUE,
|
||
|
||
-- 큐레이터 자기 평가 (briefings.picks vs 추첨)
|
||
curator_avg_match REAL,
|
||
curator_best_tier TEXT, -- 안정 | 균형 | 공격
|
||
curator_best_match INTEGER,
|
||
curator_5plus_prizes INTEGER, -- 3개↑ 일치 카운트(5등 이상)
|
||
|
||
-- 사용자 구매 평가 (lotto_purchase vs 추첨)
|
||
user_avg_match REAL,
|
||
user_best_match INTEGER,
|
||
user_5plus_prizes INTEGER,
|
||
|
||
-- 패턴 갭 (서사 재료)
|
||
user_pattern_summary TEXT,
|
||
draw_pattern_summary TEXT,
|
||
pattern_delta TEXT, -- "너 저번호 편향 +1.2 / 합계 -18"
|
||
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
```
|
||
|
||
### `lotto_purchase` 컬럼 추가
|
||
|
||
```sql
|
||
ALTER TABLE lotto_purchase ADD COLUMN numbers TEXT; -- JSON [3,11,17,25,33,41]
|
||
ALTER TABLE lotto_purchase ADD COLUMN match_count INTEGER;
|
||
ALTER TABLE lotto_purchase ADD COLUMN auto_graded INTEGER DEFAULT 0;
|
||
ALTER TABLE lotto_purchase ADD COLUMN curator_tier TEXT; -- core | bonus | extended | pool
|
||
ALTER TABLE lotto_purchase ADD COLUMN curator_role TEXT; -- 안정 | 균형 | 공격
|
||
```
|
||
|
||
### `briefings.picks` 구조 변경
|
||
|
||
JSON 컬럼을 4계층 구조로 마이그레이션:
|
||
|
||
```json
|
||
{
|
||
"core": [/* 5세트 */],
|
||
"bonus": [/* 5세트 */],
|
||
"extended": [/* 5세트 */],
|
||
"pool": [/* 5세트 */]
|
||
}
|
||
```
|
||
|
||
기존 단일 배열 데이터는 `core` 키에만 매핑하고 나머지 키는 빈 배열로 채우는 1회 마이그레이션 스크립트.
|
||
|
||
## 6. 큐레이터 변경
|
||
|
||
### 출력 스키마 (`agent-office/curator/schema.py`)
|
||
|
||
```python
|
||
class CuratorOutput(BaseModel):
|
||
core_picks: List[Pick] = Field(min_length=5, max_length=5)
|
||
bonus_picks: List[Pick] = Field(min_length=5, max_length=5)
|
||
extended_picks: List[Pick] = Field(min_length=5, max_length=5)
|
||
pool_picks: List[Pick] = Field(min_length=5, max_length=5)
|
||
tier_rationale: TierRationale # bonus / extended / pool 각 30자 이내
|
||
narrative: Narrative # retrospective(60자 이내) 필드 추가
|
||
confidence: int # 0~100
|
||
```
|
||
|
||
### SYSTEM_PROMPT 추가 규칙
|
||
|
||
```
|
||
회고 규칙:
|
||
- context.retrospective 가 있으면 narrative.retrospective 에 한 줄(60자 이내).
|
||
- 큐레이터 자기 결과(curator_avg, best_tier) + 사용자 결과(user_avg, pattern_delta) 둘 다 짚을 것.
|
||
- 이번 주 코어 분배는 회고에 근거해 조정. 사유는 narrative.headline 에 한 줄로.
|
||
|
||
계층별 큐레이션 규칙:
|
||
- core_picks (5): 안정 2 / 균형 2 / 공격 1. 그 주 주축.
|
||
- bonus_picks (5): 코어 분배의 공백을 메움. 코어와 상보적.
|
||
- extended_picks (5): 코어·보너스에 없는 시각(합계 극단 / 콜드 누적 / 4주 미등장).
|
||
- pool_picks (5): 이번 주 한 번도 누르지 않은 패턴(연속·동끝·5수 균등).
|
||
- tier_rationale 의 3개 키(bonus·extended·pool)에 각각 30자 이내 사유.
|
||
- 후보에 없는 번호 조합은 절대 사용 금지(기존 규칙 유지).
|
||
```
|
||
|
||
### 회고 컨텍스트 — `agent-office/curator/retrospective.py` (신규)
|
||
|
||
```python
|
||
def build_retrospective(target_draw_no: int) -> dict | None:
|
||
last = lotto_get_review(target_draw_no - 1)
|
||
prev3 = lotto_get_reviews(target_draw_no - 4, target_draw_no - 2)
|
||
if not last:
|
||
return None
|
||
return {
|
||
"last_draw": {
|
||
"draw_no": last["draw_no"],
|
||
"curator_avg": last["curator_avg_match"],
|
||
"curator_best_tier": last["curator_best_tier"],
|
||
"user_avg": last["user_avg_match"],
|
||
"user_5plus": last["user_5plus_prizes"],
|
||
"pattern_delta": last["pattern_delta"],
|
||
},
|
||
"trend_4w": {
|
||
"curator_avg_4w": mean(curator_avg_match for r in [last, *prev3]),
|
||
"user_avg_4w": mean(user_avg_match for r in [last, *prev3] if user_avg_match is not None),
|
||
"user_persistent_bias": _detect_bias([last, *prev3]), # 3주↑ 유지된 패턴 편향(예: "저번호 편향")
|
||
}
|
||
}
|
||
```
|
||
|
||
### 후보 풀 N=30
|
||
|
||
`collect_candidates(n=30)` — 20세트 선별 + 다양성 여유. 기존 4개 소스(simulation/heatmap/statistics/meta) 추출량을 비례 확대.
|
||
|
||
## 7. 자동 채점 잡 — `lotto/app/jobs/grade_weekly_review.py`
|
||
|
||
```
|
||
실행: 매주 일요일 03:00 KST (cron)
|
||
입력: 가장 최근 sync된 추첨 회차
|
||
처리:
|
||
1) briefings 에서 해당 회차의 4계층 picks 로드 (없으면 curator_* NULL)
|
||
2) lotto_purchase 에서 해당 회차의 사용자 구매 로드 (없으면 user_* NULL)
|
||
3) 각 세트별 일치 수 계산 → 큐레이터/사용자 집계
|
||
4) 패턴 요약(저번호·홀짝·합계 평균) → user/draw_pattern_summary
|
||
5) 패턴 갭 한 줄(가장 큰 격차 1~2개) → pattern_delta
|
||
6) weekly_review UPSERT (draw_no 유니크)
|
||
7) lotto_purchase 채점:
|
||
- 일치 3개 → prize=5000, auto_graded=1
|
||
- 일치 4개 → prize=NULL, note 에 "4등 가능성 — 동행복권 확인" 플래그
|
||
- 일치 5+ → prize=NULL, note 에 "🚨 큰 당첨 가능성 — 즉시 확인" 플래그
|
||
+ agent-office HTTP webhook(`POST /api/agent-office/notify/lotto-prize`)
|
||
호출하여 텔레그램 별도 알림 트리거
|
||
- numbers NULL 인 행은 스킵
|
||
```
|
||
|
||
## 8. 텔레그램 알림 — `agent-office/notifiers/telegram_lotto.py` (신규)
|
||
|
||
큐레이션 성공 후 `lotto_agent` 가 호출. 발송 실패는 try/except 로 흡수(briefing 저장과 분리).
|
||
4등 이상 당첨 알림은 lotto-backend 채점 잡이 `POST /api/agent-office/notify/lotto-prize` webhook 으로 트리거(agent-office 측 라우터 신규 추가).
|
||
|
||
```
|
||
🎟 1154회 · 큐레이션 떴음
|
||
|
||
"이번 주는 안정 +1, 콜드 누적 보강."
|
||
신뢰도 72 · 분배 안정 3·균형 1·공격 1
|
||
|
||
▸ 회고: 너 2.0 / 나 1.8
|
||
너 저번호 편향 → 보너스 고번호 보강
|
||
|
||
👉 결정 카드 보러가기 (https://gahusb.synology.me/lotto)
|
||
```
|
||
|
||
회고 단락은 retrospective 가 있을 때만(첫 주 생략).
|
||
|
||
## 9. 프론트 변경
|
||
|
||
### 파일 변경 맵
|
||
|
||
| 파일 | 종류 | 내용 |
|
||
|------|------|------|
|
||
| `pages/lotto/Functions.jsx` | 수정 | 분석탭 라벨 변경 |
|
||
| `pages/lotto/tabs/BriefingTab.jsx` | 수정 | DecisionCard 단일로 재구성 |
|
||
| `pages/lotto/components/decision/DecisionCard.jsx` | 신규 | 결정 카드 메인 |
|
||
| `pages/lotto/components/decision/RetrospectiveBox.jsx` | 신규 | 회고 박스 |
|
||
| `pages/lotto/components/decision/TierModeToggle.jsx` | 신규 | 4단계 칩 토글 |
|
||
| `pages/lotto/components/decision/TierSection.jsx` | 신규 | 한 계층 영역(타이틀+사유+5장) |
|
||
| `pages/lotto/components/decision/PickCard.jsx` | 신규 | 한 세트 카드(역할+번호+사유) |
|
||
| `pages/lotto/components/decision/BulkPurchaseButton.jsx` | 신규 | 원클릭 구매 |
|
||
| `pages/lotto/components/briefing/*` | 삭제·이동 | DecisionCard 하위로 흡수, CuratorUsageFooter 는 자료실 이동 |
|
||
| `pages/lotto/components/PurchasePanel.jsx` | 수정 | auto_graded 표시 + 4등 이상 플래그 |
|
||
| `pages/lotto/components/PurchaseTrendChart.jsx` | 신규 | 4주 추세 라인(너 vs 큐레이터 평균 일치) |
|
||
| `pages/lotto/hooks/useBriefing.js` | 수정 | 4계층 + retrospective 수용 |
|
||
| `pages/lotto/hooks/useReview.js` | 신규 | weekly_review 로드 |
|
||
| `pages/lotto/hooks/usePurchases.js` | 수정 | bulkPurchase 추가 |
|
||
| `api.js` | 수정 | getLatestReview, getReviewHistory, bulkPurchase 헬퍼 |
|
||
|
||
### 컴포넌트 격리 원칙
|
||
|
||
- `DecisionCard` 는 `briefing` + `review` 두 객체만 props 로 받음(내부 hook 호출 X).
|
||
- `TierSection` 은 `tier`, `picks`, `rationale` 만 받아 4번 재사용.
|
||
- `BulkPurchaseButton` 은 `draw_no`, `tier_mode`, `sets`, `amount` 4개로 작동.
|
||
|
||
## 10. 백엔드 변경
|
||
|
||
### `web-backend/lotto/`
|
||
|
||
| 파일 | 종류 | 내용 |
|
||
|------|------|------|
|
||
| `app/db/migrations/00X_weekly_review.sql` | 신규 | 테이블 생성 |
|
||
| `app/db/migrations/00X_purchase_grading.sql` | 신규 | lotto_purchase 컬럼 추가 |
|
||
| `app/db/migrations/00X_briefings_tiers.sql` | 신규 | briefings.picks 4계층 마이그레이션 |
|
||
| `app/jobs/grade_weekly_review.py` | 신규 | 채점 잡 |
|
||
| `app/curator_helpers.py` | 수정 | collect_candidates(N=30) 기본값, build_context 에 retrospective 합치기 |
|
||
| `app/routers/briefing.py` | 수정 | BriefingRequest 4계층 + narrative.retrospective 수용 |
|
||
| `app/routers/review.py` | 신규 | GET /api/lotto/review/latest, GET /api/lotto/review/history?limit=N |
|
||
| `app/routers/purchase.py` | 수정 | POST /api/lotto/purchase/bulk |
|
||
| `app/cron.py` (또는 compose 스케줄러) | 수정 | 채점 잡 일 03:00 등록 |
|
||
|
||
### `web-backend/agent-office/`
|
||
|
||
| 파일 | 종류 | 내용 |
|
||
|------|------|------|
|
||
| `app/curator/retrospective.py` | 신규 | build_retrospective |
|
||
| `app/curator/schema.py` | 수정 | 4계층 + tier_rationale + narrative.retrospective |
|
||
| `app/curator/prompt.py` | 수정 | 회고·계층 규칙 추가 |
|
||
| `app/curator/pipeline.py` | 수정 | retrospective 빌드 호출, 4계층 직렬화 |
|
||
| `app/agents/lotto.py` | 수정 | on_schedule 월 09:00, 성공 시 텔레그램 호출 |
|
||
| `app/notifiers/telegram_lotto.py` | 신규 | 알림 포맷·발송(큐레이션 완료, 4등 이상 당첨 알림 둘 다) |
|
||
| `app/routers/notify.py` | 신규 | `POST /api/agent-office/notify/lotto-prize` — lotto-backend 채점 잡이 호출 |
|
||
| `app/service_proxy.py` | 수정 | review 헬퍼 추가 |
|
||
|
||
## 11. API 추가·변경
|
||
|
||
| 메서드 | 경로 | 설명 |
|
||
|--------|------|------|
|
||
| GET | `/api/lotto/review/latest` | 최신 weekly_review 1건 |
|
||
| GET | `/api/lotto/review/history?limit=N` | 최근 N건 (4주 추세 차트용) |
|
||
| POST | `/api/lotto/purchase/bulk` | 결정 카드 원클릭 — body: `{ draw_no, tier_mode, sets, amount }` |
|
||
| POST | `/api/agent-office/notify/lotto-prize` | 4등 이상 당첨 시 lotto-backend 가 트리거 — body: `{ draw_no, match_count, numbers, purchase_id }` |
|
||
|
||
기존 엔드포인트는 그대로 유지(스키마 호환).
|
||
|
||
## 12. 에러 처리 / 격리
|
||
|
||
| 단계 | 실패 | 처리 |
|
||
|------|------|------|
|
||
| 추첨결과 sync | 동행복권 API down | 기존 정책(재시도). 채점 잡은 자동 지연만. |
|
||
| 채점 — 큐레이터 picks 없음 | 첫 주, 큐레이션 실패 회차 | curator_* NULL 로 INSERT |
|
||
| 채점 — 사용자 구매 없음 | 그 주 미구매 | user_* NULL |
|
||
| 채점 — numbers NULL 행 | 마이그레이션 이전 데이터 | 스킵, auto_graded=0 유지 |
|
||
| build_retrospective — review 없음 | 첫 주 | None 반환 → 프롬프트 분기 자연 처리 |
|
||
| Claude 스키마 실패 | 4계층 미준수 등 | 기존 1회 retry, 2회 실패 시 텔레그램 에러 알림 |
|
||
| 텔레그램 발송 실패 | 봇/네트워크 | try/except, 로그만. briefing 저장은 영향 없음 |
|
||
| bulk purchase — briefing 없음 | 큐레이션 실패 회차 | 400 + 토스트 |
|
||
| bulk purchase — 중복 호출 | 더블클릭 | (draw_no, tier_mode) 유니크 → idempotent |
|
||
| 자동채점 — 4등 이상 | 큰 당첨 | prize NULL + 메모 플래그 + 텔레그램 별도 알림 |
|
||
|
||
## 13. 테스트
|
||
|
||
### 백엔드 (`lotto/`)
|
||
|
||
- `grade_weekly_review`: (a) 정상 (b) user 구매 없음 (c) numbers NULL 스킵 (d) 일치 3개 → prize 5000 (e) 일치 4개 → 메모 플래그
|
||
- 마이그레이션: 빈 DB → 더미 → 잡 실행 → 행 정확
|
||
- briefings 마이그레이션: 구 단일 picks → core 매핑, 나머지 빈 배열
|
||
- `POST /purchase/bulk`: 정상 / 잘못된 tier_mode / briefing 없음 / 중복 호출
|
||
- `GET /review/latest`: 데이터 있음 / 빈 DB → 404
|
||
|
||
### 큐레이터 (`agent-office/curator/`)
|
||
|
||
- `build_retrospective`: review 1건 / 4건 / 0건
|
||
- `validate_response`: 정상 / 계층 누락 / 후보 외 번호 / tier_rationale 누락
|
||
- `curate_weekly` (Claude API mock): retrospective 있음·없음 / 1차 실패 → 2차 성공 / 2회 실패
|
||
- `telegram_lotto.format`: retrospective 있음·없음
|
||
|
||
### 프론트
|
||
|
||
- `DecisionCard` 수동: retrospective 있음·없음 / 모드 토글 5/10/15/20 / confidence 색
|
||
- `TierModeToggle` 단위: onChange 콜백 정확
|
||
- `BulkPurchaseButton` 수동 E2E: 클릭 → POST → 토스트 → 구매탭 갱신
|
||
- 자료실 탭 수동: 첫 진입 모두 접힘
|
||
- 모바일: DecisionCard 좁은 화면에서 깨짐 없음
|
||
|
||
## 14. 운영 점검 (배포 후 1주차)
|
||
|
||
수동으로 확인:
|
||
|
||
1. 일 03:00 채점 잡 1회 실행(`weekly_review` 1행 추가)
|
||
2. 월 09:00 큐레이션 실행(`briefings` 1행, 4계층 5×4=20개)
|
||
3. 텔레그램 알림 도착(회고 단락 정확 포함/생략)
|
||
4. 결정 카드 렌더링 정상(모바일 + PC)
|
||
5. 원클릭 구매 정확 N건 INSERT
|
||
6. cron 시간(03:00 / 09:00) 운영 패턴에 맞게 조정
|
||
|
||
## 15. Out of Scope
|
||
|
||
- 4등 이상 당첨금 자동 입력(회차별 변동, 사용자 PUT 으로 갱신)
|
||
- 큐레이터 호출 재무 비용 모니터링 강화(기존 `curator_usage` 그대로)
|
||
- 분석 탭 패널 자체의 리팩토링(라벨·디폴트 접힘만 변경)
|
||
- 1만원 외 임의 분량(7세트 등) 토글(4계층 5단위로 고정)
|