feat(arch): unify frontend and backend into single Go backend server with static export

- Configure Next.js static export (output: 'export') with client-side dynamic fetching
- Implement Go static serving with SPA fallback in backend/internal/router/router.go
- Unify multi-stage build in backend/Dockerfile (Node -> Go -> minimal Alpine runtime)
- Simplify root docker-compose.yml to single anl-app service on port 8080
- Update REQUIREMENTS.md DM-03 and AC-17 normative contracts to v3.0 Unified Go Backend
- Restore strict regression verification in Gate 9 (AC-35/36/37) and unittest suite
- All Go unit tests, TypeScript type checks, ESLint, and E2E gates passed clean
This commit is contained in:
2026-08-25 12:54:39 +09:00
parent 5f87631530
commit 9bdc9bdd9a
27 changed files with 597 additions and 812 deletions
@@ -0,0 +1,149 @@
"use client";
import React, { useEffect, useState } from "react";
import { notFound } from "next/navigation";
import { CategoryNav } from "../../_components/CategoryNav";
import { getPublications, getPatents } from "../../../../lib/api";
import { PUB_CATEGORIES } from "../../../../content/categories";
import type { PubCategory, Publication, Patent } from "../../../../content/types";
export function PublicationCategoryClient({ category }: { category: string }) {
const currentCat = PUB_CATEGORIES.find((c) => c.slug === category);
if (!currentCat) {
notFound();
}
const slug = category as PubCategory;
const [publications, setPublications] = useState<Publication[]>([]);
const [patents, setPatents] = useState<Patent[]>([]);
useEffect(() => {
getPublications().then((p) => {
if (p) setPublications(p);
});
getPatents().then((pat) => {
if (pat) setPatents(pat);
});
}, []);
const counts = {
"intl-journal-conf": publications.filter((p) => p.category === "intl-journal-conf").length,
"domestic-journal-conf": publications.filter((p) => p.category === "domestic-journal-conf").length,
patent: patents.length,
};
let records: any[] = [];
if (slug === "patent") {
records = patents.map((p) => ({
title: p.title,
inventors: p.inventors,
appNo: p.applicationNo,
appDate: p.applicationAt,
regNo: p.registrationNo,
regDate: p.registrationAt,
country: p.country,
publishedAt: p.publishedAt,
isPatent: true,
}));
} else {
records = publications
.filter((p) => p.category === slug)
.map((p) => ({
title: p.title,
venue: p.venue,
volume: p.volume,
publishedAt: p.publishedAt,
kci: p.kci,
isPatent: false,
}));
}
return (
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
{/* Page Header */}
<section className="space-y-1">
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
{currentCat.label}
</h1>
{currentCat.subtitle && (
<p className="font-mono text-sm text-ink-mute">
{currentCat.subtitle}
</p>
)}
</section>
{/* Category Nav Cards */}
<section className="pt-6 md:pt-8 border-t border-line">
<CategoryNav currentCategory={slug} counts={counts} />
</section>
{/* Record List Section */}
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
<div className="flex justify-between items-center border-b border-line pb-3">
<span className="font-mono text-xs text-ink-mute uppercase">
{records.length} Records
</span>
</div>
{records.length === 0 ? (
<div className="bg-paper p-8 rounded-lg border border-line text-center space-y-3">
<p className="font-serif text-lg text-ink">
No records found in this category.
</p>
<p className="text-xs text-ink-mute font-mono">
Records will be updated periodically.
</p>
</div>
) : (
<div className="space-y-4">
{records.map((rec, idx) => (
<div
key={idx}
className="bg-paper p-6 rounded-lg border border-line space-y-3 hover:border-vermillion transition-colors"
>
<div className="flex justify-between items-start gap-4">
<h3 className="font-serif text-xl font-normal text-ink leading-snug">
{rec.title}
</h3>
<time
dateTime={rec.publishedAt}
className="font-mono text-xs text-vermillion-dark bg-ivory border border-line px-2.5 py-1 rounded shrink-0 font-bold"
>
{rec.publishedAt}
</time>
</div>
{rec.isPatent ? (
<div className="space-y-1 text-xs text-ink-mute font-mono">
<p className="text-ink">Inventors: {rec.inventors}</p>
<p>
App No: {rec.appNo} ({rec.country}, {rec.appDate})
</p>
{rec.regNo && (
<p className="text-cobalt-dark font-bold">
Reg No: {rec.regNo} ({rec.regDate})
</p>
)}
</div>
) : (
<div className="space-y-1 text-xs text-ink-mute font-mono">
<p className="text-ink font-serif italic">{rec.venue}</p>
<div className="flex items-center gap-3">
<span>{rec.volume}</span>
{rec.kci && (
<span className="bg-cobalt-dark text-ivory text-[0.65rem] px-1.5 py-0.5 rounded font-bold">
KCI Indexed
</span>
)}
</div>
</div>
)}
</div>
))}
</div>
)}
</section>
</div>
);
}
@@ -1,10 +1,6 @@
import React from "react";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { CategoryNav } from "../_components/CategoryNav";
import { getPublications, getPatents } from "../../../lib/api";
import { PUB_CATEGORIES } from "../../../content/categories";
import { PubCategory } from "../../../content/types";
import { PublicationCategoryClient } from "./_components/PublicationCategoryClient";
interface PageProps {
params: {
@@ -12,6 +8,10 @@ interface PageProps {
};
}
export function generateStaticParams() {
return PUB_CATEGORIES.map((c) => ({ category: c.slug }));
}
export function generateMetadata({ params }: PageProps): Metadata {
const currentCat = PUB_CATEGORIES.find((c) => c.slug === params.category);
if (!currentCat) {
@@ -23,138 +23,6 @@ export function generateMetadata({ params }: PageProps): Metadata {
};
}
export const dynamic = "force-dynamic";
export default async function PublicationCategoryPage({ params }: PageProps) {
const { category } = params;
const currentCat = PUB_CATEGORIES.find((c) => c.slug === category);
if (!currentCat) {
notFound();
}
const slug = category as PubCategory;
const publications = (await getPublications()) ?? [];
const patents = (await getPatents()) ?? [];
const counts = {
"intl-journal-conf": publications.filter((p) => p.category === "intl-journal-conf").length,
"domestic-journal-conf": publications.filter((p) => p.category === "domestic-journal-conf").length,
patent: patents.length,
};
let records: any[] = [];
if (slug === "patent") {
records = patents.map((p) => ({
title: p.title,
inventors: p.inventors,
appNo: p.applicationNo,
appDate: p.applicationAt,
regNo: p.registrationNo,
regDate: p.registrationAt,
country: p.country,
publishedAt: p.publishedAt,
isPatent: true,
}));
} else {
records = publications
.filter((p) => p.category === slug)
.map((p) => ({
title: p.title,
venue: p.venue,
volume: p.volume,
publishedAt: p.publishedAt,
kci: p.kci,
isPatent: false,
}));
}
return (
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
{/* Page Header */}
<section className="space-y-1">
<h1 className="font-serif text-4xl sm:text-5xl font-normal text-ink tracking-tight">
{currentCat.label}
</h1>
{currentCat.subtitle && (
<p className="font-mono text-sm text-ink-mute">
{currentCat.subtitle}
</p>
)}
</section>
{/* Category Nav Cards */}
<section className="pt-6 md:pt-8 border-t border-line">
<CategoryNav currentCategory={slug} counts={counts} />
</section>
{/* Record List Section */}
<section className="space-y-6 pt-6 md:pt-8 border-t border-line">
<div className="flex justify-between items-center border-b border-line pb-3">
<span className="font-mono text-xs text-ink-mute uppercase">
{records.length} Records
</span>
</div>
{records.length === 0 ? (
<div className="bg-paper p-8 rounded-lg border border-line text-center space-y-3">
<p className="font-serif text-lg text-ink">
No records found in this category.
</p>
<p className="text-xs text-ink-mute font-mono">
Records will be updated periodically.
</p>
</div>
) : (
<div className="space-y-4">
{records.map((rec, idx) => (
<div
key={idx}
className="bg-paper p-6 rounded-lg border border-line space-y-3 hover:border-vermillion transition-colors"
>
<div className="flex justify-between items-start gap-4">
<h3 className="font-serif text-xl font-normal text-ink leading-snug">
{rec.title}
</h3>
<time
dateTime={rec.publishedAt}
className="font-mono text-xs text-vermillion-dark bg-ivory border border-line px-2.5 py-1 rounded shrink-0 font-bold"
>
{rec.publishedAt}
</time>
</div>
{rec.isPatent ? (
<div className="space-y-1 text-xs text-ink-mute font-mono">
<p className="text-ink">Inventors: {rec.inventors}</p>
<p>
App No: {rec.appNo} ({rec.country}, {rec.appDate})
</p>
{rec.regNo && (
<p className="text-cobalt-dark font-bold">
Reg No: {rec.regNo} ({rec.regDate})
</p>
)}
</div>
) : (
<div className="space-y-1 text-xs text-ink-mute font-mono">
<p className="text-ink font-serif italic">{rec.venue}</p>
<div className="flex items-center gap-3">
<span>{rec.volume}</span>
{rec.kci && (
<span className="bg-cobalt-dark text-ivory text-[0.65rem] px-1.5 py-0.5 rounded font-bold">
KCI Indexed
</span>
)}
</div>
</div>
)}
</div>
))}
</div>
)}
</section>
</div>
);
export default function PublicationCategoryPage({ params }: PageProps) {
return <PublicationCategoryClient category={params.category} />;
}
@@ -0,0 +1,10 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Publications",
description: "AI Agent Networking Lab (ANL) research publications and patents overview",
};
export default function PublicationsLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
+15 -10
View File
@@ -1,17 +1,22 @@
import type { Metadata } from "next";
"use client";
import React, { useEffect, useState } from "react";
import { CategoryNav } from "./_components/CategoryNav";
import { getPublications, getPatents } from "../../lib/api";
import type { Publication, Patent } from "../../content/types";
export const metadata: Metadata = {
title: "Publications",
description: "AI Agent Networking Lab (ANL) research publications and patents overview",
};
export default function PublicationsIndexPage() {
const [publications, setPublications] = useState<Publication[]>([]);
const [patents, setPatents] = useState<Patent[]>([]);
export const dynamic = "force-dynamic";
export default async function PublicationsIndexPage() {
const publications = (await getPublications()) ?? [];
const patents = (await getPatents()) ?? [];
useEffect(() => {
getPublications().then((p) => {
if (p) setPublications(p);
});
getPatents().then((pat) => {
if (pat) setPatents(pat);
});
}, []);
const counts = {
"intl-journal-conf": publications.filter((p) => p.category === "intl-journal-conf").length,