From 9cb6258d3db811462edd7d3a54ca857287b70be0 Mon Sep 17 00:00:00 2001 From: Godopu Date: Mon, 24 Aug 2026 18:40:46 +0900 Subject: [PATCH] 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 --- refer_landing_page/.env.example | 6 + .../app/_components/ProjectPageView.tsx | 18 +- refer_landing_page/app/lectures/page.tsx | 7 +- refer_landing_page/app/members/page.tsx | 8 +- refer_landing_page/app/page.tsx | 43 ++- .../app/publications/[category]/page.tsx | 16 +- .../publications/_components/CategoryNav.tsx | 18 +- refer_landing_page/app/publications/page.tsx | 16 +- .../app/standardization/page.tsx | 6 +- refer_landing_page/lib/api.ts | 317 ++++++++++++++++++ 10 files changed, 415 insertions(+), 40 deletions(-) create mode 100644 refer_landing_page/.env.example create mode 100644 refer_landing_page/lib/api.ts diff --git a/refer_landing_page/.env.example b/refer_landing_page/.env.example new file mode 100644 index 0000000..51b3e21 --- /dev/null +++ b/refer_landing_page/.env.example @@ -0,0 +1,6 @@ +# ============================================================================== +# AI Agent Networking Lab (ANL) Next.js Frontend Configuration +# ============================================================================== + +# Backend REST API Base URL (used at build time / SSR) +API_BASE_URL=http://localhost:8080/api/v1 diff --git a/refer_landing_page/app/_components/ProjectPageView.tsx b/refer_landing_page/app/_components/ProjectPageView.tsx index fc5e9f5..f43480b 100644 --- a/refer_landing_page/app/_components/ProjectPageView.tsx +++ b/refer_landing_page/app/_components/ProjectPageView.tsx @@ -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() {

- {researchProjects.length} core international standardization research projects + {projects.length} core international standardization research projects

