You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

101 lines
3.5 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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:0011: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