feat: CONAI Phase 1 MVP 초기 구현
소형 건설업체(100억 미만)를 위한 AI 기반 토목공사 통합관리 플랫폼 Backend (FastAPI): - SQLAlchemy 모델 13개 (users, projects, wbs, tasks, daily_reports, reports, inspections, quality, weather, permits, rag, settings) - API 라우터 11개 (auth, projects, tasks, daily_reports, reports, inspections, weather, rag, kakao, permits, settings) - Services: Claude AI 래퍼, CPM Gantt 계산, 기상청 API, RAG(pgvector), 카카오 Skill API - Alembic 마이그레이션 (pgvector 포함) - pytest 테스트 (CPM, 날씨 경보) Frontend (Next.js 15): - 11개 페이지 (대시보드, 프로젝트, Gantt, 일보, 검측, 품질, 날씨, 인허가, RAG, 설정) - TanStack Query + Zustand + Tailwind CSS 인프라: - Docker Compose (PostgreSQL pgvector + backend + frontend) - 한국어 README 및 설치 가이드 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
111
backend/app/services/gantt.py
Normal file
111
backend/app/services/gantt.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
CPM (Critical Path Method) calculation for Gantt chart.
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
from typing import NamedTuple
|
||||
import uuid
|
||||
|
||||
|
||||
class TaskNode(NamedTuple):
|
||||
id: uuid.UUID
|
||||
planned_start: date | None
|
||||
planned_end: date | None
|
||||
duration_days: int
|
||||
|
||||
|
||||
def compute_cpm(tasks: list, dependencies: list) -> dict[uuid.UUID, dict]:
|
||||
"""
|
||||
Compute CPM forward/backward pass.
|
||||
Returns dict: task_id -> {early_start, early_finish, late_start, late_finish, total_float, is_critical}
|
||||
"""
|
||||
if not tasks:
|
||||
return {}
|
||||
|
||||
# Build adjacency maps
|
||||
task_map = {t.id: t for t in tasks}
|
||||
successors: dict[uuid.UUID, list[uuid.UUID]] = {t.id: [] for t in tasks}
|
||||
predecessors: dict[uuid.UUID, list[uuid.UUID]] = {t.id: [] for t in tasks}
|
||||
|
||||
for dep in dependencies:
|
||||
successors[dep.predecessor_id].append(dep.successor_id)
|
||||
predecessors[dep.successor_id].append(dep.predecessor_id)
|
||||
|
||||
def get_duration(task) -> int:
|
||||
if task.planned_start and task.planned_end:
|
||||
return max(1, (task.planned_end - task.planned_start).days + 1)
|
||||
return 1
|
||||
|
||||
# Topological sort (Kahn's algorithm)
|
||||
in_degree = {t.id: len(predecessors[t.id]) for t in tasks}
|
||||
queue = [t.id for t in tasks if in_degree[t.id] == 0]
|
||||
topo_order = []
|
||||
|
||||
while queue:
|
||||
node = queue.pop(0)
|
||||
topo_order.append(node)
|
||||
for succ in successors[node]:
|
||||
in_degree[succ] -= 1
|
||||
if in_degree[succ] == 0:
|
||||
queue.append(succ)
|
||||
|
||||
# Forward pass: compute Early Start (ES) and Early Finish (EF)
|
||||
es: dict[uuid.UUID, int] = {} # days from project start
|
||||
ef: dict[uuid.UUID, int] = {}
|
||||
|
||||
for tid in topo_order:
|
||||
task = task_map[tid]
|
||||
dur = get_duration(task)
|
||||
if not predecessors[tid]:
|
||||
es[tid] = 0
|
||||
else:
|
||||
es[tid] = max(ef[p] for p in predecessors[tid])
|
||||
ef[tid] = es[tid] + dur
|
||||
|
||||
if not ef:
|
||||
return {}
|
||||
|
||||
project_duration = max(ef.values())
|
||||
|
||||
# Backward pass: compute Late Finish (LF) and Late Start (LS)
|
||||
lf: dict[uuid.UUID, int] = {}
|
||||
ls: dict[uuid.UUID, int] = {}
|
||||
|
||||
for tid in reversed(topo_order):
|
||||
task = task_map[tid]
|
||||
dur = get_duration(task)
|
||||
if not successors[tid]:
|
||||
lf[tid] = project_duration
|
||||
else:
|
||||
lf[tid] = min(ls[s] for s in successors[tid])
|
||||
ls[tid] = lf[tid] - dur
|
||||
|
||||
# Compute float and critical path
|
||||
result = {}
|
||||
# Find an actual project start date
|
||||
project_start = None
|
||||
for t in tasks:
|
||||
if t.planned_start:
|
||||
if project_start is None or t.planned_start < project_start:
|
||||
project_start = t.planned_start
|
||||
if not project_start:
|
||||
project_start = date.today()
|
||||
|
||||
for tid in topo_order:
|
||||
total_float = ls[tid] - es[tid]
|
||||
is_critical = total_float == 0
|
||||
|
||||
early_start_date = project_start + timedelta(days=es[tid])
|
||||
early_finish_date = project_start + timedelta(days=ef[tid] - 1)
|
||||
late_start_date = project_start + timedelta(days=ls[tid])
|
||||
late_finish_date = project_start + timedelta(days=lf[tid] - 1)
|
||||
|
||||
result[tid] = {
|
||||
"early_start": early_start_date,
|
||||
"early_finish": early_finish_date,
|
||||
"late_start": late_start_date,
|
||||
"late_finish": late_finish_date,
|
||||
"total_float": total_float,
|
||||
"is_critical": is_critical,
|
||||
}
|
||||
|
||||
return result, project_duration
|
||||
Reference in New Issue
Block a user