./knowledge-base/frontend/src/api/client.ts
import axios from "axios";
import { oktaAuth } from "../auth/oktaConfig";
import type {
SearchRequest,
SearchResponse,
UploadRequest,
UploadResponse,
ProcessingStatus,
BulkUploadRequest,
BulkUploadResponse,
PdfUploadUrlRequest,
PdfUploadUrlResponse,
EraMaster,
ProgramMaster,
DocumentSummary,
BookCountsResponse,
} from "../types";
// API Gateway URLを環境変数から取得
const API_BASE_URL = import.meta.env.VITE_API_GATEWAY_URL || "/api";
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
"Content-Type": "application/json",
},
timeout: 30000,
});
// リクエストインターセプター(Bearerトークン付与)
apiClient.interceptors.request.use((config) => {
const token = oktaAuth.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// レスポンスインターセプター(エラーハンドリング)
apiClient.interceptors.response.use(
(response) => response,
(error) => {
console.error("API Error:", error);
return Promise.reject(error);
},
);
// ========================================
// 検索API
// ========================================
export const searchPages = async (
params: SearchRequest,
): Promise<SearchResponse> => {
const response = await apiClient.post<SearchResponse>("/search", params);
return response.data;
};
// ========================================
// アップロードAPI
// ========================================
export const uploadPage = async (
params: UploadRequest,
): Promise<UploadResponse> => {
const response = await apiClient.post<UploadResponse>(
"/process/upload",
params,
);
return response.data;
};
export const bulkUpload = async (
params: BulkUploadRequest,
): Promise<BulkUploadResponse> => {
const response = await apiClient.post<BulkUploadResponse>(
"/process/bulk",
params,
);
return response.data;
};
/**
* PDFアップロード用 Presigned URLを取得し、その URLに対して S3 に直接 PUTアップロードする。
* アップロード後はバックエンドの pdf-splitter Lambda が自動煎起する。
*/
export const uploadPdf = async (
params: PdfUploadUrlRequest,
pdfFile: File,
onProgress?: (percent: number) => void,
): Promise<PdfUploadUrlResponse> => {
// Step 1: Presigned PUT URLを取得
const urlResponse = await apiClient.post<PdfUploadUrlResponse>(
"/process/pdf-upload-url",
params,
);
const { upload_url } = urlResponse.data;
// Step 2: CloudFront経由でS3にPUT(企業プロキシ対策)
// ブラウザから s3.amazonaws.com への直接 PUT は i-FILTER 等の企業プロキシでブロックされる。
// presigned URL のパス・クエリパラメータを同一オリジン(/pdf-upload/...)に付け替えて
// CloudFront → S3 経由でルーティングし、CORS と i-FILTER の問題を同時に回避する。
const presignedUrlObj = new URL(upload_url);
const proxiedUrl = `/pdf-upload${presignedUrlObj.pathname}${presignedUrlObj.search}`;
await axios.put(proxiedUrl, pdfFile, {
headers: { "Content-Type": "application/pdf" },
// baseURL は指定しないため相対URL(同一オリジン)として送信される
onUploadProgress: (event) => {
if (onProgress && event.total) {
onProgress(Math.round((event.loaded / event.total) * 100));
}
},
});
return urlResponse.data;
};
// ========================================
// ステータス取得API
// ========================================
export const getProcessingStatus = async (
pageId: string,
): Promise<ProcessingStatus> => {
const response = await apiClient.get<ProcessingStatus>(
`/process/status/${pageId}`,
);
return response.data;
};
// ========================================
// 時代マスタ取得
// ========================================
const eraListCache: EraMaster[] = [];
export const loadEraMasterList = async (): Promise<EraMaster[]> => {
// キャッシュがあればそれを返す
if (eraListCache.length > 0) {
return eraListCache;
}
// CloudFront経由でS3から直接読み込み(相対パス)
const response = await fetch("/master/era-list.json");
const data = await response.json();
eraListCache.push(...data);
return data;
};
// ========================================
// 番組マスタ取得(S3 + CloudFront)
// ========================================
let programListCache: ProgramMaster[] | null = null;
export const loadProgramMasterList = async (): Promise<ProgramMaster[]> => {
if (programListCache !== null) {
return programListCache;
}
// CloudFront経由でS3から直接読み込み
const response = await fetch("/master/programs-list.json");
const data = await response.json();
programListCache = data;
return data;
};
export const invalidateProgramCache = () => {
programListCache = null;
};
// ========================================
// メンテナンス API
// ========================================
export const fetchPrograms = async (): Promise<ProgramMaster[]> => {
const response = await apiClient.get<{ programs: ProgramMaster[] }>(
"/maintenance/programs",
);
return response.data.programs;
};
export const createProgram = async (name: string): Promise<ProgramMaster> => {
const response = await apiClient.post<{ program: ProgramMaster }>(
"/maintenance/programs",
{ name },
);
return response.data.program;
};
export const deleteProgram = async (programId: string): Promise<void> => {
await apiClient.delete(`/maintenance/programs/${programId}`);
};
export const fetchAllDocuments = async (): Promise<DocumentSummary[]> => {
const response = await apiClient.get<{ documents: DocumentSummary[] }>(
"/maintenance/documents",
);
return response.data.documents;
};
export const fetchBookCounts = async (): Promise<BookCountsResponse> => {
const response = await apiClient.get<BookCountsResponse>(
"/maintenance/book-counts",
);
return response.data;
};
export const updateDocumentPrograms = async (
documentId: string,
programs: string[],
): Promise<void> => {
await apiClient.put(`/maintenance/documents/${documentId}/programs`, {
programs,
});
};
// ページ単体のメタ情報更新(要件①)
export const updatePageMetadata = async (
pageId: string,
data: import("../types").UpdatePageMetadataRequest,
): Promise<void> => {
await apiClient.put(`/maintenance/pages/${pageId}/metadata`, data);
};
// 書籍全体のメタ情報一括更新(要件②)
export const updateDocumentMetadata = async (
documentId: string,
data: import("../types").UpdateDocumentMetadataRequest,
): Promise<void> => {
await apiClient.put(`/maintenance/documents/${documentId}/metadata`, data);
};
// 書籍全体の AI再解析(要件②)
export const reanalyzeDocument = async (
documentId: string,
options?: import("../types").ReanalyzeDocumentRequest,
): Promise<{ queued_pages: number; message: string }> => {
const response = await apiClient.post<{
queued_pages: number;
message: string;
}>(`/maintenance/documents/${documentId}/reanalyze`, options ?? {});
return response.data;
};
// 書籍削除(booksTable + processingStatus + OpenSearch + S3 をすべて削除)
export const deleteDocument = async (
documentId: string,
): Promise<{ document_id: string; deleted: Record<string, number> }> => {
const response = await apiClient.delete<{
document_id: string;
deleted: Record<string, number>;
}>(`/process/books/${documentId}`);
return response.data;
};
export default apiClient;