/* ============================================================================= * 프로젝트 목록 계산(92장) — 개요 숫자와 프로젝트 탭 거르기가 같은 규칙을 씀. * 화면 부품을 부르지 않는 순수 함수만 둠(시험이 바로 불러 씀). * ========================================================================== */ import type { ProjectItem, WorkflowStageState } from "./B01_Dashboard_Api_Fetch"; /** 워크플로 단계 수 — 파일 입력 · 지형 · 노선 · 종횡단 · 상세 · 수량 · 견적 */ export const STEP_COUNT = 7; /** 거르기 값 — 해시 `filter=` 로도 들어옴 */ export type ProjectFilter = "all" | "failed" | "stale" | `stage-${number}`; /** 표 「단계」 열과 같은 값 — 지금 하는 단계 번호 */ export function projectStep(project: ProjectItem): number { const step = project.workflow_state?.current_stage ?? project.workflow_stage; return Math.min(Math.max(step, 0), STEP_COUNT - 1); } export function hasStageState(project: ProjectItem, state: WorkflowStageState["state"]): boolean { return (project.workflow_state?.stages ?? []).some((stage) => stage.state === state); } export function parseFilter(raw: string | null): ProjectFilter { if (raw === "failed" || raw === "stale") return raw; const found = /^stage-(\d+)$/.exec(raw ?? ""); return found && Number(found[1]) < STEP_COUNT ? (`stage-${Number(found[1])}` as const) : "all"; } export function matchesFilter(project: ProjectItem, filter: ProjectFilter): boolean { if (filter === "all") return true; if (filter === "failed") return hasStageState(project, "FAILED"); if (filter === "stale") return hasStageState(project, "STALE"); return projectStep(project) === Number(filter.slice("stage-".length)); } /** 이름 · 지역 · 발주처 · 작성자 · 사업번호에 모든 단어가 들어간 프로젝트 */ export function matchesQuery(project: ProjectItem, text: string): boolean { const words = text.toLowerCase().split(/\s+/).filter(Boolean); if (!words.length) return true; const hay = [ project.name, project.region, project.client_org, project.owner_name, project.project_number, project.road_type, ] .join(" ") .toLowerCase(); return words.every((word) => hay.includes(word)); } export interface ProjectSummary { total: number; /** 단계 번호별 프로젝트 수 (길이 STEP_COUNT) */ byStep: number[]; failed: number; stale: number; } export function summarizeProjects(projects: ProjectItem[]): ProjectSummary { const byStep = Array.from({ length: STEP_COUNT }, () => 0); for (const project of projects) byStep[projectStep(project)] += 1; return { total: projects.length, byStep, failed: projects.filter((p) => hasStageState(p, "FAILED")).length, stale: projects.filter((p) => hasStageState(p, "STALE")).length, }; } /** 진행 현황에 보일 단계 — 실패 → 재확인 필요 → 지금 단계 순 */ export function progressStage(project: ProjectItem): WorkflowStageState | null { const stages = project.workflow_state?.stages ?? []; return ( stages.find((stage) => stage.state === "FAILED") ?? stages.find((stage) => stage.state === "STALE") ?? stages.find((stage) => stage.stage_no === projectStep(project)) ?? null ); }