Files
pipeline/frontend/lib/api/fileReader.ts
T
chpark 6a1b79dc81
Build and Push Images / build-and-push (push) Has been cancelled
feat(file-reader): CSV/TXT/Excel 파일 데이터 소스 — UI 업로드 + 폴더 감시 + 매핑 + 적재
- 신규 테이블 file_reader_configs / file_reader_mappings / file_reader_history (마이그레이션 313)
- 파서: csv-parse + xlsx 라이브러리 추가, CSV/TSV/TXT/XLSX 통합 파서 (parsers.ts)
- 서비스: 파일→매핑→타겟 DB INSERT/UPSERT/REPLACE, 호스트 경로 허용 루트 검증
- 스케줄러: source_mode='watch' 설정마다 node-cron 등록, 1분 주기 reload
- 라우트: /api/file-reader/configs CRUD + preview + run-upload + run-watch + history
- 프론트: 데이터 소스 페이지 "파일 리더" 탭 placeholder → FileReaderConnectionList 컴포넌트
- FileReaderConnectionModal: 기본/파싱/타겟/매핑 통합 폼 + 샘플 업로드 미리보기
- 환경변수 FILE_READER_ALLOWED_ROOTS (콤마 구분, 기본 /home/wace/file-imports,/mnt)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:38:10 +09:00

136 lines
4.3 KiB
TypeScript

// File Reader API 클라이언트
import { apiClient } from "./client";
export type FileType = "csv" | "tsv" | "txt" | "xlsx" | "xls";
export type SourceMode = "upload" | "watch";
export type SaveMode = "INSERT" | "UPSERT" | "REPLACE";
export type ProcessedAction = "archive" | "delete" | "mark";
export interface FileReaderMapping {
id?: number;
config_id?: number;
from_column: string;
from_column_type: "header" | "index";
to_column: string;
to_data_type: string;
default_value?: string | null;
transform_expr?: string | null;
is_required: boolean;
mapping_order: number;
}
export interface FileReaderConfig {
id?: number;
name: string;
company_code: string;
file_type: FileType;
source_mode: SourceMode;
host_path?: string | null;
file_pattern?: string;
processed_action?: ProcessedAction;
archive_subdir?: string;
has_header: boolean;
delimiter?: string;
encoding?: string;
sheet_name?: string | null;
skip_rows?: number;
target_db_id?: number | null;
target_schema?: string;
target_table?: string;
save_mode: SaveMode;
conflict_keys?: string | null;
cron_schedule?: string | null;
is_active: string;
description?: string | null;
last_run_at?: string | null;
last_run_result?: string | null;
last_run_message?: string | null;
last_processed_count?: number;
mappings?: FileReaderMapping[];
}
export interface FileReaderHistory {
id: number;
config_id: number;
file_path: string;
file_size: number;
file_mtime: string;
started_at: string;
finished_at: string | null;
status: "success" | "failure" | "partial" | "skipped";
rows_total: number;
rows_inserted: number;
rows_failed: number;
error_message: string | null;
}
export interface PreviewResult {
headers: string[];
totalRows: number;
sampleRows: Record<string, unknown>[];
}
const BASE = "/api/file-reader";
export const FileReaderAPI = {
async list(companyCode?: string): Promise<FileReaderConfig[]> {
const url = companyCode
? `${BASE}/configs?company_code=${encodeURIComponent(companyCode)}`
: `${BASE}/configs`;
const res = await apiClient.get<{ success: boolean; items: FileReaderConfig[] }>(url);
return res.data.items || [];
},
async get(id: number): Promise<{ config: FileReaderConfig; mappings: FileReaderMapping[] }> {
const res = await apiClient.get<{ success: boolean; config: FileReaderConfig; mappings: FileReaderMapping[] }>(
`${BASE}/configs/${id}`
);
return { config: res.data.config, mappings: res.data.mappings || [] };
},
async create(payload: Partial<FileReaderConfig>): Promise<number> {
const res = await apiClient.post<{ success: boolean; id: number }>(`${BASE}/configs`, payload);
return res.data.id;
},
async update(id: number, payload: Partial<FileReaderConfig>): Promise<void> {
await apiClient.put(`${BASE}/configs/${id}`, payload);
},
async remove(id: number): Promise<void> {
await apiClient.delete(`${BASE}/configs/${id}`);
},
async preview(id: number, file: File): Promise<PreviewResult> {
const fd = new FormData();
fd.append("file", file);
const res = await apiClient.post<{ success: boolean } & PreviewResult>(
`${BASE}/configs/${id}/preview`,
fd
);
return {
headers: res.data.headers,
totalRows: res.data.totalRows,
sampleRows: res.data.sampleRows,
};
},
async runUpload(id: number, file: File): Promise<{
status: string; rowsTotal: number; rowsInserted: number; rowsFailed: number; errorMessage: string | null;
}> {
const fd = new FormData();
fd.append("file", file);
const res = await apiClient.post(`${BASE}/configs/${id}/run-upload`, fd);
return res.data;
},
async runWatch(id: number): Promise<{ scanned: number; processed: number; skipped: number; failed: number }> {
const res = await apiClient.post(`${BASE}/configs/${id}/run-watch`, {});
return res.data;
},
async history(id: number, limit = 50): Promise<FileReaderHistory[]> {
const res = await apiClient.get<{ success: boolean; items: FileReaderHistory[] }>(
`${BASE}/configs/${id}/history?limit=${limit}`
);
return res.data.items || [];
},
async allowedRoots(): Promise<string[]> {
const res = await apiClient.get<{ success: boolean; roots: string[] }>(`${BASE}/allowed-roots`);
return res.data.roots || [];
},
};