- Remove redundant adminGetStatsSummary query and unused AdminStatsSummary interface - Initialize empty repository slices with make([]T, 0) to ensure empty JSON arrays instead of null - Add optional chaining in dashboard aggregations for strict null-safety - Passed 100% unanimous peer reviews from planner-reviewer-claude-01 and reviewer-cline-01
495 lines
12 KiB
TypeScript
495 lines
12 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 || 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 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[];
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|