로또 프리미엄 Phase 2 — 구매 이력 + 개인 패턴 분석 + 주간 리포트 캐싱
- purchase_history 테이블 추가 (draw_no, amount, sets, prize, note) - weekly_reports 캐시 테이블 추가 (drw_no UNIQUE, report JSON) - GET /api/lotto/purchase 구매 이력 조회 (draw_no, days 필터) - POST /api/lotto/purchase 구매 이력 추가 - PUT /api/lotto/purchase/:id 구매 이력 수정 (당첨금 업데이트) - DELETE /api/lotto/purchase/:id 구매 이력 삭제 - GET /api/lotto/purchase/stats 투자 수익률 통계 - GET /api/lotto/analysis/personal 개인 패턴 분석 (top/least picks, 홀짝/구간/연속번호) - GET /api/lotto/report/history 저장된 주간 리포트 목록 - GET /api/lotto/report/:drw_no 캐시 우선 조회 + cached 플래그 - 스케줄러: 토요일 09:00 주간 리포트 자동 생성 및 DB 캐싱 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -355,6 +355,76 @@ def get_statistical_report(draws: List[Tuple[int, List[int]]]) -> Dict[str, Any]
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_personal_patterns(
|
||||||
|
all_numbers: List[List[int]],
|
||||||
|
draws: List[Tuple[int, List[int]]],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
사용자 추천 이력 기반 개인 패턴 분석.
|
||||||
|
all_numbers: 저장된 모든 추천 번호 리스트 (각 원소는 6개짜리 리스트)
|
||||||
|
draws: 역대 당첨번호 (홀짝/합계 평균 비교용)
|
||||||
|
"""
|
||||||
|
if not all_numbers:
|
||||||
|
return {"total_analyzed": 0, "message": "추천 이력이 없습니다"}
|
||||||
|
|
||||||
|
total = len(all_numbers)
|
||||||
|
flat = [n for nums in all_numbers for n in nums]
|
||||||
|
freq = Counter(flat)
|
||||||
|
|
||||||
|
# 번호별 선택 빈도
|
||||||
|
number_frequency = {n: freq.get(n, 0) for n in range(1, 46)}
|
||||||
|
top_picks = sorted(range(1, 46), key=lambda n: -freq.get(n, 0))[:10]
|
||||||
|
least_picks = [n for n in sorted(range(1, 46), key=lambda n: freq.get(n, 0)) if freq.get(n, 0) == 0][:10]
|
||||||
|
|
||||||
|
# 패턴 지표
|
||||||
|
odd_counts = [sum(1 for n in nums if n % 2 == 1) for nums in all_numbers]
|
||||||
|
sums = [sum(nums) for nums in all_numbers]
|
||||||
|
ranges = [max(nums) - min(nums) for nums in all_numbers]
|
||||||
|
consecutive_count = sum(
|
||||||
|
1 for nums in all_numbers
|
||||||
|
if any(sorted(nums)[i + 1] - sorted(nums)[i] == 1 for i in range(5))
|
||||||
|
)
|
||||||
|
|
||||||
|
zone_totals = {k: 0 for k in ["1-9", "10-19", "20-29", "30-39", "40-45"]}
|
||||||
|
zone_ranges = [("1-9", 1, 9), ("10-19", 10, 19), ("20-29", 20, 29), ("30-39", 30, 39), ("40-45", 40, 45)]
|
||||||
|
for nums in all_numbers:
|
||||||
|
for label, lo, hi in zone_ranges:
|
||||||
|
zone_totals[label] += sum(1 for n in nums if lo <= n <= hi)
|
||||||
|
zone_avg = {k: round(v / total, 2) for k, v in zone_totals.items()}
|
||||||
|
|
||||||
|
avg_odd = sum(odd_counts) / total
|
||||||
|
avg_sum = sum(sums) / total
|
||||||
|
avg_range = sum(ranges) / total
|
||||||
|
|
||||||
|
# 역대 당첨번호 평균과 비교
|
||||||
|
if draws:
|
||||||
|
draw_odd_avg = sum(sum(1 for n in nums if n % 2 == 1) for _, nums in draws) / len(draws)
|
||||||
|
draw_sum_avg = sum(sum(nums) for _, nums in draws) / len(draws)
|
||||||
|
else:
|
||||||
|
draw_odd_avg = 3.0
|
||||||
|
draw_sum_avg = 138.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_analyzed": total,
|
||||||
|
"number_frequency": number_frequency,
|
||||||
|
"top_picks": top_picks,
|
||||||
|
"least_picks": least_picks,
|
||||||
|
"pattern": {
|
||||||
|
"avg_odd_count": round(avg_odd, 2),
|
||||||
|
"avg_sum": round(avg_sum, 1),
|
||||||
|
"avg_range": round(avg_range, 1),
|
||||||
|
"consecutive_rate": round(consecutive_count / total, 3),
|
||||||
|
"zone_avg": zone_avg,
|
||||||
|
},
|
||||||
|
"vs_draw_avg": {
|
||||||
|
"odd_diff": round(avg_odd - draw_odd_avg, 2),
|
||||||
|
"sum_diff": round(avg_sum - draw_sum_avg, 1),
|
||||||
|
"odd_tendency": "홀수 선호" if avg_odd > draw_odd_avg + 0.2 else ("짝수 선호" if avg_odd < draw_odd_avg - 0.2 else "균형"),
|
||||||
|
"sum_tendency": "고합계 선호" if avg_sum > draw_sum_avg + 5 else ("저합계 선호" if avg_sum < draw_sum_avg - 5 else "균형"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def generate_weekly_report(draws: List[Tuple[int, List[int]]], target_drw_no: int) -> Dict[str, Any]:
|
def generate_weekly_report(draws: List[Tuple[int, List[int]]], target_drw_no: int) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
특정 회차 공략 리포트 생성.
|
특정 회차 공략 리포트 생성.
|
||||||
|
|||||||
@@ -251,6 +251,34 @@ def init_db() -> None:
|
|||||||
"CREATE INDEX IF NOT EXISTS idx_sub_items_created ON subscription_items(created_at DESC);"
|
"CREATE INDEX IF NOT EXISTS idx_sub_items_created ON subscription_items(created_at DESC);"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── purchase_history 테이블 ────────────────────────────────────────────
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS purchase_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
draw_no INTEGER NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
sets INTEGER NOT NULL DEFAULT 1,
|
||||||
|
prize INTEGER NOT NULL DEFAULT 0,
|
||||||
|
note TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_purchase_draw ON purchase_history(draw_no DESC);")
|
||||||
|
|
||||||
|
# ── weekly_reports 캐시 테이블 ──────────────────────────────────────────
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS weekly_reports (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
drw_no INTEGER UNIQUE NOT NULL,
|
||||||
|
report TEXT NOT NULL,
|
||||||
|
generated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# ── subscription_profile 테이블 (싱글톤 id=1) ──────────────────────────
|
# ── subscription_profile 테이블 (싱글톤 id=1) ──────────────────────────
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -1109,6 +1137,144 @@ def get_subscription_profile() -> Optional[Dict[str, Any]]:
|
|||||||
return _profile_row_to_dict(r) if r else None
|
return _profile_row_to_dict(r) if r else None
|
||||||
|
|
||||||
|
|
||||||
|
# ── purchase_history CRUD ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _purchase_row_to_dict(r) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": r["id"],
|
||||||
|
"draw_no": r["draw_no"],
|
||||||
|
"amount": r["amount"],
|
||||||
|
"sets": r["sets"],
|
||||||
|
"prize": r["prize"],
|
||||||
|
"note": r["note"],
|
||||||
|
"created_at": r["created_at"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_purchase(draw_no: int, amount: int, sets: int, prize: int = 0, note: str = "") -> Dict[str, Any]:
|
||||||
|
with _conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO purchase_history (draw_no, amount, sets, prize, note) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
(draw_no, amount, sets, prize, note),
|
||||||
|
)
|
||||||
|
row = conn.execute("SELECT * FROM purchase_history WHERE rowid = last_insert_rowid()").fetchone()
|
||||||
|
return _purchase_row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def get_purchases(draw_no: int = None, days: int = None) -> List[Dict[str, Any]]:
|
||||||
|
conditions, params = [], []
|
||||||
|
if draw_no is not None:
|
||||||
|
conditions.append("draw_no = ?")
|
||||||
|
params.append(draw_no)
|
||||||
|
if days:
|
||||||
|
conditions.append("created_at >= datetime('now', ? || ' days')")
|
||||||
|
params.append(f"-{days}")
|
||||||
|
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
||||||
|
with _conn() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
f"SELECT * FROM purchase_history {where} ORDER BY draw_no DESC, id DESC",
|
||||||
|
params,
|
||||||
|
).fetchall()
|
||||||
|
return [_purchase_row_to_dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def update_purchase(purchase_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
allowed = {"draw_no", "amount", "sets", "prize", "note"}
|
||||||
|
updates = {k: v for k, v in data.items() if k in allowed}
|
||||||
|
if not updates:
|
||||||
|
with _conn() as conn:
|
||||||
|
row = conn.execute("SELECT * FROM purchase_history WHERE id = ?", (purchase_id,)).fetchone()
|
||||||
|
return _purchase_row_to_dict(row) if row else None
|
||||||
|
set_clause = ", ".join(f"{k} = ?" for k in updates)
|
||||||
|
with _conn() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
f"UPDATE purchase_history SET {set_clause} WHERE id = ?",
|
||||||
|
list(updates.values()) + [purchase_id],
|
||||||
|
)
|
||||||
|
if cur.rowcount == 0:
|
||||||
|
return None
|
||||||
|
row = conn.execute("SELECT * FROM purchase_history WHERE id = ?", (purchase_id,)).fetchone()
|
||||||
|
return _purchase_row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_purchase(purchase_id: int) -> bool:
|
||||||
|
with _conn() as conn:
|
||||||
|
cur = conn.execute("DELETE FROM purchase_history WHERE id = ?", (purchase_id,))
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_purchase_stats() -> Dict[str, Any]:
|
||||||
|
with _conn() as conn:
|
||||||
|
rows = conn.execute("SELECT amount, prize FROM purchase_history").fetchall()
|
||||||
|
if not rows:
|
||||||
|
return {
|
||||||
|
"total_records": 0,
|
||||||
|
"total_invested": 0,
|
||||||
|
"total_prize": 0,
|
||||||
|
"net": 0,
|
||||||
|
"return_rate": 0.0,
|
||||||
|
"prize_count": 0,
|
||||||
|
"max_prize": 0,
|
||||||
|
}
|
||||||
|
amounts = [r["amount"] for r in rows]
|
||||||
|
prizes = [r["prize"] for r in rows]
|
||||||
|
total_invested = sum(amounts)
|
||||||
|
total_prize = sum(prizes)
|
||||||
|
return {
|
||||||
|
"total_records": len(rows),
|
||||||
|
"total_invested": total_invested,
|
||||||
|
"total_prize": total_prize,
|
||||||
|
"net": total_prize - total_invested,
|
||||||
|
"return_rate": round((total_prize / total_invested * 100) if total_invested else 0.0, 2),
|
||||||
|
"prize_count": sum(1 for p in prizes if p > 0),
|
||||||
|
"max_prize": max(prizes),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── weekly_reports CRUD ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def save_weekly_report(drw_no: int, report_json: str) -> None:
|
||||||
|
with _conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO weekly_reports (drw_no, report)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT(drw_no) DO UPDATE SET
|
||||||
|
report = excluded.report,
|
||||||
|
generated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||||
|
""",
|
||||||
|
(drw_no, report_json),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_weekly_report_list(limit: int = 10) -> List[Dict[str, Any]]:
|
||||||
|
with _conn() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT drw_no, generated_at FROM weekly_reports ORDER BY drw_no DESC LIMIT ?",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_weekly_report(drw_no: int) -> Optional[Dict[str, Any]]:
|
||||||
|
with _conn() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT drw_no, report, generated_at FROM weekly_reports WHERE drw_no = ?",
|
||||||
|
(drw_no,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
import json as _json
|
||||||
|
return {"drw_no": row["drw_no"], "generated_at": row["generated_at"], **_json.loads(row["report"])}
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_recommendation_numbers() -> List[List[int]]:
|
||||||
|
"""개인 패턴 분석용: 저장된 모든 추천 번호 반환"""
|
||||||
|
with _conn() as conn:
|
||||||
|
rows = conn.execute("SELECT numbers FROM recommendations ORDER BY id DESC").fetchall()
|
||||||
|
return [json.loads(r["numbers"]) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
def upsert_subscription_profile(data: Dict[str, Any]) -> Dict[str, Any]:
|
def upsert_subscription_profile(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
field_map = {
|
field_map = {
|
||||||
"isHouseholdHead": "is_household_head",
|
"isHouseholdHead": "is_household_head",
|
||||||
|
|||||||
@@ -22,13 +22,19 @@ from .db import (
|
|||||||
get_subscription_profile, upsert_subscription_profile,
|
get_subscription_profile, upsert_subscription_profile,
|
||||||
# 성과 통계
|
# 성과 통계
|
||||||
get_recommendation_performance,
|
get_recommendation_performance,
|
||||||
|
# Phase 2: 구매 이력
|
||||||
|
add_purchase, get_purchases, update_purchase, delete_purchase, get_purchase_stats,
|
||||||
|
# Phase 2: 주간 리포트 캐시
|
||||||
|
save_weekly_report, get_weekly_report_list, get_weekly_report,
|
||||||
|
# Phase 2: 개인 패턴 분석
|
||||||
|
get_all_recommendation_numbers,
|
||||||
)
|
)
|
||||||
from .recommender import recommend_numbers, recommend_with_heatmap
|
from .recommender import recommend_numbers, recommend_with_heatmap
|
||||||
from .collector import sync_latest, sync_ensure_all
|
from .collector import sync_latest, sync_ensure_all
|
||||||
from .generator import run_simulation, generate_smart_recommendations
|
from .generator import run_simulation, generate_smart_recommendations
|
||||||
from .checker import check_results_for_draw
|
from .checker import check_results_for_draw
|
||||||
from .utils import calc_metrics, calc_recent_overlap
|
from .utils import calc_metrics, calc_recent_overlap
|
||||||
from .analyzer import get_statistical_report, generate_weekly_report
|
from .analyzer import get_statistical_report, generate_weekly_report, analyze_personal_patterns
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul"))
|
scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul"))
|
||||||
@@ -57,6 +63,20 @@ def on_startup():
|
|||||||
|
|
||||||
scheduler.add_job(_run_simulation_job, "cron", hour="0,4,8,12,16,20", minute=5)
|
scheduler.add_job(_run_simulation_job, "cron", hour="0,4,8,12,16,20", minute=5)
|
||||||
|
|
||||||
|
# 3. 토요일 오전 9시 — 다음 회차 공략 리포트 자동 캐싱
|
||||||
|
def _save_weekly_report_job():
|
||||||
|
import json as _json
|
||||||
|
draws = get_all_draw_numbers()
|
||||||
|
latest = get_latest_draw()
|
||||||
|
if not draws or not latest:
|
||||||
|
return
|
||||||
|
target = latest["drw_no"] + 1
|
||||||
|
report = generate_weekly_report(draws, target)
|
||||||
|
save_weekly_report(target, _json.dumps(report, ensure_ascii=False))
|
||||||
|
print(f"[WeeklyReport] {target}회차 리포트 저장 완료")
|
||||||
|
|
||||||
|
scheduler.add_job(_save_weekly_report_job, "cron", day_of_week="sat", hour=9, minute=0)
|
||||||
|
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
|
|
||||||
@@ -179,20 +199,95 @@ def api_report_latest():
|
|||||||
return generate_weekly_report(draws, target)
|
return generate_weekly_report(draws, target)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/lotto/report/history")
|
||||||
|
def api_report_history(limit: int = 10):
|
||||||
|
"""저장된 주간 리포트 목록 (자동 저장된 것만)"""
|
||||||
|
return {"reports": get_weekly_report_list(limit=min(limit, 52))}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/lotto/report/{drw_no}")
|
@app.get("/api/lotto/report/{drw_no}")
|
||||||
def api_report_by_draw(drw_no: int):
|
def api_report_by_draw(drw_no: int):
|
||||||
"""
|
"""
|
||||||
특정 회차 공략 리포트 (해당 회차 이전 데이터 기준).
|
특정 회차 공략 리포트 (캐시 우선, 없으면 실시간 생성).
|
||||||
drw_no: 공략 대상 회차 번호
|
|
||||||
"""
|
"""
|
||||||
|
cached = get_weekly_report(drw_no)
|
||||||
|
if cached:
|
||||||
|
return {**cached, "cached": True}
|
||||||
|
|
||||||
draws = get_all_draw_numbers()
|
draws = get_all_draw_numbers()
|
||||||
if not draws:
|
if not draws:
|
||||||
raise HTTPException(status_code=404, detail="No data yet")
|
raise HTTPException(status_code=404, detail="No data yet")
|
||||||
# drw_no 이전 데이터만 사용
|
|
||||||
base_draws = [(no, nums) for no, nums in draws if no < drw_no]
|
base_draws = [(no, nums) for no, nums in draws if no < drw_no]
|
||||||
if not base_draws:
|
if not base_draws:
|
||||||
raise HTTPException(status_code=400, detail=f"{drw_no}회차 이전 데이터가 없습니다")
|
raise HTTPException(status_code=400, detail=f"{drw_no}회차 이전 데이터가 없습니다")
|
||||||
return generate_weekly_report(base_draws, drw_no)
|
return {**generate_weekly_report(base_draws, drw_no), "cached": False}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 개인 패턴 분석 (Phase 2) ─────────────────────────────────────────────────
|
||||||
|
@app.get("/api/lotto/analysis/personal")
|
||||||
|
def api_personal_analysis():
|
||||||
|
"""
|
||||||
|
저장된 추천 이력 기반 개인 패턴 분석.
|
||||||
|
- 자주 선택한 번호 TOP 10 / 한 번도 선택 안 한 번호
|
||||||
|
- 홀짝 비율, 합계, 범위, 연속번호 포함률
|
||||||
|
- 구간별 분포, 역대 당첨번호 평균과 비교
|
||||||
|
"""
|
||||||
|
all_numbers = get_all_recommendation_numbers()
|
||||||
|
draws = get_all_draw_numbers()
|
||||||
|
return analyze_personal_patterns(all_numbers, draws)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 구매 이력 API (Phase 2) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class PurchaseCreate(BaseModel):
|
||||||
|
draw_no: int
|
||||||
|
amount: int
|
||||||
|
sets: int = 1
|
||||||
|
prize: int = 0
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseUpdate(BaseModel):
|
||||||
|
draw_no: Optional[int] = None
|
||||||
|
amount: Optional[int] = None
|
||||||
|
sets: Optional[int] = None
|
||||||
|
prize: Optional[int] = None
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/lotto/purchase/stats")
|
||||||
|
def api_purchase_stats():
|
||||||
|
"""투자 수익률 통계 (총 투자금, 총 당첨금, 수익률 등)"""
|
||||||
|
return get_purchase_stats()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/lotto/purchase")
|
||||||
|
def api_purchase_list(draw_no: Optional[int] = None, days: Optional[int] = None):
|
||||||
|
"""구매 이력 조회 (draw_no, days 필터 선택)"""
|
||||||
|
return {"records": get_purchases(draw_no=draw_no, days=days)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/lotto/purchase", status_code=201)
|
||||||
|
def api_purchase_create(body: PurchaseCreate):
|
||||||
|
"""구매 이력 추가"""
|
||||||
|
return add_purchase(body.draw_no, body.amount, body.sets, body.prize, body.note)
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/lotto/purchase/{purchase_id}")
|
||||||
|
def api_purchase_update(purchase_id: int, body: PurchaseUpdate):
|
||||||
|
"""구매 이력 수정 (당첨금 업데이트 등)"""
|
||||||
|
updated = update_purchase(purchase_id, body.model_dump(exclude_none=True))
|
||||||
|
if updated is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Purchase not found")
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/lotto/purchase/{purchase_id}")
|
||||||
|
def api_purchase_delete(purchase_id: int):
|
||||||
|
"""구매 이력 삭제"""
|
||||||
|
if not delete_purchase(purchase_id):
|
||||||
|
raise HTTPException(status_code=404, detail="Purchase not found")
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
# ── 통계 분석 리포트 ────────────────────────────────────────────────────────
|
# ── 통계 분석 리포트 ────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user