@@ -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) => (
diff --git a/refer_landing_page/app/lectures/page.tsx b/refer_landing_page/app/lectures/page.tsx index 0e83f89..e1d909f 100644 --- a/refer_landing_page/app/lectures/page.tsx +++ b/refer_landing_page/app/lectures/page.tsx @@ -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); diff --git a/refer_landing_page/app/members/page.tsx b/refer_landing_page/app/members/page.tsx index 97442ef..a4e3ea3 100644 --- a/refer_landing_page/app/members/page.tsx +++ b/refer_landing_page/app/members/page.tsx @@ -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 = { 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" || diff --git a/refer_landing_page/app/page.tsx b/refer_landing_page/app/page.tsx index 3b15a36..3af860f 100644 --- a/refer_landing_page/app/page.tsx +++ b/refer_landing_page/app/page.tsx @@ -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 (
@@ -66,7 +73,7 @@ export default function HomePage() {
- {intlPubsCount} + {stats.intlPubsCount}
International Journals & Conference @@ -74,7 +81,7 @@ export default function HomePage() {
- {stdDocsCount} + {stats.stdDocsCount}
Standardization Contributions @@ -82,7 +89,7 @@ export default function HomePage() {
- {patentCount} + {stats.patentCount}
Patent
@@ -101,7 +108,7 @@ export default function HomePage() { {/* Research Project PageView Component */} - + {/* 14 Research Areas Grid */}
@@ -118,12 +125,12 @@ export default function HomePage() {

- {RESEARCH_AREAS.length} fundamental networking protocols and AI system domains + {researchAreas.length} fundamental networking protocols and AI system domains

- {RESEARCH_AREAS.map((area, idx) => { + {researchAreas.map((area, idx) => { const no = String(idx + 1).padStart(2, "0"); const isOdd = (idx + 1) % 2 !== 0; diff --git a/refer_landing_page/app/publications/[category]/page.tsx b/refer_landing_page/app/publications/[category]/page.tsx index a927504..4ad2a28 100644 --- a/refer_landing_page/app/publications/[category]/page.tsx +++ b/refer_landing_page/app/publications/[category]/page.tsx @@ -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 */}
- +
{/* Record List Section */} diff --git a/refer_landing_page/app/publications/_components/CategoryNav.tsx b/refer_landing_page/app/publications/_components/CategoryNav.tsx index 8c27613..b85ed39 100644 --- a/refer_landing_page/app/publications/_components/CategoryNav.tsx +++ b/refer_landing_page/app/publications/_components/CategoryNav.tsx @@ -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 ( diff --git a/refer_landing_page/app/publications/page.tsx b/refer_landing_page/app/publications/page.tsx index 3c23ce7..c75b58a 100644 --- a/refer_landing_page/app/publications/page.tsx +++ b/refer_landing_page/app/publications/page.tsx @@ -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 */}
- +
{/* Highlights Section */} diff --git a/refer_landing_page/app/standardization/page.tsx b/refer_landing_page/app/standardization/page.tsx index 9c2b491..77c05d6 100644 --- a/refer_landing_page/app/standardization/page.tsx +++ b/refer_landing_page/app/standardization/page.tsx @@ -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 (
{/* Page Header */} diff --git a/refer_landing_page/lib/api.ts b/refer_landing_page/lib/api.ts new file mode 100644 index 0000000..d839c5d --- /dev/null +++ b/refer_landing_page/lib/api.ts @@ -0,0 +1,317 @@ +import type { + Member, + Alumnus, + Publication, + Patent, + Semester, + StandardsBody, + ResearchProject, + PubCategory, + DocStatus, +} from "../content/types"; + +const API_BASE_URL = process.env.API_BASE_URL ?? "http://localhost:8080/api/v1"; +const FETCH_TIMEOUT_MS = 3000; + +interface APIResponse { + data: T; +} + +// Raw backend response schemas (snake_case) +interface BackendResearchProject { + id: number; + slug: string; + title: string; + abstract: string; + keywords: string[]; + organization?: string | null; + standards_org?: string | null; + period?: string | null; + funder?: string | null; +} + +interface BackendResearchArea { + id: number; + name_en: string; + name_kr?: string | null; + display_order: number; +} + +interface BackendStatsSummary { + intl_publications: number; + standardization_docs: number; + patents: number; +} + +interface BackendMember { + id: number; + name_ko: string; + name_en: string; + degree: string; + affiliation?: string | null; + email?: string | null; + is_advisor: boolean; + display_order: number; +} + +interface BackendAlumnus { + id: number; + name_ko: string; + name_en: string; + degree: string; + graduated_at: string; + major?: string | null; + current_position?: string | null; +} + +interface BackendPublication { + id: number; + category: string; + title: string; + authors?: string | null; + venue: string; + volume?: string | null; + published_at: string; + kci: boolean; + doi?: string | null; + is_highlight: boolean; +} + +interface BackendPatent { + id: number; + title: string; + inventors: string; + application_no: string; + application_at: string; + registration_no?: string | null; + registration_at?: string | null; + country: string; + published_at: string; +} + +interface BackendCourse { + id: number; + semester_id: number; + code: string; + name_en: string; + name_kr?: string | null; + note?: string | null; + display_order: number; +} + +interface BackendSemester { + id: number; + year: number; + term: string; + courses: BackendCourse[]; +} + +interface BackendStandardDoc { + id: number; + body_id: number; + wg: string; + project_name: string; + title: string; + doc_ref: string; + status: string; + published_at?: string | null; +} + +interface BackendStandardProject { + wg: string; + name: string; + documents: BackendStandardDoc[]; +} + +interface BackendStandardsBody { + id: number; + org: string; + full_name: string; + scope: string; + period: string; + role?: string | null; + display_order: number; + projects: BackendStandardProject[]; +} + +/** + * Generic fetch helper with bounded timeout and safe JSON handling. + * Returns data payload or null on any failure (network, timeout, non-2xx, parse error). + */ +async function apiGet(path: string): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + const res = await fetch(`${API_BASE_URL}${path}`, { + signal: controller.signal, + }); + clearTimeout(timeout); + + if (!res.ok) { + return null; + } + + const json = (await res.json()) as APIResponse; + return json?.data ?? null; + } catch { + return null; + } +} + +// --- Home Domain --- + +export async function getResearchProjects(): Promise { + const raw = await apiGet("/research-projects"); + if (!raw) return null; + return raw.map((p) => ({ + slug: p.slug, + title: p.title, + abstract: p.abstract, + keywords: p.keywords, + standardsTrack: p.organization ?? undefined, + standardsOrg: p.standards_org ?? undefined, + period: p.period ?? undefined, + funder: p.funder ?? undefined, + deliverables: [], + })); +} + +export async function getResearchAreaNames(): Promise { + const raw = await apiGet("/research-areas"); + if (!raw) return null; + return raw.map((a) => a.name_en); +} + +export async function getStatsSummary(): Promise<{ + intlPubsCount: number; + stdDocsCount: number; + patentCount: number; +} | null> { + const raw = await apiGet("/stats/summary"); + if (!raw) return null; + return { + intlPubsCount: raw.intl_publications, + stdDocsCount: raw.standardization_docs, + patentCount: raw.patents, + }; +} + +// --- Members Domain --- + +export async function getMembers(): Promise { + const raw = await apiGet("/members"); + if (!raw) return null; + return raw.map((m) => ({ + ko: m.name_ko, + en: m.name_en, + degree: m.degree as Member["degree"], + affiliation: m.affiliation ?? undefined, + })); +} + +export async function getAlumni(): Promise { + const raw = await apiGet("/alumni"); + if (!raw) return null; + return raw.map((a) => ({ + ko: a.name_ko, + en: a.name_en, + degree: a.degree as Alumnus["degree"], + graduatedAt: a.graduated_at, + major: a.major ?? undefined, + currentPosition: a.current_position ?? undefined, + })); +} + +// --- Publications & Patents Domain --- + +export async function getPublications( + category?: "intl-journal-conf" | "domestic-journal-conf" +): Promise { + const raw = await apiGet( + category ? `/publications?category=${category}` : "/publications" + ); + if (!raw) return null; + return raw.map((p) => ({ + category: p.category as PubCategory, + title: p.title, + authors: p.authors ?? undefined, + venue: p.venue, + volume: p.volume ?? undefined, + publishedAt: p.published_at, + kci: p.kci, + doi: p.doi ?? undefined, + isHighlight: p.is_highlight, + })); +} + +export async function getHighlights(): Promise { + const raw = await apiGet("/publications/highlights"); + if (!raw) return null; + return raw.map((p) => ({ + category: p.category as PubCategory, + title: p.title, + authors: p.authors ?? undefined, + venue: p.venue, + volume: p.volume ?? undefined, + publishedAt: p.published_at, + kci: p.kci, + doi: p.doi ?? undefined, + isHighlight: p.is_highlight, + })); +} + +export async function getPatents(): Promise { + const raw = await apiGet("/patents"); + if (!raw) return null; + return raw.map((p) => ({ + category: "patent", + title: p.title, + inventors: p.inventors, + applicationNo: p.application_no, + applicationAt: p.application_at, + registrationNo: p.registration_no ?? undefined, + registrationAt: p.registration_at ?? undefined, + country: p.country, + publishedAt: p.published_at, + })); +} + +// --- Lectures Domain --- + +export async function getSemesters(): Promise { + const raw = await apiGet("/semesters"); + if (!raw) return null; + return raw.map((s) => ({ + term: s.term as Semester["term"], + year: s.year, + courses: s.courses.map((c) => ({ + code: c.code, + en: c.name_en, + note: c.note ?? undefined, + })), + })); +} + +// --- Standardization Domain --- + +export async function getStandardsBodies(): Promise { + const raw = await apiGet("/standards-bodies"); + if (!raw) return null; + return raw.map((b) => ({ + org: b.org, + full: b.full_name, + scope: b.scope as StandardsBody["scope"], + period: b.period, + role: b.role ?? undefined, + projects: b.projects.map((p) => ({ + wg: p.wg, + name: p.name, + documents: p.documents.map((d) => ({ + title: d.title, + ref: d.doc_ref, + status: d.status as DocStatus, + publishedAt: d.published_at ?? undefined, + })), + })), + })); +}