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.
123 lines
4.9 KiB
TypeScript
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>
|
|
);
|
|
}
|