매주 같은 시간에 큐레이터가 한 번 더 똑똑해지는 컨셉으로 - 회고 컨텍스트(weekly_review + 자동 채점 잡) - 4계층 위계(코어/보너스/확장/풀, 5~20세트) - 결정 카드 단일 화면(브리핑 탭 재구성) - 분석 탭은 자료실로 강등 - 월요일 09:00 큐레이션 + 텔레그램 푸시
17 KiB
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로 재구성한다. 정보 위계는 위→아래로:
- 헤더 — 회차 + 한 줄 헤드라인 + 신뢰도(0~100, 큐레이터 자기 평가)
- 회고 박스 (▸ 보라색 라벨) — 지난 주 너 + 큐레이터 한 줄 회고. 시간축의 핵심.
- 헤드라인 + 3줄 — 이번 주 전망 + 근거 3줄(기존 narrative 유지).
- 분배 칩 — 선택 모드까지의 안정/균형/공격 합산 + "왜 이 분배인지" 한 줄.
- 모드 토글 — 4단계 칩(코어 5 / +보너스 5 / +확장 5 / +풀 5).
- 계층 섹션 × 4 — 각 계층마다 타이틀 + 사유 한 줄 + 5장 PickCard. 코어는 항상 펼침, 그 외는 모드에 따라.
- 하단 액션 — "이대로 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
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 컬럼 추가
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계층 구조로 마이그레이션:
{
"core": [/* 5세트 */],
"bonus": [/* 5세트 */],
"extended": [/* 5세트 */],
"pool": [/* 5세트 */]
}
기존 단일 배열 데이터는 core 키에만 매핑하고 나머지 키는 빈 배열로 채우는 1회 마이그레이션 스크립트.
6. 큐레이터 변경
출력 스키마 (agent-office/curator/schema.py)
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 (신규)
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,amount4개로 작동.
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주차)
수동으로 확인:
- 일 03:00 채점 잡 1회 실행(
weekly_review1행 추가) - 월 09:00 큐레이션 실행(
briefings1행, 4계층 5×4=20개) - 텔레그램 알림 도착(회고 단락 정확 포함/생략)
- 결정 카드 렌더링 정상(모바일 + PC)
- 원클릭 구매 정확 N건 INSERT
- cron 시간(03:00 / 09:00) 운영 패턴에 맞게 조정
15. Out of Scope
- 4등 이상 당첨금 자동 입력(회차별 변동, 사용자 PUT 으로 갱신)
- 큐레이터 호출 재무 비용 모니터링 강화(기존
curator_usage그대로) - 분석 탭 패널 자체의 리팩토링(라벨·디폴트 접힘만 변경)
- 1만원 외 임의 분량(7세트 등) 토글(4계층 5단위로 고정)