Files
Godopu 5041ab1ea8 feat(frontend): convert to live real-time dynamic runtime fetching with force-dynamic and cache no-store
- Set cache: 'no-store' in lib/api.ts to trigger real-time REST API requests on every page load
- Add export const dynamic = 'force-dynamic' across all 6 app pages (/home, /members, /publications, /lectures, /standardization)
- Update REQUIREMENTS.md and E2E test runner to validate live server routes and dynamic runtime fetching
- Pass all 12 quality gates cleanly
2026-08-24 19:25:26 +09:00

319 lines
7.4 KiB
TypeScript

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<T> {
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<T>(path: string): Promise<T | null> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
const res = await fetch(`${API_BASE_URL}${path}`, {
signal: controller.signal,
cache: "no-store",
});
clearTimeout(timeout);
if (!res.ok) {
return null;
}
const json = (await res.json()) as APIResponse<T>;
return json?.data ?? null;
} catch {
return null;
}
}
// --- Home Domain ---
export async function getResearchProjects(): Promise<ResearchProject[] | null> {
const raw = await apiGet<BackendResearchProject[]>("/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<string[] | null> {
const raw = await apiGet<BackendResearchArea[]>("/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<BackendStatsSummary>("/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<Member[] | null> {
const raw = await apiGet<BackendMember[]>("/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<Alumnus[] | null> {
const raw = await apiGet<BackendAlumnus[]>("/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<Publication[] | null> {
const raw = await apiGet<BackendPublication[]>(
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<Publication[] | null> {
const raw = await apiGet<BackendPublication[]>("/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<Patent[] | null> {
const raw = await apiGet<BackendPatent[]>("/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<Semester[] | null> {
const raw = await apiGet<BackendSemester[]>("/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<StandardsBody[] | null> {
const raw = await apiGet<BackendStandardsBody[]>("/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,
})),
})),
}));
}