import type { Member, Alumnus, Publication, Patent, Semester, StandardsBody, ResearchProject, PubCategory, DocStatus, } from "../content/types"; export function normalizeApiUrl(rawUrl?: string): string { let url = (rawUrl || "").trim().replace(/\/+$/, ""); if (!url || url === "/" || url === "/api" || url === "/api/v1") { return "/api/v1"; } if (url.endsWith("/api/v1")) { return url; } if (url.endsWith("/api")) { return `${url}/v1`; } return `${url}/api/v1`; } export function getApiBaseUrl(): string { if (typeof window !== "undefined") { // Browser / CSR context: if (process.env.NEXT_PUBLIC_API_BASE_URL) { return normalizeApiUrl(process.env.NEXT_PUBLIC_API_BASE_URL); } return "/api/v1"; } // Server / build-time context: defensive fallback during static prerendering const raw = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080/api/v1"; return normalizeApiUrl(raw); } 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 baseUrl = getApiBaseUrl(); const res = await fetch(`${baseUrl}${path}`, { signal: controller.signal, cache: "no-store", }); 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, })), })), })); }