feat(frontend): connect Next.js server components to Go backend REST API with resilient fallback
- Implement typed REST API client in refer_landing_page/lib/api.ts - Support API_BASE_URL environment variable with graceful fallback to static content - Connect Home page (projects, areas, stats), Members page (active, alumni), Publications (highlights, list, patents), Lectures, and Standardization pages to backend endpoints - Pass all 12 SSG build, TypeScript, ESLint, and E2E quality gates
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import React from "react";
|
||||
import { researchProjects } from "../../content/projects";
|
||||
import { researchProjects as staticProjects } from "../../content/projects";
|
||||
import type { ResearchProject } from "../../content/types";
|
||||
import ProjectPageViewControls from "./ProjectPageViewControls";
|
||||
|
||||
export function ProjectPageView() {
|
||||
const slugs = researchProjects.map((p) => p.slug);
|
||||
interface ProjectPageViewProps {
|
||||
projects?: ResearchProject[];
|
||||
}
|
||||
|
||||
export function ProjectPageView({ projects: projectsProp }: ProjectPageViewProps = {}) {
|
||||
const projects = projectsProp ?? staticProjects;
|
||||
const slugs = projects.map((p) => p.slug);
|
||||
const trackId = "research-projects-track";
|
||||
|
||||
return (
|
||||
@@ -21,7 +27,7 @@ export function ProjectPageView() {
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs text-ink-mute font-mono">
|
||||
{researchProjects.length} core international standardization research projects
|
||||
{projects.length} core international standardization research projects
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -34,13 +40,13 @@ export function ProjectPageView() {
|
||||
aria-label="연구 프로젝트 목록"
|
||||
className="flex gap-6 overflow-x-auto snap-x snap-mandatory scroll-smooth [scrollbar-width:none] [&::-webkit-scrollbar]:hidden desktop:grid desktop:grid-cols-3 desktop:overflow-visible focus:outline-none focus:ring-1 focus:ring-cobalt/50 rounded"
|
||||
>
|
||||
{researchProjects.map((proj, idx) => (
|
||||
{projects.map((proj, idx) => (
|
||||
<article
|
||||
key={proj.slug}
|
||||
id={`project-${proj.slug}`}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label={`${researchProjects.length}개 중 ${idx + 1}번째 프로젝트`}
|
||||
aria-label={`${projects.length}개 중 ${idx + 1}번째 프로젝트`}
|
||||
className="snap-start shrink-0 min-w-0 w-full md:w-[calc(50%-12px)] desktop:w-full flex flex-col justify-between bg-paper p-6 rounded-lg border border-line hover:border-cobalt/60 transition-colors space-y-5 break-words [word-break:keep-all]"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { semesters } from "../../content/lectures";
|
||||
import { semesters as staticSemesters } from "../../content/lectures";
|
||||
import { getSemesters } from "../../lib/api";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Lectures",
|
||||
description: "AI Agent Networking Lab (ANL) lecture courses and curriculum history",
|
||||
};
|
||||
|
||||
export default function LecturesPage() {
|
||||
export default async function LecturesPage() {
|
||||
const semesters = (await getSemesters()) ?? staticSemesters;
|
||||
|
||||
const recentSemesters = semesters.slice(0, 3);
|
||||
const olderSemesters = semesters.slice(3);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { activeMembers, alumni } from "../../content/members";
|
||||
import { activeMembers as staticMembers, alumni as staticAlumni } from "../../content/members";
|
||||
import { getMembers, getAlumni } from "../../lib/api";
|
||||
import { ProfessorDetailsDialog } from "./_components/ProfessorDetailsDialog";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -18,7 +19,10 @@ const DEGREE_LABEL: Record<string, string> = {
|
||||
MS: "M. S.",
|
||||
};
|
||||
|
||||
export default function MembersPage() {
|
||||
export default async function MembersPage() {
|
||||
const activeMembers = (await getMembers()) ?? staticMembers;
|
||||
const alumni = (await getAlumni()) ?? staticAlumni;
|
||||
|
||||
const phdStudents = activeMembers.filter(
|
||||
(m) =>
|
||||
m.degree === "PhD" ||
|
||||
|
||||
@@ -2,8 +2,10 @@ import React from "react";
|
||||
import Link from "next/link";
|
||||
import HeroComposition from "./_components/HeroComposition";
|
||||
import { ProjectPageView } from "./_components/ProjectPageView";
|
||||
import { publications, patents } from "../content/publications";
|
||||
import { standardsBodies } from "../content/standards";
|
||||
import { publications as staticPubs, patents as staticPatents } from "../content/publications";
|
||||
import { standardsBodies as staticStandards } from "../content/standards";
|
||||
import { researchProjects as staticProjects } from "../content/projects";
|
||||
import { getStatsSummary, getResearchAreaNames, getResearchProjects } from "../lib/api";
|
||||
|
||||
// CUSTOMIZATION HOOK: 14 Core Research Areas
|
||||
const RESEARCH_AREAS: string[] = [
|
||||
@@ -31,16 +33,21 @@ const INTERNATIONAL_SDOS = [
|
||||
"ISO/IEC JTC1/SC6: Reliable Multicasting",
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
const intlPubsCount = publications.filter(
|
||||
(p) => p.category === "intl-journal-conf"
|
||||
).length;
|
||||
const patentCount = patents.length;
|
||||
const stdDocsCount = standardsBodies.reduce(
|
||||
(sum, b) =>
|
||||
sum + b.projects.reduce((pSum, p) => pSum + p.documents.length, 0),
|
||||
0
|
||||
);
|
||||
export default async function HomePage() {
|
||||
const stats = (await getStatsSummary()) ?? {
|
||||
intlPubsCount: staticPubs.filter(
|
||||
(p) => p.category === "intl-journal-conf"
|
||||
).length,
|
||||
patentCount: staticPatents.length,
|
||||
stdDocsCount: staticStandards.reduce(
|
||||
(sum, b) =>
|
||||
sum + b.projects.reduce((pSum, p) => pSum + p.documents.length, 0),
|
||||
0
|
||||
),
|
||||
};
|
||||
|
||||
const researchAreas = (await getResearchAreaNames()) ?? RESEARCH_AREAS;
|
||||
const researchProjects = (await getResearchProjects()) ?? staticProjects;
|
||||
|
||||
return (
|
||||
<div className="container-content space-y-16 md:space-y-24 pt-10 md:pt-16 pb-16 md:pb-24">
|
||||
@@ -66,7 +73,7 @@ export default function HomePage() {
|
||||
<div className="pt-6 border-t border-line grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<div className="font-serif text-3xl text-cobalt-dark font-normal">
|
||||
{intlPubsCount}
|
||||
{stats.intlPubsCount}
|
||||
</div>
|
||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
|
||||
International Journals & Conference
|
||||
@@ -74,7 +81,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-serif text-3xl text-cobalt-dark font-normal">
|
||||
{stdDocsCount}
|
||||
{stats.stdDocsCount}
|
||||
</div>
|
||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
|
||||
Standardization Contributions
|
||||
@@ -82,7 +89,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-serif text-3xl text-cobalt-dark font-normal">
|
||||
{patentCount}
|
||||
{stats.patentCount}
|
||||
</div>
|
||||
<div className="font-mono text-xs text-ink-mute uppercase tracking-wider mt-1 leading-tight">
|
||||
Patent<br />
|
||||
@@ -101,7 +108,7 @@ export default function HomePage() {
|
||||
</section>
|
||||
|
||||
{/* Research Project PageView Component */}
|
||||
<ProjectPageView />
|
||||
<ProjectPageView projects={researchProjects} />
|
||||
|
||||
{/* 14 Research Areas Grid */}
|
||||
<section className="space-y-8 pt-16 md:pt-20 border-t border-line">
|
||||
@@ -118,12 +125,12 @@ export default function HomePage() {
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs text-ink-mute font-mono">
|
||||
{RESEARCH_AREAS.length} fundamental networking protocols and AI system domains
|
||||
{researchAreas.length} fundamental networking protocols and AI system domains
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{RESEARCH_AREAS.map((area, idx) => {
|
||||
{researchAreas.map((area, idx) => {
|
||||
const no = String(idx + 1).padStart(2, "0");
|
||||
const isOdd = (idx + 1) % 2 !== 0;
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { CategoryNav } from "../_components/CategoryNav";
|
||||
import { publications, patents } from "../../../content/publications";
|
||||
import { publications as staticPubs, patents as staticPatents } from "../../../content/publications";
|
||||
import { getPublications, getPatents } from "../../../lib/api";
|
||||
import { PUB_CATEGORIES } from "../../../content/categories";
|
||||
import { PubCategory } from "../../../content/types";
|
||||
|
||||
@@ -30,7 +31,7 @@ export function generateStaticParams() {
|
||||
}));
|
||||
}
|
||||
|
||||
export default function PublicationCategoryPage({ params }: PageProps) {
|
||||
export default async function PublicationCategoryPage({ params }: PageProps) {
|
||||
const { category } = params;
|
||||
const currentCat = PUB_CATEGORIES.find((c) => c.slug === category);
|
||||
|
||||
@@ -40,6 +41,15 @@ export default function PublicationCategoryPage({ params }: PageProps) {
|
||||
|
||||
const slug = category as PubCategory;
|
||||
|
||||
const publications = (await getPublications()) ?? staticPubs;
|
||||
const patents = (await getPatents()) ?? staticPatents;
|
||||
|
||||
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) => ({
|
||||
@@ -82,7 +92,7 @@ export default function PublicationCategoryPage({ params }: PageProps) {
|
||||
|
||||
{/* Category Nav Cards */}
|
||||
<section className="pt-6 md:pt-8 border-t border-line">
|
||||
<CategoryNav currentCategory={slug} />
|
||||
<CategoryNav currentCategory={slug} counts={counts} />
|
||||
</section>
|
||||
|
||||
{/* Record List Section */}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { PUB_CATEGORIES } from "../../../content/categories";
|
||||
import { publications, patents } from "../../../content/publications";
|
||||
import { publications as staticPubs, patents as staticPatents } from "../../../content/publications";
|
||||
import { PubCategory } from "../../../content/types";
|
||||
|
||||
export interface CategoryCounts {
|
||||
"intl-journal-conf"?: number;
|
||||
"domestic-journal-conf"?: number;
|
||||
patent?: number;
|
||||
}
|
||||
|
||||
interface CategoryNavProps {
|
||||
currentCategory?: PubCategory;
|
||||
counts?: CategoryCounts;
|
||||
}
|
||||
|
||||
export function CategoryNav({ currentCategory }: CategoryNavProps) {
|
||||
export function CategoryNav({ currentCategory, counts }: CategoryNavProps) {
|
||||
const getCount = (slug: string) => {
|
||||
if (slug === "patent") return patents.length;
|
||||
return publications.filter((p) => p.category === slug).length;
|
||||
if (counts?.[slug as keyof CategoryCounts] !== undefined) {
|
||||
return counts[slug as keyof CategoryCounts]!;
|
||||
}
|
||||
if (slug === "patent") return staticPatents.length;
|
||||
return staticPubs.filter((p) => p.category === slug).length;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import { CategoryNav } from "./_components/CategoryNav";
|
||||
import { publications } from "../../content/publications";
|
||||
import { publications as staticPubs, patents as staticPatents } from "../../content/publications";
|
||||
import { getPublications, getPatents } from "../../lib/api";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Publications",
|
||||
description: "AI Agent Networking Lab (ANL) research publications and patents overview",
|
||||
};
|
||||
|
||||
export default function PublicationsIndexPage() {
|
||||
export default async function PublicationsIndexPage() {
|
||||
const publications = (await getPublications()) ?? staticPubs;
|
||||
const patents = (await getPatents()) ?? staticPatents;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
// Selected representative publications (curated via database isHighlight flag)
|
||||
const highlights = publications.filter((p) => p.isHighlight);
|
||||
|
||||
@@ -22,7 +32,7 @@ export default function PublicationsIndexPage() {
|
||||
|
||||
{/* Category Summary Cards */}
|
||||
<section className="pt-6 md:pt-8 border-t border-line">
|
||||
<CategoryNav />
|
||||
<CategoryNav counts={counts} />
|
||||
</section>
|
||||
|
||||
{/* Highlights Section */}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { standardsBodies } from "../../content/standards";
|
||||
import { standardsBodies as staticStandards } from "../../content/standards";
|
||||
import { getStandardsBodies } from "../../lib/api";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Standardization",
|
||||
description: "AI Agent Networking Lab (ANL) international standardization research and contributions",
|
||||
};
|
||||
|
||||
export default function StandardizationPage() {
|
||||
export default async function StandardizationPage() {
|
||||
const standardsBodies = (await getStandardsBodies()) ?? staticStandards;
|
||||
return (
|
||||
<div className="container-content py-8 sm:py-12 space-y-10 sm:space-y-14">
|
||||
{/* Page Header */}
|
||||
|
||||
Reference in New Issue
Block a user