feat(project): 달력을 휴가 모듈식 월간 그리드로 교체
FullCalendar 제거. 휴가 모듈과 동일한 서버 렌더 월간 달력(구글식 bar 레인 배치)로 변경. 업무 start~due 기간을 bar 로 표시, 클릭 시 모달. ?y=&m= 로 월 이동, 프로젝트 색상 반영, 완료 업무는 회색+취소선. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar as _calendar
|
||||
import logging
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Request
|
||||
@@ -132,6 +134,96 @@ def _build_tree(projects: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return roots
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# 월간 달력 레이아웃 (휴가 모듈과 동일한 구글식 bar 레인 배치)
|
||||
# ────────────────────────────────────────────────────────────
|
||||
def _assign_lanes(bars: list[dict[str, Any]]) -> int:
|
||||
"""주(week) 내 bar 들에 겹치지 않는 lane(행)을 그리디 배정. 사용 lane 수 반환."""
|
||||
bars.sort(key=lambda b: (b["start_col"], -b["span"]))
|
||||
lane_end: list[int] = [] # lane 별 마지막 점유 col(포함)
|
||||
for b in bars:
|
||||
placed = False
|
||||
for li, end_col in enumerate(lane_end):
|
||||
if b["start_col"] > end_col:
|
||||
b["lane"] = li
|
||||
lane_end[li] = b["start_col"] + b["span"] - 1
|
||||
placed = True
|
||||
break
|
||||
if not placed:
|
||||
b["lane"] = len(lane_end)
|
||||
lane_end.append(b["start_col"] + b["span"] - 1)
|
||||
return len(lane_end)
|
||||
|
||||
|
||||
def _task_span(task: dict[str, Any]) -> tuple[date, date] | None:
|
||||
"""업무의 달력 표시 기간(start..due). 둘 중 하나만 있으면 단일일. 없으면 None."""
|
||||
s = task.get("start_date")
|
||||
d = task.get("due_date")
|
||||
try:
|
||||
sd = date.fromisoformat(s[:10]) if s else None
|
||||
dd = date.fromisoformat(d[:10]) if d else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
if sd and dd:
|
||||
return (sd, dd) if sd <= dd else (dd, sd)
|
||||
if sd:
|
||||
return (sd, sd)
|
||||
if dd:
|
||||
return (dd, dd)
|
||||
return None
|
||||
|
||||
|
||||
def _build_task_calendar(
|
||||
tasks: list[dict[str, Any]], year: int, month: int
|
||||
) -> list[dict[str, Any]]:
|
||||
"""업무 리스트 → 월간 달력 주(week) 데이터(휴가 _build_calendar 와 동일 구조)."""
|
||||
cal = _calendar.Calendar(firstweekday=6) # 일요일 시작
|
||||
weeks_dates = cal.monthdatescalendar(year, month)
|
||||
today = today_kst()
|
||||
|
||||
spans: list[tuple[date, date, dict[str, Any]]] = []
|
||||
for t in tasks:
|
||||
sp = _task_span(t)
|
||||
if sp:
|
||||
spans.append((sp[0], sp[1], t))
|
||||
|
||||
weeks: list[dict[str, Any]] = []
|
||||
for week in weeks_dates:
|
||||
w_start, w_end = week[0], week[-1]
|
||||
days = [
|
||||
{
|
||||
"date": d.isoformat(),
|
||||
"day": d.day,
|
||||
"in_month": d.month == month,
|
||||
"is_today": d == today,
|
||||
"is_sunday": d.weekday() == 6,
|
||||
"is_saturday": d.weekday() == 5,
|
||||
}
|
||||
for d in week
|
||||
]
|
||||
bars: list[dict[str, Any]] = []
|
||||
for rs, re_, t in spans:
|
||||
if re_ < w_start or rs > w_end:
|
||||
continue
|
||||
seg_start = max(rs, w_start)
|
||||
seg_end = min(re_, w_end)
|
||||
bars.append(
|
||||
{
|
||||
"id": t["id"],
|
||||
"label": t["title"],
|
||||
"color": t.get("project_color") or "#4573d2",
|
||||
"completed": bool(t.get("completed_at")),
|
||||
"start_col": (seg_start - w_start).days,
|
||||
"span": (seg_end - seg_start).days + 1,
|
||||
"continues_left": rs < w_start,
|
||||
"continues_right": re_ > w_end,
|
||||
}
|
||||
)
|
||||
lane_count = _assign_lanes(bars)
|
||||
weeks.append({"days": days, "bars": bars, "lane_count": lane_count})
|
||||
return weeks
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# 페이지
|
||||
# ────────────────────────────────────────────────────────────
|
||||
@@ -206,6 +298,19 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse:
|
||||
can_manage = _can_manage(request, st, user, project_id)
|
||||
subprojects = [p for p in all_projects if p.get("parent_id") == project_id]
|
||||
|
||||
# 달력(휴가식 월간 그리드) — ?y=&m= 로 월 이동, 기본 이번 달
|
||||
today = today_kst()
|
||||
try:
|
||||
year = int(request.query_params.get("y") or today.year)
|
||||
month = int(request.query_params.get("m") or today.month)
|
||||
if not (1 <= month <= 12):
|
||||
year, month = today.year, today.month
|
||||
except (TypeError, ValueError):
|
||||
year, month = today.year, today.month
|
||||
weeks = _build_task_calendar(tasks, year, month)
|
||||
prev_y, prev_m = (year - 1, 12) if month == 1 else (year, month - 1)
|
||||
next_y, next_m = (year + 1, 1) if month == 12 else (year, month + 1)
|
||||
|
||||
return render_template(
|
||||
request,
|
||||
"project/project.html",
|
||||
@@ -224,7 +329,13 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse:
|
||||
"subprojects": subprojects,
|
||||
"priority_labels": store.PRIORITY_LABELS,
|
||||
"color_palette": list(store.COLOR_PALETTE),
|
||||
"today": today_kst().isoformat(),
|
||||
"today": today.isoformat(),
|
||||
"year": year,
|
||||
"month": month,
|
||||
"prev_y": prev_y, "prev_m": prev_m,
|
||||
"next_y": next_y, "next_m": next_m,
|
||||
"weekdays": ["일", "월", "화", "수", "목", "금", "토"],
|
||||
"weeks": weeks,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user