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; project?: string; publishedAt?: string; }; function comparePublishedAt( left: Pick, right: Pick, ) { 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( kind: K, slug: string, ): Extract | undefined { return publicRecords.find( (record) => record.kind === kind && record.slug === slug, ) as Extract | 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;