- 상세 화면(view=project) — 머리 · [⋯] · [작업 열기 — 지금 단계 ›] · 단계 진행(누름 = 그 단계) · 노선 지도(배경 타일 + 선 · 시점 · 종점) · 핵심 숫자 · 기본 정보 · 연결된 것 · 산출물 · 최근 변경(있는 칸만) - 목록 · 상세 숫자 · 썸네일 = 547-1 카드 요약(project_summaries) 한 벌 - 색 = 셸 변수(--b01-*) 이름에 맞춤 - 시험 test_548_project_overview — 칸반 나눔 · 산출물 · 내 것 · 단계 수 짝 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
5.2 KiB
TypeScript
131 lines
5.2 KiB
TypeScript
/* =============================================================================
|
|
* B01_Dashboard_UI_ProjectList_Parts.ts
|
|
* 프로젝트 목록 · 상세 공용 조각(PLAN 548) — 종류 배지 · 단계 글 · 진행 막대 · 썸네일 선 · 칸반 칸 이름.
|
|
* ========================================================================== */
|
|
|
|
import { el } from "@ui/ui_template_elements_base";
|
|
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
|
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
|
import { groupFlowOf } from "../A00_Common/group_workflow";
|
|
import type { RoutePath } from "@config/config_frontend";
|
|
import type { BoardColumn, OverviewProject } from "./B01_Dashboard_Api_Projects";
|
|
import { L } from "./B01_Dashboard_UI_Common";
|
|
|
|
/** 종류 — 칩 · 배지 차례 · 사방설계 = 준비 중 */
|
|
export const KINDS = [
|
|
{ key: "ROAD", label: () => L("DE_New_Road") },
|
|
{ key: "PLAN", label: () => L("DE_New_Plan") },
|
|
{ key: "STABILITY", label: () => L("DE_New_Stability") },
|
|
{ key: "EROSION", label: () => L("DE_New_Erosion"), soon: true },
|
|
] as const;
|
|
|
|
export const kindLabel = (kind?: string | null): string =>
|
|
(KINDS.find((k) => k.key === (kind ?? "ROAD")) ?? KINDS[0]).label();
|
|
|
|
export function kindBadge(kind?: string | null): HTMLElement {
|
|
const key = (kind ?? "ROAD").toLowerCase();
|
|
return el("span", { className: `b01-proj-badge b01-proj-badge--${key}`, text: kindLabel(kind) });
|
|
}
|
|
|
|
export const BOARD_COLUMNS: { key: BoardColumn; label: string }[] = [
|
|
{ key: "start", label: "시작 전" },
|
|
{ key: "design", label: "설계 중" },
|
|
{ key: "review", label: "검토" },
|
|
{ key: "confirm", label: "확정 대기" },
|
|
{ key: "done", label: "완료" },
|
|
];
|
|
|
|
export const boardLabel = (key: BoardColumn): string =>
|
|
BOARD_COLUMNS.find((c) => c.key === key)?.label ?? key;
|
|
|
|
/** 종류별 단계 이름 · 주소 */
|
|
export function stageFlow(kind?: string | null): {
|
|
steps: string[];
|
|
routes: readonly RoutePath[];
|
|
} {
|
|
const group = groupFlowOf(kind);
|
|
return group
|
|
? { steps: group.steps, routes: group.routes }
|
|
: { steps: workflowSteps(), routes: WORKFLOW_STEP_ROUTES };
|
|
}
|
|
|
|
/** 지금 단계 번호(0 부터) · 마친 단계 수 */
|
|
export function stageProgress(project: OverviewProject): { current: number; done: number } {
|
|
const stages = project.workflow_state?.stages ?? [];
|
|
return {
|
|
current: project.workflow_state?.current_stage ?? 0,
|
|
done: stages.filter((s) => s.state === "COMPLETE").length,
|
|
};
|
|
}
|
|
|
|
/** 「횡단설계 4/7」 — 시작 전 = 0 · 완료 = 전체 */
|
|
export function stageText(project: OverviewProject): string {
|
|
const { steps } = stageFlow(project.project_kind);
|
|
const total = project.stage_total || steps.length;
|
|
if (!total) return L("DE_New_Soon");
|
|
const { current } = stageProgress(project);
|
|
if (project.board === "done") return `완료 ${total}/${total}`;
|
|
const n = project.board === "start" ? 0 : current + 1;
|
|
return `${steps[current] ?? ""} ${n}/${total}`;
|
|
}
|
|
|
|
export function progressBar(project: OverviewProject): HTMLElement {
|
|
const total = project.stage_total || 1;
|
|
const bar = el("span", { className: "b01-proj-bar" });
|
|
const fill = el("i");
|
|
fill.style.width = `${Math.round((stageProgress(project).done / total) * 100)}%`;
|
|
bar.append(fill);
|
|
return bar;
|
|
}
|
|
|
|
/** 단계 점 — 마침 · 지금 · 남음 */
|
|
export function stageDots(project: OverviewProject): HTMLElement {
|
|
const total = project.stage_total;
|
|
const { current } = stageProgress(project);
|
|
const states = new Map((project.workflow_state?.stages ?? []).map((s) => [s.stage_no, s.state]));
|
|
const row = el("span", { className: "b01-proj-dots" });
|
|
for (let i = 0; i < total; i += 1) {
|
|
const state = states.get(i);
|
|
const cls =
|
|
state === "COMPLETE"
|
|
? "is-done"
|
|
: state === "STALE" || state === "FAILED"
|
|
? "is-stale"
|
|
: i === current && project.board !== "start"
|
|
? "is-cur"
|
|
: "";
|
|
row.append(el("i", { className: cls }));
|
|
}
|
|
return row;
|
|
}
|
|
|
|
/** 단계 화면으로 — 종류별 주소 */
|
|
export function openStage(project: OverviewProject, index: number): void {
|
|
const { routes } = stageFlow(project.project_kind);
|
|
const route = routes[Math.max(0, Math.min(index, routes.length - 1))];
|
|
if (route) void goToWorkflowStage(project.id, route);
|
|
}
|
|
|
|
/** 썸네일 선 — 카드 요약(547-1) 0~1 칸 좌표(y 아래로) · 비율 그대로 가운데 · 없으면 빈 판 */
|
|
export function thumbSvg(points: number[][] | null | undefined): SVGElement {
|
|
const ns = "http://www.w3.org/2000/svg";
|
|
const svg = document.createElementNS(ns, "svg");
|
|
svg.setAttribute("viewBox", "-0.1 -0.1 1.2 1.2");
|
|
svg.classList.add("b01-proj-thumb");
|
|
if (!points || points.length < 2) return svg;
|
|
const path = document.createElementNS(ns, "path");
|
|
path.setAttribute("d", points.map(([x, y], i) => `${i ? "L" : "M"}${x} ${y}`).join(" "));
|
|
svg.append(path);
|
|
return svg;
|
|
}
|
|
|
|
/** 「오늘」 · 「어제」 · 「n 일 전」 · 그 밖 날짜 */
|
|
export function ago(project: OverviewProject): string {
|
|
const days = project.idle_days;
|
|
if (days === null || days === undefined) return "-";
|
|
if (days <= 0) return "오늘";
|
|
if (days === 1) return "어제";
|
|
if (days < 7) return `${days} 일 전`;
|
|
return (project.updated_at ?? "").slice(0, 10);
|
|
}
|