195 lines
6.6 KiB
Markdown
195 lines
6.6 KiB
Markdown
# 다중 테이블 엑셀 업로드 범용 시스템
|
|
|
|
## 개요
|
|
하나의 플랫 엑셀 파일로 계층적 다중 테이블(2~N개)에 데이터를 일괄 등록하는 범용 시스템.
|
|
거래처 관리(customer_mng → customer_item_mapping → customer_item_prices)를 첫 번째 적용 대상으로 하되,
|
|
공급업체, BOM 등 다른 화면에서도 재사용 가능하도록 설계한다.
|
|
|
|
## 핵심 기능
|
|
1. 모드 선택: 어느 레벨까지 등록할지 사용자가 선택
|
|
2. 템플릿 다운로드: 모드에 맞는 엑셀 양식 자동 생성
|
|
3. 파일 업로드: 플랫 엑셀 → 계층 그룹핑 → 트랜잭션 UPSERT
|
|
4. 컬럼 매핑: 엑셀 헤더 ↔ DB 컬럼 자동/수동 매핑
|
|
|
|
## DB 테이블 관계 (거래처 관리)
|
|
|
|
```
|
|
customer_mng (Level 1 - 루트)
|
|
PK: id (SERIAL)
|
|
UNIQUE: customer_code
|
|
└─ customer_item_mapping (Level 2)
|
|
PK: id (UUID)
|
|
FK: customer_id → customer_mng.id
|
|
UPSERT키: customer_id + customer_item_code
|
|
└─ customer_item_prices (Level 3)
|
|
PK: id (UUID)
|
|
FK: mapping_id → customer_item_mapping.id
|
|
항상 INSERT (기간별 단가 이력)
|
|
```
|
|
|
|
## 범용 설정 구조 (TableChainConfig)
|
|
|
|
```typescript
|
|
interface TableLevel {
|
|
tableName: string;
|
|
label: string;
|
|
// 부모와의 관계
|
|
parentFkColumn?: string; // 이 테이블에서 부모를 참조하는 FK 컬럼
|
|
parentRefColumn?: string; // 부모 테이블에서 참조되는 컬럼 (PK 또는 UNIQUE)
|
|
// UPSERT 설정
|
|
upsertMode: 'upsert' | 'insert'; // upsert: 기존 데이터 있으면 UPDATE, insert: 항상 신규
|
|
upsertKeyColumns?: string[]; // UPSERT 매칭 키 (예: ['customer_code'])
|
|
// 엑셀 매핑 컬럼
|
|
columns: Array<{
|
|
dbColumn: string;
|
|
excelHeader: string;
|
|
required: boolean;
|
|
defaultValue?: any;
|
|
}>;
|
|
}
|
|
|
|
interface TableChainConfig {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
levels: TableLevel[]; // 0 = 루트, 1 = 자식, 2 = 손자...
|
|
uploadModes: Array<{
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
activeLevels: number[]; // 이 모드에서 활성화되는 레벨 인덱스
|
|
}>;
|
|
}
|
|
```
|
|
|
|
## 거래처 관리 설정 예시
|
|
|
|
```typescript
|
|
const customerChainConfig: TableChainConfig = {
|
|
id: 'customer_management',
|
|
name: '거래처 관리',
|
|
description: '거래처, 품목매핑, 단가 일괄 등록',
|
|
levels: [
|
|
{
|
|
tableName: 'customer_mng',
|
|
label: '거래처',
|
|
upsertMode: 'upsert',
|
|
upsertKeyColumns: ['customer_code'],
|
|
columns: [
|
|
{ dbColumn: 'customer_code', excelHeader: '거래처코드', required: true },
|
|
{ dbColumn: 'customer_name', excelHeader: '거래처명', required: true },
|
|
{ dbColumn: 'division', excelHeader: '구분', required: false },
|
|
{ dbColumn: 'contact_person', excelHeader: '담당자', required: false },
|
|
{ dbColumn: 'contact_phone', excelHeader: '연락처', required: false },
|
|
{ dbColumn: 'email', excelHeader: '이메일', required: false },
|
|
{ dbColumn: 'business_number', excelHeader: '사업자번호', required: false },
|
|
{ dbColumn: 'address', excelHeader: '주소', required: false },
|
|
],
|
|
},
|
|
{
|
|
tableName: 'customer_item_mapping',
|
|
label: '품목매핑',
|
|
parentFkColumn: 'customer_id',
|
|
parentRefColumn: 'id',
|
|
upsertMode: 'upsert',
|
|
upsertKeyColumns: ['customer_id', 'customer_item_code'],
|
|
columns: [
|
|
{ dbColumn: 'customer_item_code', excelHeader: '거래처품번', required: true },
|
|
{ dbColumn: 'customer_item_name', excelHeader: '거래처품명', required: true },
|
|
{ dbColumn: 'item_id', excelHeader: '품목ID', required: false },
|
|
],
|
|
},
|
|
{
|
|
tableName: 'customer_item_prices',
|
|
label: '단가',
|
|
parentFkColumn: 'mapping_id',
|
|
parentRefColumn: 'id',
|
|
upsertMode: 'insert',
|
|
columns: [
|
|
{ dbColumn: 'base_price', excelHeader: '기준단가', required: true },
|
|
{ dbColumn: 'discount_type', excelHeader: '할인유형', required: false },
|
|
{ dbColumn: 'discount_value', excelHeader: '할인값', required: false },
|
|
{ dbColumn: 'start_date', excelHeader: '적용시작일', required: false },
|
|
{ dbColumn: 'end_date', excelHeader: '적용종료일', required: false },
|
|
{ dbColumn: 'currency_code', excelHeader: '통화', required: false },
|
|
],
|
|
},
|
|
],
|
|
uploadModes: [
|
|
{ id: 'customer_only', label: '거래처만 등록', description: '거래처 기본정보만', activeLevels: [0] },
|
|
{ id: 'customer_item', label: '거래처 + 품목정보', description: '거래처와 품목매핑', activeLevels: [0, 1] },
|
|
{ id: 'customer_item_price', label: '거래처 + 품목 + 단가', description: '전체 등록', activeLevels: [0, 1, 2] },
|
|
],
|
|
};
|
|
```
|
|
|
|
## 처리 로직 (백엔드)
|
|
|
|
### 1단계: 그룹핑
|
|
엑셀의 플랫 행을 계층별 그룹으로 변환:
|
|
- Level 0 (거래처): customer_code 기준 그룹핑
|
|
- Level 1 (품목매핑): customer_code + customer_item_code 기준 그룹핑
|
|
- Level 2 (단가): 매 행마다 INSERT
|
|
|
|
### 2단계: 계단식 UPSERT (트랜잭션)
|
|
```
|
|
BEGIN TRANSACTION
|
|
|
|
FOR EACH unique customer_code:
|
|
1. customer_mng UPSERT → 결과에서 id 획득 (returnedId)
|
|
|
|
FOR EACH unique customer_item_code (해당 거래처):
|
|
2. customer_item_mapping의 customer_id = returnedId 주입
|
|
UPSERT → 결과에서 id 획득 (mappingId)
|
|
|
|
FOR EACH price row (해당 품목매핑):
|
|
3. customer_item_prices의 mapping_id = mappingId 주입
|
|
INSERT
|
|
|
|
COMMIT (전체 성공) or ROLLBACK (하나라도 실패)
|
|
```
|
|
|
|
### 3단계: 결과 반환
|
|
```json
|
|
{
|
|
"success": true,
|
|
"results": {
|
|
"customer_mng": { "inserted": 2, "updated": 1 },
|
|
"customer_item_mapping": { "inserted": 5, "updated": 2 },
|
|
"customer_item_prices": { "inserted": 12 }
|
|
},
|
|
"errors": []
|
|
}
|
|
```
|
|
|
|
## 테스트 계획
|
|
|
|
### 1단계: 백엔드 서비스
|
|
- [x] plan.md 작성
|
|
- [ ] multiTableExcelService.ts 기본 구조 작성
|
|
- [ ] 그룹핑 로직 구현
|
|
- [ ] 계단식 UPSERT 로직 구현
|
|
- [ ] 트랜잭션 처리
|
|
- [ ] 에러 핸들링
|
|
|
|
### 2단계: API 엔드포인트
|
|
- [ ] POST /api/data/multi-table/upload 추가
|
|
- [ ] POST /api/data/multi-table/template 추가 (템플릿 다운로드)
|
|
- [ ] 입력값 검증
|
|
|
|
### 3단계: 프론트엔드
|
|
- [ ] MultiTableExcelUploadModal.tsx 컴포넌트 작성
|
|
- [ ] 모드 선택 UI
|
|
- [ ] 템플릿 다운로드 버튼
|
|
- [ ] 파일 업로드 + 미리보기
|
|
- [ ] 컬럼 매핑 UI
|
|
- [ ] 업로드 결과 표시
|
|
|
|
### 4단계: 통합
|
|
- [ ] 거래처 관리 화면에 연결
|
|
- [ ] 실제 데이터로 테스트
|
|
|
|
## 진행 상태
|
|
- 완료된 테스트는 [x]로 표시
|
|
- 현재 진행 중인 테스트는 [진행중]으로 표시
|