# 다중 테이블 엑셀 업로드 범용 시스템

## 개요
하나의 플랫 엑셀 파일로 계층적 다중 테이블(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]로 표시
- 현재 진행 중인 테스트는 [진행중]으로 표시
