202 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
}
|