Files
landing_page/refer_landing_page/lib/adminApi.ts
T
Godopu 643ce8376c fix(api): ensure /api/v1 prefix normalization across all custom host URLs
- Add normalizeApiUrl() to ensure custom host URLs (e.g. http://puclouds-2:8080) automatically append /api/v1
- Fix 404 error when querying /stats/summary instead of /api/v1/stats/summary
2026-08-25 14:45:41 +09:00

503 lines
13 KiB
TypeScript

/**
* Admin API client for /console dashboard.
* Preserves database IDs and handles authenticated mutating requests (POST, PUT, DELETE).
*/
export function normalizeApiUrl(rawUrl?: string): string {
let url = (rawUrl || "").trim().replace(/\/+$/, "");
if (!url) {
return "/api/v1";
}
if (!url.endsWith("/api/v1")) {
url = `${url}/api/v1`;
}
return url;
}
export function getAdminApiBaseUrl(): 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);
}
export interface AdminResearchProject {
id: number;
slug: string;
title: string;
abstract: string;
keywords: string[];
organization?: string;
standards_org?: string;
period?: string;
funder?: string;
created_at?: string;
updated_at?: string;
}
export interface AdminResearchArea {
id: number;
name_en: string;
name_kr?: string;
display_order: number;
}
export interface AdminMember {
id: number;
name_ko: string;
name_en: string;
degree: string;
affiliation?: string;
email?: string;
is_advisor: boolean;
display_order: number;
created_at?: string;
}
export interface AdminAlumnus {
id: number;
name_ko: string;
name_en: string;
degree: string;
graduated_at: string;
major?: string;
current_position?: string;
created_at?: string;
}
export interface AdminPublication {
id: number;
category: "intl-journal-conf" | "domestic-journal-conf";
title: string;
authors?: string;
venue: string;
volume?: string;
published_at: string;
kci: boolean;
doi?: string;
is_highlight: boolean;
created_at?: string;
}
export interface AdminPatent {
id: number;
title: string;
inventors: string;
application_no: string;
application_at: string;
registration_no?: string;
registration_at?: string;
country: string;
published_at: string;
created_at?: string;
}
export interface AdminCourse {
id: number;
semester_id: number;
code: string;
name_en: string;
name_kr?: string;
note?: string;
display_order: number;
}
export interface AdminSemester {
id: number;
year: number;
term: "Spring" | "Fall";
courses: AdminCourse[];
}
export interface AdminStandardDocument {
id: number;
body_id: number;
wg: string;
project_name: string;
title: string;
doc_ref: string;
status: "IS" | "TS" | "TR" | "CDV" | "WDTR" | "InProgress";
published_at?: string;
}
export interface AdminStandardProjectGroup {
wg: string;
name: string;
documents: AdminStandardDocument[];
}
export interface AdminStandardsBody {
id: number;
org: string;
full_name: string;
scope: "international" | "domestic";
period: string;
role?: string;
display_order: number;
projects: AdminStandardProjectGroup[];
}
export interface AdminStatsSummary {
intl_publications: number;
standardization_docs: number;
patents: number;
}
async function request<T>(
endpoint: string,
options: RequestInit = {},
token?: string
): Promise<T> {
const headers = new Headers(options.headers || {});
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
if (options.body && typeof options.body === "string") {
headers.set("Content-Type", "application/json");
}
const baseUrl = getAdminApiBaseUrl();
const url = `${baseUrl}${endpoint}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 4000);
try {
const response = await fetch(url, {
...options,
headers,
signal: controller.signal,
cache: "no-store",
});
if (response.status === 204) {
return {} as T;
}
const json = await response.json().catch(() => ({}));
if (!response.ok) {
const errorMsg = json?.error?.message || `Request failed with status ${response.status}`;
throw new Error(errorMsg);
}
return json.data;
} finally {
clearTimeout(timeoutId);
}
}
// 0. Auth Verification
export async function adminVerifyToken(token: string): Promise<boolean> {
try {
await request<{ status: string }>("/admin/verify", { method: "GET" }, token);
return true;
} catch {
return false;
}
}
// 1. Stats Summary
export async function adminGetStatsSummary(): Promise<AdminStatsSummary> {
return request<AdminStatsSummary>("/stats/summary");
}
// 2. Research Projects
export async function adminGetResearchProjects(): Promise<AdminResearchProject[]> {
return request<AdminResearchProject[]>("/research-projects");
}
export async function adminCreateResearchProject(
token: string,
data: Omit<AdminResearchProject, "id" | "created_at" | "updated_at">
): Promise<AdminResearchProject> {
return request<AdminResearchProject>("/research-projects", {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdateResearchProject(
token: string,
id: number,
data: Partial<AdminResearchProject>
): Promise<AdminResearchProject> {
return request<AdminResearchProject>(`/research-projects/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeleteResearchProject(token: string, id: number): Promise<void> {
return request<void>(`/research-projects/${id}`, { method: "DELETE" }, token);
}
// 3. Research Areas
export async function adminGetResearchAreas(): Promise<AdminResearchArea[]> {
return request<AdminResearchArea[]>("/research-areas");
}
export async function adminCreateResearchArea(
token: string,
data: Omit<AdminResearchArea, "id">
): Promise<AdminResearchArea> {
return request<AdminResearchArea>("/research-areas", {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdateResearchArea(
token: string,
id: number,
data: Partial<AdminResearchArea>
): Promise<AdminResearchArea> {
return request<AdminResearchArea>(`/research-areas/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeleteResearchArea(token: string, id: number): Promise<void> {
return request<void>(`/research-areas/${id}`, { method: "DELETE" }, token);
}
// 4. Members & Alumni
export async function adminGetMembers(): Promise<AdminMember[]> {
return request<AdminMember[]>("/members");
}
export async function adminCreateMember(
token: string,
data: Omit<AdminMember, "id" | "created_at">
): Promise<AdminMember> {
return request<AdminMember>("/members", {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdateMember(
token: string,
id: number,
data: Partial<AdminMember>
): Promise<AdminMember> {
return request<AdminMember>(`/members/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeleteMember(token: string, id: number): Promise<void> {
return request<void>(`/members/${id}`, { method: "DELETE" }, token);
}
export async function adminGetAlumni(): Promise<AdminAlumnus[]> {
return request<AdminAlumnus[]>("/alumni");
}
export async function adminCreateAlumnus(
token: string,
data: Omit<AdminAlumnus, "id" | "created_at">
): Promise<AdminAlumnus> {
return request<AdminAlumnus>("/alumni", {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdateAlumnus(
token: string,
id: number,
data: Partial<AdminAlumnus>
): Promise<AdminAlumnus> {
return request<AdminAlumnus>(`/alumni/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeleteAlumnus(token: string, id: number): Promise<void> {
return request<void>(`/alumni/${id}`, { method: "DELETE" }, token);
}
// 5. Publications & Patents
export async function adminGetPublications(category?: string): Promise<AdminPublication[]> {
const query = category ? `?category=${category}` : "";
return request<AdminPublication[]>(`/publications${query}`);
}
export async function adminCreatePublication(
token: string,
data: Omit<AdminPublication, "id" | "created_at">
): Promise<AdminPublication> {
return request<AdminPublication>("/publications", {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdatePublication(
token: string,
id: number,
data: Partial<AdminPublication>
): Promise<AdminPublication> {
return request<AdminPublication>(`/publications/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeletePublication(token: string, id: number): Promise<void> {
return request<void>(`/publications/${id}`, { method: "DELETE" }, token);
}
export async function adminGetPatents(): Promise<AdminPatent[]> {
return request<AdminPatent[]>("/patents");
}
export async function adminCreatePatent(
token: string,
data: Omit<AdminPatent, "id" | "created_at">
): Promise<AdminPatent> {
return request<AdminPatent>("/patents", {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdatePatent(
token: string,
id: number,
data: Partial<AdminPatent>
): Promise<AdminPatent> {
return request<AdminPatent>(`/patents/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeletePatent(token: string, id: number): Promise<void> {
return request<void>(`/patents/${id}`, { method: "DELETE" }, token);
}
// 6. Lectures (Semesters & Courses)
export async function adminGetSemesters(): Promise<AdminSemester[]> {
return request<AdminSemester[]>("/semesters");
}
export async function adminCreateSemester(
token: string,
data: Omit<AdminSemester, "id" | "courses">
): Promise<AdminSemester> {
return request<AdminSemester>("/semesters", {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdateSemester(
token: string,
id: number,
data: Partial<AdminSemester>
): Promise<AdminSemester> {
return request<AdminSemester>(`/semesters/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeleteSemester(token: string, id: number): Promise<void> {
return request<void>(`/semesters/${id}`, { method: "DELETE" }, token);
}
export async function adminCreateCourse(
token: string,
semesterId: number,
data: Omit<AdminCourse, "id" | "semester_id">
): Promise<AdminCourse> {
return request<AdminCourse>(`/semesters/${semesterId}/courses`, {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdateCourse(
token: string,
id: number,
data: Partial<AdminCourse>
): Promise<AdminCourse> {
return request<AdminCourse>(`/courses/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeleteCourse(token: string, id: number): Promise<void> {
return request<void>(`/courses/${id}`, { method: "DELETE" }, token);
}
// 7. Standardization (Bodies & Documents)
export async function adminGetStandardsBodies(): Promise<AdminStandardsBody[]> {
return request<AdminStandardsBody[]>("/standards-bodies");
}
export async function adminCreateStandardsBody(
token: string,
data: Omit<AdminStandardsBody, "id" | "projects">
): Promise<AdminStandardsBody> {
return request<AdminStandardsBody>("/standards-bodies", {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdateStandardsBody(
token: string,
id: number,
data: Partial<AdminStandardsBody>
): Promise<AdminStandardsBody> {
return request<AdminStandardsBody>(`/standards-bodies/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeleteStandardsBody(token: string, id: number): Promise<void> {
return request<void>(`/standards-bodies/${id}`, { method: "DELETE" }, token);
}
export async function adminCreateStandardDocument(
token: string,
bodyId: number,
data: Omit<AdminStandardDocument, "id" | "body_id">
): Promise<AdminStandardDocument> {
return request<AdminStandardDocument>(`/standards-bodies/${bodyId}/documents`, {
method: "POST",
body: JSON.stringify(data),
}, token);
}
export async function adminUpdateStandardDocument(
token: string,
id: number,
data: Partial<AdminStandardDocument>
): Promise<AdminStandardDocument> {
return request<AdminStandardDocument>(`/standard-documents/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}, token);
}
export async function adminDeleteStandardDocument(token: string, id: number): Promise<void> {
return request<void>(`/standard-documents/${id}`, { method: "DELETE" }, token);
}