264 lines
10 KiB
Markdown
264 lines
10 KiB
Markdown
# v2-table-list Entity 조인 기능 분석
|
|
|
|
v2-repeater에 동일 기능을 추가하기 위한 상세 분석 문서입니다.
|
|
|
|
---
|
|
|
|
## 1. 개요
|
|
|
|
v2-table-list의 Entity 조인 기능은 두 가지 유형으로 구분됩니다:
|
|
|
|
| 유형 | 설명 | 설정 방식 |
|
|
|------|------|-----------|
|
|
| **isEntityJoin** | 테이블 컬럼이 `input_type=entity`인 경우 (테이블 타입 관리에서 참조 테이블 설정됨) | 자동 감지 + entityDisplayConfig로 표시 컬럼 선택 |
|
|
| **additionalJoinInfo** | ConfigPanel "Entity 조인 컬럼" 탭에서 수동 추가한 참조 테이블 컬럼 | addEntityColumn으로 추가, additionalJoinInfo 저장 |
|
|
|
|
---
|
|
|
|
## 2. Entity 조인 설정 UI 구조 (TableListConfigPanel)
|
|
|
|
### 2.1 데이터 소스
|
|
|
|
- **entityJoinApi.getEntityJoinColumns(tableName)** 호출
|
|
- targetTableName 변경 시 useEffect로 재호출
|
|
|
|
### 2.2 entityJoinColumns 상태 구조
|
|
|
|
```typescript
|
|
{
|
|
availableColumns: Array<{
|
|
tableName: string; // 참조 테이블명 (예: dept_info)
|
|
columnName: string; // 참조 테이블 컬럼명 (예: company_name)
|
|
columnLabel: string;
|
|
dataType: string;
|
|
joinAlias: string; // 예: dept_code_company_name (sourceColumn_columnName)
|
|
suggestedLabel: string;
|
|
}>;
|
|
joinTables: Array<{
|
|
tableName: string; // 참조 테이블명
|
|
currentDisplayColumn: string;
|
|
joinConfig: { // 백엔드 entity-join-columns API에서 반환
|
|
sourceColumn: string; // 기준 테이블 FK 컬럼 (예: dept_code)
|
|
referenceTable: string;
|
|
referenceColumn: string;
|
|
displayColumn: string;
|
|
// ...
|
|
};
|
|
availableColumns: Array<{
|
|
columnName: string;
|
|
columnLabel: string;
|
|
dataType: string;
|
|
inputType?: string;
|
|
}>;
|
|
}>;
|
|
}
|
|
```
|
|
|
|
### 2.3 Entity 조인 컬럼 UI (ConfigPanel)
|
|
|
|
- **위치**: 기본 컬럼 선택 영역 아래, "Entity 조인 컬럼" 섹션
|
|
- **조건**: `entityJoinColumns.joinTables.length > 0` 일 때만 표시
|
|
- **구조**: joinTables별로 그룹화 → 각 그룹 내 availableColumns를 체크박스로 표시
|
|
- **추가 로직**: `addEntityColumn(joinColumn)` 호출
|
|
|
|
### 2.4 addEntityColumn 함수 (핵심)
|
|
|
|
```typescript
|
|
const addEntityColumn = (joinColumn: availableColumns[0]) => {
|
|
// joinTables에서 sourceColumn 추출 (필수!)
|
|
const joinTableInfo = entityJoinColumns.joinTables?.find(
|
|
(jt) => jt.tableName === joinColumn.tableName
|
|
);
|
|
const sourceColumn = joinTableInfo?.joinConfig?.sourceColumn || "";
|
|
|
|
const newColumn: ColumnConfig = {
|
|
columnName: joinColumn.joinAlias, // 예: dept_code_company_name
|
|
displayName: joinColumn.columnLabel,
|
|
// ...
|
|
isEntityJoin: false, // 조인 탭에서 추가한 컬럼은 엔티티 타입이 아님
|
|
additionalJoinInfo: {
|
|
sourceTable: config.selectedTable || screenTableName || "",
|
|
sourceColumn: sourceColumn, // dept_code
|
|
referenceTable: joinColumn.tableName, // dept_info
|
|
joinAlias: joinColumn.joinAlias, // dept_code_company_name
|
|
},
|
|
};
|
|
handleChange("columns", [...config.columns, newColumn]);
|
|
};
|
|
```
|
|
|
|
**주의**: `sourceColumn`은 반드시 `joinTableInfo.joinConfig.sourceColumn`에서 가져와야 합니다. `joinColumn`에는 없습니다.
|
|
|
|
---
|
|
|
|
## 3. additionalJoinInfo 데이터 구조
|
|
|
|
### 3.1 타입 정의 (types.ts)
|
|
|
|
```typescript
|
|
additionalJoinInfo?: {
|
|
sourceTable: string; // 기준 테이블 (예: user_info)
|
|
sourceColumn: string; // 기준 테이블 FK 컬럼 (예: dept_code)
|
|
referenceTable?: string; // 참조 테이블 (예: dept_info)
|
|
joinAlias: string; // 조인 결과 컬럼 별칭 (예: dept_code_company_name)
|
|
};
|
|
```
|
|
|
|
### 3.2 네이밍 규칙
|
|
|
|
- **joinAlias**: `${sourceColumn}_${referenceTable컬럼명}`
|
|
- 예: `dept_code` + `company_name` → `dept_code_company_name`
|
|
- 백엔드가 이 규칙으로 SELECT 시 alias를 생성하고, 응답 row에 `dept_code_company_name` 키로 값이 들어옴
|
|
|
|
---
|
|
|
|
## 4. 백엔드 API 호출 흐름
|
|
|
|
### 4.1 TableListComponent 데이터 로딩
|
|
|
|
```typescript
|
|
// 1. additionalJoinInfo가 있는 컬럼만 추출
|
|
const entityJoinColumns = (tableConfig.columns || [])
|
|
.filter((col) => col.additionalJoinInfo)
|
|
.map((col) => ({
|
|
sourceTable: col.additionalJoinInfo!.sourceTable,
|
|
sourceColumn: col.additionalJoinInfo!.sourceColumn,
|
|
joinAlias: col.additionalJoinInfo!.joinAlias,
|
|
referenceTable: col.additionalJoinInfo!.referenceTable,
|
|
}));
|
|
|
|
// 2. entityDisplayConfig가 있는 컬럼 (isEntityJoin) - 화면별 표시 설정
|
|
const screenEntityConfigs: Record<string, any> = {};
|
|
(tableConfig.columns || [])
|
|
.filter((col) => col.entityDisplayConfig?.displayColumns?.length > 0)
|
|
.forEach((col) => {
|
|
screenEntityConfigs[col.columnName] = {
|
|
displayColumns: col.entityDisplayConfig!.displayColumns,
|
|
separator: col.entityDisplayConfig!.separator || " - ",
|
|
sourceTable: col.entityDisplayConfig!.sourceTable || tableConfig.selectedTable,
|
|
joinTable: col.entityDisplayConfig!.joinTable,
|
|
};
|
|
});
|
|
|
|
// 3. API 호출
|
|
response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
|
page, size, sortBy, sortOrder,
|
|
search: hasFilters ? filters : undefined,
|
|
enableEntityJoin: true,
|
|
additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined,
|
|
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined,
|
|
dataFilter: tableConfig.dataFilter,
|
|
excludeFilter: excludeFilterParam,
|
|
});
|
|
```
|
|
|
|
### 4.2 entityJoinApi.getTableDataWithJoins 파라미터
|
|
|
|
```typescript
|
|
additionalJoinColumns?: Array<{
|
|
sourceTable: string;
|
|
sourceColumn: string;
|
|
joinAlias: string;
|
|
referenceTable?: string; // 백엔드에서 referenceTable로 기존 조인 찾을 때 사용
|
|
}>;
|
|
```
|
|
|
|
- **전달 방식**: `JSON.stringify(additionalJoinColumns)` 후 쿼리 파라미터로 전달
|
|
- **백엔드**: `entityJoinController` → `tableManagementService.getTableDataWithEntityJoins`
|
|
|
|
### 4.3 백엔드 처리 (tableManagementService)
|
|
|
|
1. `detectEntityJoins`로 기본 Entity 조인 설정 조회
|
|
2. `additionalJoinColumns`가 있으면:
|
|
- `sourceColumn` 또는 `referenceTable`로 기존 joinConfig 찾기
|
|
- `joinAlias`에서 실제 컬럼명 추출 (예: `dept_code_company_name` → `company_name`)
|
|
- 기존 config에 `displayColumns` 병합 또는 새 config 추가
|
|
- `aliasColumn`: `${sourceColumn}_${actualColumnName}` (예: `dept_code_company_name`)
|
|
3. `additionalJoinColumns`가 있으면 **full_join** 전략 강제 사용 (캐시 미사용)
|
|
|
|
---
|
|
|
|
## 5. 데이터 표시 시 조인 데이터 매핑
|
|
|
|
### 5.1 additionalJoinInfo 컬럼 (조인 탭에서 추가한 컬럼)
|
|
|
|
- **백엔드 응답**: row에 `joinAlias` 키로 값이 직접 들어옴
|
|
- 예: `row.dept_code_company_name = "개발팀"`
|
|
- **프론트엔드**: `column.columnName`이 `joinAlias`와 동일하므로 `rowData[column.columnName]`으로 바로 접근
|
|
- **formatCellValue**: `entityDisplayConfig`가 없으면 일반 컬럼처럼 `value` 사용 (이미 row에 joinAlias로 들어있음)
|
|
|
|
### 5.2 entityDisplayConfig 컬럼 (isEntityJoin, 테이블 타입 관리에서 entity 설정된 컬럼)
|
|
|
|
- **formatCellValue** 로직:
|
|
```typescript
|
|
if (column.entityDisplayConfig && rowData) {
|
|
const displayColumns = column.entityDisplayConfig.displayColumns;
|
|
const separator = column.entityDisplayConfig.separator;
|
|
const values = displayColumns.map((colName) => {
|
|
const joinedKey = `${column.columnName}_${colName}`; // 예: manager_user_name
|
|
let cellValue = rowData[joinedKey];
|
|
if (cellValue == null) cellValue = rowData[colName];
|
|
return cellValue ?? "";
|
|
});
|
|
return values.filter(v => v !== "").join(separator || " - ");
|
|
}
|
|
```
|
|
- **백엔드 alias 규칙**: `${sourceColumn}_${displayColumn}` (예: `manager_user_name`)
|
|
|
|
### 5.3 joinedColumnMeta (inputType/category 매핑)
|
|
|
|
- additionalJoinInfo 컬럼도 `joinedColumnMeta`에 등록됨
|
|
- `actualColumn` 추출: `joinAlias.replace(\`${sourceColumn}_\`, "")` → 참조 테이블의 실제 컬럼명
|
|
- 조인 테이블별로 `tableTypeApi.getColumnInputTypes` 호출하여 inputType 로드
|
|
|
|
---
|
|
|
|
## 6. entity-join-columns API (ConfigPanel용)
|
|
|
|
- **엔드포인트**: `GET /api/table-management/tables/:tableName/entity-join-columns`
|
|
- **역할**: 화면 편집기에서 "Entity 조인 컬럼" 탭에 표시할 데이터 제공
|
|
- **응답**:
|
|
- `joinTables`: 각 Entity 조인별 `joinConfig`, `tableName`, `availableColumns`
|
|
- `availableColumns`: 모든 조인 컬럼을 flat하게 (joinAlias 포함)
|
|
- **joinConfig**: `entityJoinService.detectEntityJoins` 결과에서 옴 (테이블 타입 관리의 reference_table 설정 기반)
|
|
|
|
---
|
|
|
|
## 7. v2-repeater 적용 시 체크리스트
|
|
|
|
### ConfigPanel
|
|
|
|
- [ ] `entityJoinApi.getEntityJoinColumns(targetTableName)` 호출
|
|
- [ ] `entityJoinColumns` 상태 (availableColumns, joinTables)
|
|
- [ ] "Entity 조인 컬럼" UI 섹션 (joinTables.length > 0일 때)
|
|
- [ ] `addEntityColumn` 함수: `joinConfig.sourceColumn` 사용
|
|
- [ ] RepeaterColumnConfig에 `additionalJoinInfo` 타입 추가
|
|
|
|
### 데이터 로딩 (RepeaterComponent)
|
|
|
|
- [ ] `additionalJoinInfo`가 있는 컬럼 추출 → `entityJoinColumns` 배열 생성
|
|
- [ ] `entityJoinApi.getTableDataWithJoins` 호출 시 `additionalJoinColumns` 전달
|
|
- [ ] `entityDisplayConfig`가 있으면 `screenEntityConfigs`에도 포함 (isEntityJoin 컬럼용)
|
|
|
|
### 셀 렌더링
|
|
|
|
- [ ] additionalJoinInfo 컬럼: `rowData[column.columnName]` (joinAlias와 동일)
|
|
- [ ] entityDisplayConfig 컬럼: displayColumns + separator로 조합, `joinedKey = ${columnName}_${colName}`
|
|
|
|
### 타입 정의
|
|
|
|
- [ ] `RepeaterColumnConfig`에 `additionalJoinInfo?: { sourceTable, sourceColumn, referenceTable, joinAlias }` 추가
|
|
|
|
---
|
|
|
|
## 8. 참고 파일
|
|
|
|
| 파일 | 용도 |
|
|
|------|------|
|
|
| `frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx` | Entity 조인 UI, addEntityColumn |
|
|
| `frontend/lib/registry/components/v2-table-list/TableListComponent.tsx` | 데이터 로딩, formatCellValue |
|
|
| `frontend/lib/registry/components/v2-table-list/types.ts` | additionalJoinInfo 타입 |
|
|
| `frontend/lib/api/entityJoin.ts` | getTableDataWithJoins, getEntityJoinColumns |
|
|
| `backend-node/src/controllers/entityJoinController.ts` | entity-join-columns, data-with-joins |
|
|
| `backend-node/src/services/tableManagementService.ts` | additionalJoinColumns 병합 로직 |
|