feat: 엑셀 업로드 템플릿 시스템 (V19)
Agent Teams 3명 병렬 작업 (dba / api / frontend) 결과물.
DB (V19__업로드템플릿.sql):
- upload_template / upload_template_column / upload_history 3 테이블
- 인덱스 5종 (category / company_code / template_id / started_at)
- SYSTEM_UPLOAD_TEMPLATE 메뉴 + READ/CREATE/UPDATE/DELETE 권한
- 모든 역할에 권한 부여 (MANAGER 는 READ 만)
Backend:
- ga-core: VO 7개 (Template/Column/History의 VO/Resp/SaveReq/SearchParam) +
Mapper 3개 + XML 3개 (UploadTemplate/Column/History)
- ga-api: TransformEngine (12 변환 규칙: TRIM/UPPER/LOWER/DATE/DIVIDE/MULTIPLY/
LOOKUP/CODE_MAP/FIXED/REPLACE/SUBSTRING/CONCAT/DEFAULT) +
LookupCache (O(1) HashMap 캐시) + UploadService (processUpload/preview) +
LookupMapper / DynamicInsertMapper (whitelist 기반 동적 SQL)
- ga-api Controller 2개:
· UploadTemplateController: /api/upload-templates CRUD + columns + history
· UploadController: POST /api/upload/{templateCode}, /preview
Frontend:
- api/uploadTemplate.ts (10 endpoints)
- pages/system/UploadTemplateManager.tsx — 좌(템플릿 목록) 우(컬럼 매핑 편집기)
+ ModalForm 등록/수정 + 미리보기 Modal + 이력 Drawer
- components/common/TemplateUpload.tsx — 범용 컴포넌트 (Select 템플릿 + Dragger
+ 진행률 + 결과 Modal + 에러 파일 다운로드)
- App.tsx 라우트 추가, MenuIcon.tsx 매핑 추가
검증:
- ./gradlew :ga-api:bootJar 통과
- Flyway V19 운영 DB 적용 성공 (200ms)
- ga-api 부팅 7.8s, /api/upload-templates 200 응답
- 메뉴 트리에 SYSTEM_UPLOAD_TEMPLATE 노출 확인
- 프론트 tsc -b / vite build 통과
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import api, { PageResponse, unwrap } from './request';
|
||||
|
||||
export interface UploadTemplateResp {
|
||||
templateId: number;
|
||||
templateCode: string;
|
||||
templateName: string;
|
||||
category: string;
|
||||
targetTable: string;
|
||||
companyCode?: string;
|
||||
headerRow: number;
|
||||
dataStartRow: number;
|
||||
sheetIndex: number;
|
||||
sheetName?: string;
|
||||
batchSize: number;
|
||||
isActive: string;
|
||||
description?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface UploadTemplateDetailResp extends UploadTemplateResp {
|
||||
columns: UploadTemplateColumnVO[];
|
||||
}
|
||||
|
||||
export interface UploadTemplateColumnVO {
|
||||
columnId?: number;
|
||||
templateId?: number;
|
||||
sortOrder: number;
|
||||
sourceType: string;
|
||||
sourceColumn?: string;
|
||||
sourceHeader?: string;
|
||||
targetField: string;
|
||||
targetType: string;
|
||||
transformRule?: string;
|
||||
transformParam?: string;
|
||||
dateFormat?: string;
|
||||
numberDivide?: number;
|
||||
numberMultiply?: number;
|
||||
fixedValue?: string;
|
||||
defaultValue?: string;
|
||||
lookupTable?: string;
|
||||
lookupSourceField?: string;
|
||||
lookupTargetField?: string;
|
||||
codeMapGroup?: string;
|
||||
isRequired?: string;
|
||||
validationRegex?: string;
|
||||
validationMessage?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UploadTemplateQueryParams {
|
||||
category?: string;
|
||||
companyCode?: string;
|
||||
isActive?: string;
|
||||
searchKeyword?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface UploadTemplateSaveReq {
|
||||
templateCode: string;
|
||||
templateName: string;
|
||||
category: string;
|
||||
targetTable: string;
|
||||
companyCode?: string;
|
||||
headerRow?: number;
|
||||
dataStartRow?: number;
|
||||
sheetIndex?: number;
|
||||
sheetName?: string;
|
||||
batchSize?: number;
|
||||
isActive?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UploadHistoryResp {
|
||||
historyId: number;
|
||||
templateId: number;
|
||||
fileName: string;
|
||||
totalCount: number;
|
||||
successCount: number;
|
||||
errorCount: number;
|
||||
skipCount: number;
|
||||
uploadedBy?: string;
|
||||
uploadedAt?: string;
|
||||
durationSec?: number;
|
||||
}
|
||||
|
||||
export interface UploadResultResp {
|
||||
totalCount: number;
|
||||
successCount: number;
|
||||
errorCount: number;
|
||||
skipCount: number;
|
||||
errorFileId?: number;
|
||||
durationSec?: number;
|
||||
errors?: {
|
||||
rowNumber: number;
|
||||
fieldName?: string;
|
||||
value?: string;
|
||||
errorMessage: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PreviewRowResp {
|
||||
rowNumber: number;
|
||||
[field: string]: unknown;
|
||||
}
|
||||
|
||||
export const uploadTemplateApi = {
|
||||
list: (params: UploadTemplateQueryParams) =>
|
||||
unwrap<PageResponse<UploadTemplateResp>>(api.get('/api/upload-templates', { params })),
|
||||
|
||||
detail: (id: number) =>
|
||||
unwrap<UploadTemplateDetailResp>(api.get(`/api/upload-templates/${id}`)),
|
||||
|
||||
create: (req: UploadTemplateSaveReq) =>
|
||||
unwrap<number>(api.post('/api/upload-templates', req)),
|
||||
|
||||
update: (id: number, req: UploadTemplateSaveReq) =>
|
||||
unwrap<void>(api.put(`/api/upload-templates/${id}`, req)),
|
||||
|
||||
remove: (id: number) =>
|
||||
unwrap<void>(api.delete(`/api/upload-templates/${id}`)),
|
||||
|
||||
getColumns: (id: number) =>
|
||||
unwrap<UploadTemplateColumnVO[]>(api.get(`/api/upload-templates/${id}/columns`)),
|
||||
|
||||
saveColumns: (id: number, columns: UploadTemplateColumnVO[]) =>
|
||||
unwrap<void>(api.put(`/api/upload-templates/${id}/columns`, columns)),
|
||||
|
||||
history: (id: number) =>
|
||||
unwrap<UploadHistoryResp[]>(api.get(`/api/upload-templates/${id}/history`)),
|
||||
|
||||
preview: (templateCode: string, file: File) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
return unwrap<PreviewRowResp[]>(
|
||||
api.post(`/api/upload-templates/${templateCode}/preview`, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
upload: (templateCode: string, file: File) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
return unwrap<UploadResultResp>(
|
||||
api.post(`/api/upload/${templateCode}`, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user