Files
tech-log-frontend/src/features/tech-log/adapters/static/public-query.ts
T

222 lines
6.1 KiB
TypeScript

import {
projects,
publicRecords,
releases,
type Project,
type ProjectActivity,
type ProjectDecision,
type PublicRecord,
type RecordKind,
type Release,
type HomeFocusItem,
} from "./public-content.ts";
import type { PublicContentQueries } from "../../application/ports/public-content-queries.ts";
export type RecordFilters = {
kind?: RecordKind;
topic?: string;
project?: string;
openQuestionsOnly?: boolean;
};
export type SearchablePublicEntity = {
contentType: RecordKind | "PROJECT" | "RELEASE";
title: string;
summary: string;
path: string;
topic?: string;
topics?: ReadonlyArray<string>;
project?: string;
publishedAt?: string;
};
function comparePublishedAt(
left: Pick<PublicRecord, "publishedAt">,
right: Pick<PublicRecord, "publishedAt">,
) {
return right.publishedAt.localeCompare(left.publishedAt);
}
export function listRecords(filters: RecordFilters = {}): PublicRecord[] {
const hasTopicFilter = filters.topic !== undefined;
const hasProjectFilter = filters.project !== undefined;
const requestedTopic = filters.topic?.trim().toLocaleLowerCase("ko-KR");
const requestedProject = filters.project
?.trim()
.toLocaleLowerCase("ko-KR");
return publicRecords
.filter((record) => !filters.kind || record.kind === filters.kind)
.filter(
(record) =>
!hasTopicFilter ||
record.topic.toLocaleLowerCase("ko-KR") === requestedTopic,
)
.filter(
(record) =>
!hasProjectFilter ||
record.projectSlug.toLocaleLowerCase("ko-KR") === requestedProject ||
record.projectTitle.toLocaleLowerCase("ko-KR") === requestedProject,
)
.filter(
(record) =>
!filters.openQuestionsOnly ||
(record.kind === "QUESTION" &&
record.questionStatus !== "RESOLVED" &&
record.questionStatus !== "ARCHIVED"),
)
.sort(comparePublishedAt);
}
export function getRecord<K extends RecordKind>(
kind: K,
slug: string,
): Extract<PublicRecord, { kind: K }> | undefined {
return publicRecords.find(
(record) => record.kind === kind && record.slug === slug,
) as Extract<PublicRecord, { kind: K }> | undefined;
}
export function getProject(slug: string): Project | undefined {
return projects.find((project) => project.slug === slug);
}
export function getRelease(version: string): Release | undefined {
return releases.find((release) => release.version === version);
}
export function getProjectRecords(projectSlug: string): PublicRecord[] {
if (!getProject(projectSlug)) return [];
return listRecords({ project: projectSlug });
}
export function getProjectDecisions(projectSlug: string): ProjectDecision[] {
return [...(getProject(projectSlug)?.decisions ?? [])];
}
export function getProjectActivity(projectSlug: string): ProjectActivity[] {
return [...(getProject(projectSlug)?.activity ?? [])];
}
export function getHomeFocusItems(): HomeFocusItem[] {
const project = getProject("backend-skeleton");
const question = getRecord("QUESTION", "validate-edge-token-again");
const decision = getProjectDecisions("backend-skeleton").find(
(item) => item.id === "storage-port-unification",
);
if (!project || !question || !decision) {
throw new Error("Missing canonical entity required for the public home focus");
}
return [
{
key: "current",
label: "현재 작업",
title: project.title,
summary: project.summary,
details: [
{ label: "단계", value: project.stage },
{ label: "현재 목표", value: project.currentGoal },
{ label: "다음 작업", value: project.nextStep },
],
targetPath: `/projects/${project.slug}`,
},
{
key: "question",
label: "열린 질문",
title: question.title,
summary: question.summary,
details: [
{ label: "확인한 사실", value: question.facts[0] ?? "" },
{ label: "남은 미지수", value: question.unknowns[0] ?? "" },
{ label: "다음 검증", value: question.nextValidation },
],
targetPath: question.path,
},
{
key: "decision",
label: "최근 결정",
title: decision.statement,
summary: decision.rationale,
details: [
{ label: "영향", value: decision.consequences[0] ?? "" },
{ label: "근거", value: decision.evidence[0]?.title ?? "" },
],
targetPath: `/projects/${project.slug}/decisions#${decision.id}`,
},
];
}
function recordSearchEntity(record: PublicRecord): SearchablePublicEntity {
return {
contentType: record.kind,
title: record.title,
summary: record.summary,
path: record.path,
topic: record.topic,
project: record.projectTitle,
publishedAt: record.publishedAt,
};
}
function projectSearchEntity(project: Project): SearchablePublicEntity {
return {
contentType: "PROJECT",
title: project.title,
summary: project.summary,
path: `/projects/${project.slug}`,
topics: project.topics,
project: project.title,
};
}
function releaseSearchEntity(release: Release): SearchablePublicEntity {
return {
contentType: "RELEASE",
title: release.title,
summary: release.summary,
path: release.path,
topic: "TechLog",
project: "TechLog",
publishedAt: release.publishedAt,
};
}
export function searchPublicContent(query: string): SearchablePublicEntity[] {
const normalizedQuery = query.trim().toLocaleLowerCase("ko-KR");
const entities = [
...publicRecords.map(recordSearchEntity),
...projects.map(projectSearchEntity),
...releases.map(releaseSearchEntity),
];
if (!normalizedQuery) return entities;
return entities.filter((entity) =>
[
entity.title,
entity.summary,
entity.topic,
entity.project,
...(entity.topics ?? []),
]
.filter((value): value is string => Boolean(value))
.some((value) =>
value.toLocaleLowerCase("ko-KR").includes(normalizedQuery),
),
);
}
export const publicContentQueries = Object.freeze({
listRecords,
getRecord,
getProject,
getRelease,
getProjectRecords,
getProjectDecisions,
getProjectActivity,
getHomeFocusItems,
searchPublicContent,
}) satisfies PublicContentQueries;