Files
Aislo/B01_Dashboard/B01_Dashboard_UI_ProjectList_Parts.ts
T
eomsangdonandClaude Opus 5.5 070791854c feat(B01): 프로젝트 상세 화면 · 카드 요약 연결 · 지킴 시험 (PLAN 548-2)
- 상세 화면(view=project) — 머리 · [⋯] · [작업 열기 — 지금 단계 ›] · 단계 진행(누름 = 그 단계) ·
  노선 지도(배경 타일 + 선 · 시점 · 종점) · 핵심 숫자 · 기본 정보 · 연결된 것 · 산출물 · 최근 변경(있는 칸만)
- 목록 · 상세 숫자 · 썸네일 = 547-1 카드 요약(project_summaries) 한 벌
- 색 = 셸 변수(--b01-*) 이름에 맞춤
- 시험 test_548_project_overview — 칸반 나눔 · 산출물 · 내 것 · 단계 수 짝

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 18:08:18 +09:00

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);
}