import { useCallback, useEffect, useState, type FormEvent } from "react";
import type { ProjectIndexItem, TopicEdit } from "../../../contracts/management/contract.ts";
import { slugFromName } from "./slug-from-name.ts";
import { useStudio } from "../use-studio.ts";
/**
* 주제와 프로젝트 관리.
*
*
이 화면이 존재하는 이유는 문서 발행이 주제를 요구하는데 주제를 만들 곳이 없었기 때문이다.
* 그래서 범위를 목록·생성·삭제로 끊었다 — 편집(이름 변경, 단계 전환, 공개 범위)은 계약에 있고
* 백엔드도 구현돼 있으나, 그 화면은 별도 설계가 필요하다.
*
*
새 CSS 를 만들지 않는다. 작업본 목록이 쓰는 클래스만 재사용하므로 이 화면은 Studio 의
* 나머지와 같은 간격·타이포·색을 그대로 따른다.
*/
export function TaxonomyManager() {
const { managementGateway, setRequestAnnouncement } = useStudio();
const [topics, setTopics] = useState(null);
const [projects, setProjects] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [pending, setPending] = useState(false);
const [generation, setGeneration] = useState(0);
const [topicName, setTopicName] = useState("");
const [topicSlug, setTopicSlug] = useState("");
const [projectTitle, setProjectTitle] = useState("");
const reload = useCallback(() => setGeneration((value) => value + 1), []);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError("");
void Promise.all([managementGateway.listTopics(), managementGateway.listProjects(0, 50)]).then(
([topicList, projectPage]) => {
if (cancelled) return;
setTopics(topicList);
setProjects(projectPage.items ?? []);
setLoading(false);
},
() => {
if (cancelled) return;
setError("주제와 프로젝트를 불러오지 못했습니다.");
setLoading(false);
},
);
return () => {
cancelled = true;
};
}, [managementGateway, generation]);
/**
* slug 를 비워 두면 이름에서 만든다. 한글 이름이 흔한데 이전 규칙은 한글을 전부 버려서, 이름에
* 섞인 영문·숫자만 남았다 — `인증` 은 빈 slug 가 되고 `Redis 캐시` 와 `Redis 클러스터` 는 둘 다
* `redis` 가 됐다. 지금은 로마자로 옮긴다 ({@link slugFromName}).
*/
const slugify = slugFromName;
const submitTopic = async (event: FormEvent) => {
event.preventDefault();
if (pending) return;
const name = topicName.trim();
const slug = slugify(topicSlug || topicName);
if (!name || !slug) {
setError("주제 이름과 slug 를 입력해 주세요. slug 는 영문·숫자·하이픈만 가능합니다.");
return;
}
setPending(true);
setError("");
try {
await managementGateway.createTopic({ name, slug } as TopicEdit);
setTopicName("");
setTopicSlug("");
setRequestAnnouncement(`주제 ${name} 을(를) 만들었습니다.`);
reload();
} catch {
setError("주제를 만들지 못했습니다. 같은 이름이나 slug 가 이미 있을 수 있습니다.");
} finally {
setPending(false);
}
};
const submitProject = async (event: FormEvent) => {
event.preventDefault();
if (pending) return;
const title = projectTitle.trim();
if (!title) {
setError("프로젝트 이름을 입력해 주세요.");
return;
}
setPending(true);
setError("");
try {
await managementGateway.createProject(title);
setProjectTitle("");
setRequestAnnouncement(`프로젝트 ${title} 을(를) 만들었습니다.`);
reload();
} catch {
setError("프로젝트를 만들지 못했습니다.");
} finally {
setPending(false);
}
};
const removeTopic = async (topic: TopicEdit) => {
if (pending || topic.id === undefined || topic.version === undefined) return;
setPending(true);
setError("");
try {
await managementGateway.deleteTopic(topic.id, topic.version);
setRequestAnnouncement(`주제 ${topic.name} 을(를) 삭제했습니다.`);
reload();
} catch {
setError("주제를 삭제하지 못했습니다. 이 주제를 쓰는 기록이 있을 수 있습니다.");
} finally {
setPending(false);
}
};
const removeProject = async (project: ProjectIndexItem) => {
if (pending) return;
setPending(true);
setError("");
try {
await managementGateway.deleteProject(project.id, project.version);
setRequestAnnouncement(`프로젝트 ${project.name} 을(를) 삭제했습니다.`);
reload();
} catch {
setError("프로젝트를 삭제하지 못했습니다. 연결된 기록이 있을 수 있습니다.");
} finally {
setPending(false);
}
};
return (
{loading ? (
주제와 프로젝트를 불러오는 중입니다.
) : null}
{error ? (
{error}
) : null}
{!loading && topics ? (
<>
{topics.length}개의 주제
{topics.length ? (
{topics.map((topic) => (
TOPIC
{topic.name}
{topic.slug}
- 상태
- {topic.status === "ARCHIVED" ? "보관" : "사용 중"}
))}
) : (
아직 주제가 없습니다. 위에서 하나 만들어 주세요.
)}
>
) : null}
{!loading && projects ? (
<>
{projects.length}개의 프로젝트
{projects.length ? (
{projects.map((project) => (
PROJECT
{project.name}
{project.currentObjective ?? "목표 미지정"}
- 단계
- {project.phase}
- 공개
- {project.targetVisibility}
))}
) : (
아직 프로젝트가 없습니다.
)}
>
) : null}
);
}