- Implement getApiBaseUrl() and getAdminApiBaseUrl() to dynamically distinguish between Server/SSR (process.env.API_BASE_URL) and Client/CSR (process.env.NEXT_PUBLIC_API_BASE_URL) - Fix missing data on main landing page in containerized environment - Sanitize and trim ADMIN_TOKEN parsing in Go backend to prevent whitespace/quote mismatch - Real-time token validation in AdminLayout with auto-redirection on token mismatch - All unit and E2E test gates passed clean with unanimous PASS reviews
336 lines
7.9 KiB
TypeScript
336 lines
7.9 KiB
TypeScript
import type {
|
|
Member,
|
|
Alumnus,
|
|
Publication,
|
|
Patent,
|
|
Semester,
|
|
StandardsBody,
|
|
ResearchProject,
|
|
PubCategory,
|
|
DocStatus,
|
|
} from "../content/types";
|
|
|
|
export function getApiBaseUrl(): string {
|
|
if (typeof window !== "undefined") {
|
|
// Browser / CSR context: use public endpoint
|
|
return (
|
|
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
|
process.env.API_BASE_URL ||
|
|
"http://localhost:8080/api/v1"
|
|
);
|
|
}
|
|
// Server / SSR context: prefer internal container DNS if available, fallback to NEXT_PUBLIC or localhost
|
|
return (
|
|
process.env.API_BASE_URL ||
|
|
process.env.NEXT_PUBLIC_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 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<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,
|
|
})),
|
|
})),
|
|
}));
|
|
}
|