Files
Aislo/B01_Dashboard/B01_Dashboard_UI_ProjectList_Parts.ts
T
eomsangdonandClaude Sonnet 5.5 59610fecf1 feat(시간 표시): 화면 시각 = 보는 사람 PC 시간대 · 서버 응답 UTC Z · 서버가 만드는 파일 글 = 한국 시각 (PLAN 593-1 · 요구 456)
- 공용 `common_util_time.ts`(날짜 · 시각 · 오늘/어제/n 일 전 · Z 없는 옛 글 = UTC) · `common_util_time.py`(UTC Z 글 · 한국 시각)
- 화면 바꿈: 대시보드(프로젝트 · 휴지통 · 시스템 로그 · 보관함 · 홈 카드) · B09 굳힘 · E03 · E05 · E07 · M02 호표 이력 · 목록 고친 날 · 지난 판
- 서버: 응답 datetime = UTC + Z(main.py) · 로컬 시각 저장 → UTC · 대시보드 경과일 = UTC 기준 · 엑셀 날짜 · 파일 이름 = 한국 시각
- 시험: test_593_time(서울 · 뉴욕 · Z 없는 옛 글 · 서버 글)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 10:03:35 +09:00

127 lines
5.1 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 { agoText } from "../common_util/common_util_time";
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 {
return agoText(project.updated_at);
}