Files
tech-log-frontend/src/features/tech-log/presentation/studio/components/studio-dashboard.tsx
T

202 lines
6.5 KiB
TypeScript

import { useEffect, useState } from "react";
import type { StudioDashboard as StudioDashboardData } from "../../../contracts/studio/contract.ts";
import type { components } from "../../../contracts/studio/generated.ts";
import { useStudio } from "../use-studio.ts";
import { GuardedStudioLink } from "./guarded-studio-link.tsx";
type DocumentSummary = components["schemas"]["DocumentSummary"];
const kindLabel = {
CASE: "Case",
REFERENCE: "Reference",
QUESTION: "Question",
} as const;
const actionLabel = {
CONTINUE_EDITING: "작성 계속",
VALIDATE: "검증하기",
FIX_VALIDATION: "오류 수정",
CREATE_PREVIEW: "미리보기 만들기",
PUBLISH: "게시하기",
NONE: "게시 완료",
} as const;
function WorkRow({ item }: Readonly<{ item: DocumentSummary }>) {
return (
<article className="studio-work-row">
<p className="studio-row-label">{kindLabel[item.kind]}</p>
<div>
<h3>
<GuardedStudioLink href={`/studio/documents/${item.id}/edit`}>
{item.title || "제목 없는 작업본"}
</GuardedStudioLink>
</h3>
<p>
{item.project?.label ?? "프로젝트 미지정"} · {actionLabel[item.nextAction]}
</p>
</div>
<time dateTime={item.updatedAt}>
{new Date(item.updatedAt).toLocaleDateString("ko-KR", {
timeZone: "Asia/Seoul",
})}
</time>
</article>
);
}
function WorkSection({
title,
items,
href,
empty,
}: Readonly<{
title: string;
items: DocumentSummary[];
href: string;
empty: string;
}>) {
return (
<section className="studio-work-section">
<div className="studio-section-title">
<h2>{title}</h2>
<GuardedStudioLink href={href}>전체 보기</GuardedStudioLink>
</div>
<div className="studio-work-list">
{items.length ? (
items.map((item) => <WorkRow key={item.id} item={item} />)
) : (
<p className="studio-empty-inline">{empty}</p>
)}
</div>
</section>
);
}
function isAbortError(error: unknown): boolean {
return error instanceof DOMException && error.name === "AbortError";
}
export function StudioDashboard() {
const { gateway } = useStudio();
const [dashboard, setDashboard] = useState<StudioDashboardData | null>(null);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
void gateway.getDashboard({ signal: controller.signal }).then(
(value) => setDashboard(value),
(reason: unknown) => {
if (!isAbortError(reason)) setError("작업 흐름을 불러오지 못했습니다.");
},
);
return () => controller.abort();
}, [gateway]);
const validationItems =
dashboard?.continueWriting.filter((item) =>
["VALIDATE", "FIX_VALIDATION", "CREATE_PREVIEW"].includes(item.nextAction),
) ?? [];
return (
<div className="studio-page studio-dashboard-page">
<header className="studio-page-top">
<div className="studio-page-heading">
<p className="studio-eyebrow">WORKSPACE</p>
<h1>작업 흐름</h1>
<p>작성 중인 기록을 이어서 정리하고 검증·게시 흐름으로 연결합니다.</p>
</div>
<GuardedStudioLink className="studio-primary-action" href="/studio/documents/new">
문서
</GuardedStudioLink>
</header>
{error ? (
<p className="studio-screen-error" role="alert">
{error}
</p>
) : null}
{!dashboard && !error ? (
<p className="studio-loading" role="status">
Studio 요약을 불러오는 중입니다.
</p>
) : null}
{dashboard ? (
<>
<section className="studio-summary-grid" aria-label="Studio 요약">
<div>
<span>전체 작업본</span>
<strong>{dashboard.totals.documents}</strong>
</div>
<div>
<span>검증할 기록</span>
<strong>{validationItems.length}</strong>
</div>
<div>
<span>게시 준비</span>
<strong>{dashboard.totals.readyToPublish}</strong>
</div>
<div>
<span>게시 기록</span>
<strong>{dashboard.totals.publications}</strong>
</div>
</section>
<WorkSection
title="이어서 작성"
items={dashboard.continueWriting}
href="/studio/documents"
empty="이어서 작성할 문서가 없습니다."
/>
<WorkSection
title="검증과 미리보기"
items={validationItems}
href="/studio/documents"
empty="현재 검증할 문서가 없습니다."
/>
<WorkSection
title="게시 준비"
items={dashboard.readyToPublish}
href="/studio/documents"
empty="게시 준비가 끝난 문서가 없습니다."
/>
<section className="studio-work-section">
<div className="studio-section-title">
<h2>최근 게시</h2>
<GuardedStudioLink href="/studio/publications">
게시 기록 보기
</GuardedStudioLink>
</div>
<div className="studio-work-list">
{dashboard.recentPublications.length ? (
dashboard.recentPublications.map((item) => (
<article
className="studio-work-row"
key={item.event.publicationEventId}
>
<p className="studio-row-label">
{item.event.type === "UNPUBLISHED"
? "게시 취소"
: item.event.type === "REPUBLISHED"
? "재게시"
: "게시"}
</p>
<div>
<h3>{item.document.title}</h3>
<p>{item.document.project?.label ?? "프로젝트 미지정"}</p>
</div>
<time dateTime={item.event.occurredAt}>
{new Date(item.event.occurredAt).toLocaleDateString("ko-KR", {
timeZone: "Asia/Seoul",
})}
</time>
</article>
))
) : (
<p className="studio-empty-inline">아직 게시 기록이 없습니다.</p>
)}
</div>
</section>
</>
) : null}
</div>
);
}