6a1b79dc81
Build and Push Images / build-and-push (push) Has been cancelled
- 신규 테이블 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>
136 lines
4.3 KiB
TypeScript
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 || [];
|
|
},
|
|
};
|