# backend/app/routers/worker.py — 능동 워커 트리거 + 능동 카드 + 주간 리뷰 API (phase-14) import json import uuid from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select from ..db import get_session from ..models import ( AutomationRule, FocusBlock, ProactiveCard, Project, Task, TaskStatus, ) from ..schemas import ( ProactiveAcceptOut, ProactiveCardOut, WeeklyReviewOut, WorkerRunOut, ) from ..worker.jobs.weekly_review import run_weekly_review from ..worker.triggers import JOBS router = APIRouter() def _pc_out(c: ProactiveCard) -> ProactiveCardOut: return ProactiveCardOut( id=c.id, kind=c.kind, icon=c.icon, tone=c.tone, title=c.title, why=c.why, cta=c.cta, action_kind=c.action_kind, detected_at=c.detected_at, status=c.status, ) @router.post("/worker/run/{job}", response_model=WorkerRunOut) def run_job(job: str, payload: dict | None = None, s: Session = Depends(get_session)): if job not in JOBS: raise HTTPException(404, f"unknown job: {job}") result = JOBS[job](s, payload or {}) return WorkerRunOut(job=job, ok=True, detail=json.dumps(result, ensure_ascii=False)) @router.get("/proactive", response_model=list[ProactiveCardOut]) def list_proactive(s: Session = Depends(get_session)): rows = s.exec(select(ProactiveCard).where(ProactiveCard.status == "active")).all() return [_pc_out(c) for c in rows] @router.post("/proactive/{pid}/accept", response_model=ProactiveAcceptOut) def accept_proactive(pid: str, s: Session = Depends(get_session)): c = s.get(ProactiveCard, pid) if not c: raise HTTPException(404, "proactive card not found") created_id, message = _materialize(s, c) c.status = "accepted" s.add(c) s.commit() return ProactiveAcceptOut( id=c.id, action_kind=c.action_kind or "none", created_id=created_id, message=message ) @router.post("/proactive/{pid}/dismiss") def dismiss_proactive(pid: str, s: Session = Depends(get_session)): c = s.get(ProactiveCard, pid) if not c: raise HTTPException(404, "proactive card not found") c.status = "dismissed" s.add(c) s.commit() return {"ok": True} @router.get("/weekly-review", response_model=WeeklyReviewOut) def weekly_review(week: str = "6/7~6/13", s: Session = Depends(get_session)): wr = run_weekly_review(s, week_label=week) return WeeklyReviewOut( id=wr.id, week_label=wr.week_label, deep_work_h=wr.deep_work_h, meeting_h=wr.meeting_h, done_count=wr.done_count, auto_count=wr.auto_count, saved_minutes=wr.saved_minutes, patterns=json.loads(wr.patterns_json or "[]"), note=wr.note, ) def _materialize(s: Session, c: ProactiveCard) -> tuple[str | None, str]: """action_kind → focus_block/task/automation 실체화. (id, 메시지) 반환.""" payload = json.loads(c.action_payload) if c.action_payload else {} kind = c.action_kind if kind == "focus_block": fb = FocusBlock( id="fb-" + uuid.uuid4().hex[:8], day=int(payload.get("day", 0)), start=payload.get("start", ""), end=payload.get("end", ""), title=payload.get("title", "보호 블록"), type=payload.get("type", "light"), auto=True, ) s.add(fb) s.commit() return fb.id, "집중 블록을 잡아뒀어요" if kind == "task": pid = payload.get("project_id", "me") if not s.get(Project, pid): first = s.exec(select(Project)).first() pid = first.id if first else pid t = Task( id="kx-" + uuid.uuid4().hex[:8], project_id=pid, title=payload.get("title", "확인할 일"), status=TaskStatus.todo, ) s.add(t) s.commit() return t.id, "작업으로 추가했어요" if kind == "automation": r = AutomationRule( id="rule-" + uuid.uuid4().hex[:8], name=payload.get("name", "새 규칙"), cat=payload.get("cat", "focus"), trigger=payload.get("trigger", ""), cond=payload.get("cond"), action=payload.get("action", ""), source="suggestion", fresh=True, last="방금 만듦", ) s.add(r) s.commit() return r.id, "자동화 규칙을 만들었어요" return None, "확인했어요"