Files
tech-log-frontend/src/features/tech-log/presentation/public/pages/profile-page.tsx
T
DongHyeonka c5e8735041 feat: read the profile's topics from Studio, and add working-copy deletion
Two things an author could not control from Studio.

The profile's "주요 관심 주제" was four strings in the JSX. Creating or removing
a topic in Studio changed nothing, and correcting the list meant a rebuild and
a redeploy. It now renders the published topic list. The old literal opened
with "Backend Architecture", which no record in the catalogue actually carries
— the profile was advertising a topic that did not exist, and nothing could
have caught that while the list lived in the markup.

The working-copy list gained a delete control. It routes by kind because the
contract and the storage both do: Case and Reference share one table split by
type, Question is its own. Decision has no delete — its lifecycle is accept,
reject, supersede, which records what happened rather than erasing it — so the
control does not appear for it.

The list summary carries no version, so deletion reads the working copy first
and uses the version it finds. A stale version from a list left open should
fail as a conflict, not delete whatever is there now.
2026-08-21 13:30:37 +09:00

123 lines
4.9 KiB
TypeScript

import { Link } from "react-router-dom";
import { TECH_LOG_FEATURE_ID } from "../../../application/tech-log-feature-input.ts";
import { publicSiteConfig } from "../../../contracts/public-site-config.ts";
import { usePublicContent } from "../use-public-content.tsx";
import { useApplication } from "../../../../../presentation/providers/application-provider.tsx";
const principles = [
{
title: "관찰한 사실과 판단을 나눕니다",
description:
"측정값, 문서 근거, 아직 확인하지 못한 가정을 같은 문장에 섞지 않습니다.",
},
{
title: "결론보다 경계를 남깁니다",
description:
"어떤 조건에서 선택했고 어디까지 적용할 수 있는지 함께 기록합니다.",
},
{
title: "프로젝트 맥락으로 다시 연결합니다",
description:
"Case와 Reference, Question, Decision이 따로 흩어지지 않게 실제 작업과 연결합니다.",
},
] as const;
export function ProfilePage() {
// The two project slugs this named were the static fixture's, and they exist
// in no real deployment — the page asked the backend for them, took two 404s,
// and rendered nothing but an error. "Current projects" means the published
// ones, so read them from the catalogue the projects index already reads.
const view = usePublicContent(["tech-log", "profile"], async (queries) => {
const entries = (await queries.searchPublicContent("")).filter(
(item) => item.contentType === "PROJECT",
);
const [resolved, topics] = await Promise.all([
Promise.all(entries.map((item) => queries.getProject(item.path.replace("/projects/", "")))),
queries.listTopics(),
]);
return {
currentProjects: resolved.filter((project) => project !== undefined),
topics,
};
});
// Only the project list comes from the network. Returning the page-wide
// fallback here — as every public screen did — held the operator's name, the
// principles, and the topics behind a request that has nothing to do with
// them, so a visitor saw a skeleton, then possibly an error, where the page
// could have been readable the whole time. The markup below is unchanged;
// the fallback now sits in the one section that is actually waiting.
return (
<main id="main-content" className="shell profile-page">
<header className="profile-header">
<p className="section-kicker">Profile</p>
<h1>{publicSiteConfig.operator}</h1>
<p>{publicSiteConfig.identityStatement}</p>
</header>
<section className="profile-principles" aria-labelledby="principles-title">
<div>
<p className="section-kicker">Principles</p>
<h2 id="principles-title">기록을 운영하는 원칙</h2>
</div>
<ol>
{principles.map((principle, index) => (
<li key={principle.title}>
<span>{String(index + 1).padStart(2, "0")}</span>
<div>
<h3>{principle.title}</h3>
<p>{principle.description}</p>
</div>
</li>
))}
</ol>
</section>
<section className="profile-projects" aria-labelledby="profile-projects-title">
<div>
<p className="section-kicker">Current</p>
<h2 id="profile-projects-title">현재 프로젝트</h2>
</div>
{!view.ready ? (
view.fallback
) : view.data.currentProjects.length === 0 ? (
<p className="public-empty-note">아직 공개된 프로젝트가 없습니다.</p>
) : (
<ul>
{view.data.currentProjects.map((project) => (
<li key={project.slug}>
<Link to={`/projects/${project.slug}`}>
<div>
<strong>{project.title}</strong>
<span>{project.stage}</span>
</div>
<p>{project.currentGoal}</p>
<span aria-hidden="true"></span>
</Link>
</li>
))}
</ul>
)}
</section>
{/*
이 목록은 코드에 네 개가 박혀 있었다 — Studio 에서 주제를 만들거나 지워도 프로필은
그대로였고, 고치려면 배포를 다시 해야 했다. 이제 공개 주제 목록을 그대로 그린다.
*/}
<section className="profile-topics" aria-labelledby="profile-topics-title">
<p className="section-kicker">Topics</p>
<h2 id="profile-topics-title">주요 관심 주제</h2>
{!view.ready ? (
view.fallback
) : view.data.topics.length === 0 ? (
<p className="public-empty-note">아직 등록한 주제가 없습니다.</p>
) : (
<ul>
{view.data.topics.map((topic) => (
<li key={topic.slug}>{topic.name}</li>
))}
</ul>
)}
</section>
</main>
);
}