/* ============================================================================= * B01_Dashboard_UI_Home.ts * 대시보드 홈(PLAN 547-2 · 요구 369~374) — 진행 중 프로젝트 수에 따라 꼴이 저절로 바뀜 * 1 = 큰 카드 · 2~3 = 중간 카드 · 4~8 = 한 줄 목록 + 팀원별 · 단계 분포 · 9 넘음 = 요약 · 그래프 * 오른쪽 = 처리할 일 · 보관함 · 회사(회사 관리자) · 예정노선(산림청) · 처음이면 세 걸음 * 꼴 고정(자동 · 작게 · 크게) = localStorage 기억 * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { fetchHome, type HomeData, type ProjectSummary } from "./B01_Dashboard_Api_Home"; import { openNewProjectPicker } from "./B01_Dashboard_UI_NewProject"; import { bigCard, midCard, rowCard, type Go } from "./B01_Dashboard_UI_Home_Cards"; import { manyView, memberPane, pane, stagePane } from "./B01_Dashboard_UI_Home_Many"; import type { ViewCtx } from "./B01_Dashboard_UI_Views"; import { homeLayout, type HomeFixed, type HomeLayout } from "./logic/B01_Dashboard_Home_Layout"; import "./B01_Dashboard_UI_Style_Home.css"; const FIXED_KEY = "aislo.b01.homeLayout"; const LAYOUT_TEXT: Record = { empty: "처음 시작", one: "진행 중 1 개 → 큰 카드", few: "진행 중 2~3 개 → 중간 카드", list: "진행 중 4~8 개 → 한 줄 목록", many: "진행 중 9 개 넘음 → 요약 · 그래프", }; function readFixed(): HomeFixed { const saved = localStorage.getItem(FIXED_KEY); return saved === "small" || saved === "large" ? saved : "auto"; } function fixedSwitch(current: HomeFixed, onPick: (next: HomeFixed) => void): HTMLElement { const seg = el("div", { className: "b01-home__seg", attrs: { role: "group", "aria-label": "꼴" }, }); for (const [value, label] of [ ["auto", "자동"], ["small", "작게"], ["large", "크게"], ] as const) { const b = el("button", { className: value === current ? "on" : "", text: label, attrs: { type: "button", "aria-pressed": String(value === current) }, }); b.addEventListener("click", () => onPick(value)); seg.append(b); } return seg; } function todoRow(dot: string, text: string, action: string, onClick: () => void): HTMLElement { return el("div", { className: "b01-home__todo", children: [ el("span", { className: `b01-home__dot b01-home__dot--${dot}` }), el("span", { text }), createButton({ label: action, variant: "ghost", size: "sm", onClick }), ], }); } /** 처리할 일 — 없으면 「처리할 일 없음」 */ function todoPane(data: HomeData, go: Go): HTMLElement { const t = data.todos; const rows: HTMLElement[] = []; for (const p of t.new_plans) rows.push( todoRow("plan", `새 예정노선 — ${p.project_name} ${p.year}`, "보기", () => go("assigned")), ); for (const p of t.updated_plans) rows.push( todoRow("warn", `업데이트 — ${p.project_name} ${p.year}`, "보기", () => go("assigned")), ); if (t.join_requests) rows.push(todoRow("alert", `가입 요청 ${t.join_requests} 건`, "처리", () => go("people"))); for (const b of t.expiring_temp) rows.push( todoRow("warn", `보관 파일 ${b.name} · ${b.days_left} 일 남음`, "열기", () => go("temp")), ); if (!rows.length) rows.push(el("div", { className: "b01-home__muted", text: "처리할 일 없음" })); return pane("처리할 일", ...rows); } function sidePanes(data: HomeData, go: Go): HTMLElement[] { const panes = [todoPane(data, go)]; panes.push( pane( "임시 보관함", el("div", { className: "b01-home__muted", text: `보관 묶음 ${data.todos.temp_count} 개` }), createButton({ label: "보관함 ›", variant: "ghost", size: "sm", onClick: () => go("temp") }), ), ); if (data.company) { const c = data.company; panes.push( pane( c.name ?? "회사", el("div", { className: "b01-home__muted", text: `팀원 ${c.member_count ?? 0} 명 · 진행 중 ${data.projects.length} 개`, }), createButton({ label: "사람 ›", variant: "ghost", size: "sm", onClick: () => go("people"), }), ), ); } if (data.forest) { const f = data.forest; panes.push( pane( "예정노선", el("div", { className: "b01-home__muted", text: `분석 전 ${f.plan_status.draft} · 분석됨 ${f.plan_status.analyzed} · 확정 ${f.plan_status.confirmed}`, }), el("div", { className: f.assign_waiting ? "b01-home__stale" : "b01-home__muted", text: `업체 지정 대기 ${f.assign_waiting}`, }), ), ); } return panes; } /** 처음 — 세 걸음(내 정보 · 파일 · 첫 프로젝트) */ function firstSteps(ctx: ViewCtx, data: HomeData): HTMLElement { const me = ctx.me; const steps: [boolean, string, string, () => void][] = [ [ Boolean(me.position || me.department || me.phone), "내 정보 채우기", "내 정보 ›", () => ctx.go("me"), ], [data.todos.temp_count > 0, "측량 파일 올려 두기", "보관함 ›", () => ctx.go("temp")], [false, "첫 프로젝트 만들기", "+ 새 프로젝트", () => void openNewProjectPicker()], ]; return pane( "처음 오셨네요 — 세 걸음", ...steps.map(([done, text, action, onClick], i) => el("div", { className: `b01-home__step${done ? " b01-home__step--done" : ""}`, children: [ el("span", { className: "b01-home__step-no", text: done ? "✓" : String(i + 1) }), el("span", { text }), createButton({ label: action, variant: done ? "ghost" : "filled", size: "sm", onClick }), ], }), ), ); } function mainArea(layout: HomeLayout, ctx: ViewCtx, data: HomeData): HTMLElement { const cards: ProjectSummary[] = data.projects; const go = ctx.go; if (layout === "empty") return data.project_total ? pane( "진행 중인 프로젝트 없음", el("div", { className: "b01-home__muted", text: "모두 끝났습니다." }), ) : firstSteps(ctx, data); if (layout === "one") return bigCard(cards[0], go); if (layout === "few") { const mids = el("div", { className: "b01-home__mids", children: cards.slice(0, 8).map((c, i) => midCard(c, go, i === 0)), }); if (cards.length <= 8) return mids; const all = createButton({ label: "모든 프로젝트 ›", onClick: () => go("projects") }); return el("div", { className: "b01-home__many", children: [mids, all] }); } if (layout === "list") return el("div", { className: "b01-home__split b01-home__split--list", children: [ el("div", { children: [ el("div", { className: "b01-home__muted", text: `진행 중 ${cards.length} · 최근 작업 순`, }), el("div", { className: "b01-home__rows", children: cards.map((c) => rowCard(c, go)) }), ], }), el("div", { children: [memberPane(cards), stagePane(cards)] }), ], }); return manyView(data, go); } export function mountHome(host: HTMLElement, ctx: ViewCtx): () => void { let alive = true; let data: HomeData | null = null; const root = el("div", { className: "b01-home" }); host.append(root); const draw = (): void => { if (!data) return; const fixed = readFixed(); const layout = homeLayout(data.projects.length, fixed); root.dataset.layout = layout; const head = el("div", { className: "b01-home__head", children: [ el("h2", { text: "홈" }), el("span", { className: "b01-home__muted", text: fixed === "auto" || layout === "empty" ? LAYOUT_TEXT[layout] : `진행 중 ${data.projects.length} 개 · ${fixed === "small" ? "작게" : "크게"} 고정`, }), el("span", { className: "b01-home__push" }), fixedSwitch(fixed, (next) => { if (next === "auto") localStorage.removeItem(FIXED_KEY); else localStorage.setItem(FIXED_KEY, next); draw(); }), createButton({ label: "+ 새 프로젝트", onClick: () => void openNewProjectPicker() }), ], }); const body = el("div", { className: "b01-home__body", children: [ el("div", { className: "b01-home__main", children: [mainArea(layout, ctx, data)] }), el("aside", { className: "b01-home__side", children: sidePanes(data, ctx.go) }), ], }); root.replaceChildren(head, body); }; root.append(el("div", { className: "b01-home__muted", text: "불러오는 중…" })); fetchHome() .then((loaded) => { if (!alive) return; data = loaded; draw(); }) .catch((error: unknown) => { if (!alive) return; root.replaceChildren( el("div", { className: "b01-home__muted", text: "홈을 불러오지 못했습니다." }), ); showToast(error instanceof Error ? error.message : String(error), "error"); }); return () => { alive = false; }; }