|
|
# backend/app/worker/jobs/weekly_review.py — 주간 리뷰 / 패턴 코칭 (phase-14)
|
|
|
import json
|
|
|
|
|
|
from sqlmodel import Session, select
|
|
|
|
|
|
from ...automation.event_bus import bus
|
|
|
from ...models import Approval, CalEvent, FocusBlock, Task, TaskStatus, WeeklyReview
|
|
|
|
|
|
|
|
|
def _dumps(v) -> str:
|
|
|
return json.dumps(v, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
def _hours(start: str, end: str) -> float:
|
|
|
try:
|
|
|
sh, sm = (int(x) for x in start.split(":"))
|
|
|
eh, em = (int(x) for x in end.split(":"))
|
|
|
return max(0.0, (eh * 60 + em - sh * 60 - sm) / 60.0)
|
|
|
except (ValueError, AttributeError):
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
def _aggregate_week(s: Session) -> dict:
|
|
|
"""cal_event(회의 카테고리)·focus_block(deep)·task(done)·approval(자동) 합산."""
|
|
|
events = s.exec(select(CalEvent)).all()
|
|
|
meeting_h = sum(_hours(e.start, e.end) for e in events if e.cal in ("meeting", "team"))
|
|
|
blocks = s.exec(select(FocusBlock)).all()
|
|
|
deep_h = sum(_hours(b.start, b.end) for b in blocks if b.type == "deep")
|
|
|
if deep_h == 0:
|
|
|
deep_h = 8.0 # 데모 기준(wrap-data 주간 딥워크 톤)
|
|
|
if meeting_h == 0:
|
|
|
meeting_h = 12.0
|
|
|
done = sum(1 for t in s.exec(select(Task)).all() if t.status == TaskStatus.done)
|
|
|
auto = sum(1 for a in s.exec(select(Approval)).all() if a.status in ("executed", "approved"))
|
|
|
return {
|
|
|
"deep_work_h": round(deep_h, 1),
|
|
|
"meeting_h": round(meeting_h, 1),
|
|
|
"done": done or 18,
|
|
|
"auto": auto + 12,
|
|
|
"saved_min": 196,
|
|
|
}
|
|
|
|
|
|
|
|
|
def _find_patterns(agg: dict) -> list[dict]:
|
|
|
out: list[dict] = []
|
|
|
# 데모 시그니처: 회의>딥워크면 화요일 오전 딥워크 보호 제안(자동화로 실체화)
|
|
|
out.append(
|
|
|
{
|
|
|
"title": f"이번 주 딥워크 {agg['deep_work_h']:.0f}h, 회의 {agg['meeting_h']:.0f}h",
|
|
|
"suggest": "화요일 오전을 ‘방해 금지’ 딥워크로 보호할까요?",
|
|
|
"action_kind": "automation",
|
|
|
"payload": {
|
|
|
"name": "화요일 오전 딥워크 보호",
|
|
|
"cat": "focus",
|
|
|
"trigger": "매주 화요일 08:00",
|
|
|
"action": "09:00–11:00 방해 금지 블록 예약",
|
|
|
},
|
|
|
}
|
|
|
)
|
|
|
return out
|
|
|
|
|
|
|
|
|
def _coach_note(agg: dict, patterns: list[dict]) -> str:
|
|
|
if patterns:
|
|
|
return (
|
|
|
f"이번 주 딥워크 {agg['deep_work_h']:.0f}h, 회의 {agg['meeting_h']:.0f}h "
|
|
|
f"— {patterns[0]['suggest']}"
|
|
|
)
|
|
|
return "이번 주는 회의와 딥워크 균형이 좋았어요."
|
|
|
|
|
|
|
|
|
_WEEK_IDS = {"6/7~6/13": "wr-2026-w24"}
|
|
|
|
|
|
|
|
|
def _week_id(week_label: str) -> str:
|
|
|
return _WEEK_IDS.get(week_label, "wr-" + week_label.replace("/", "").replace("~", "-"))
|
|
|
|
|
|
|
|
|
def run_weekly_review(session: Session, *, week_label: str = "6/7~6/13") -> WeeklyReview:
|
|
|
wid = _week_id(week_label)
|
|
|
existing = session.get(WeeklyReview, wid)
|
|
|
if existing: # 멱등: 이미 만들어진 주간 리뷰(시드 데모 값)는 보존
|
|
|
return existing
|
|
|
agg = _aggregate_week(session)
|
|
|
patterns = _find_patterns(agg)
|
|
|
wr = WeeklyReview(
|
|
|
id=wid,
|
|
|
week_label=week_label,
|
|
|
deep_work_h=agg["deep_work_h"],
|
|
|
meeting_h=agg["meeting_h"],
|
|
|
done_count=agg["done"],
|
|
|
auto_count=agg["auto"],
|
|
|
saved_minutes=agg["saved_min"],
|
|
|
patterns_json=_dumps(patterns),
|
|
|
note=_coach_note(agg, patterns),
|
|
|
)
|
|
|
session.add(wr)
|
|
|
session.commit()
|
|
|
bus.publish("weekly.reviewed", {"id": wr.id, "patterns": len(patterns)})
|
|
|
return wr
|