리피터 케이블 설정 구현
This commit is contained in:
parent
bed7f5f5c4
commit
e168753d87
|
|
@ -13,15 +13,238 @@ alwaysApply: false
|
||||||
|
|
||||||
## 목차
|
## 목차
|
||||||
|
|
||||||
1. [엔티티 조인 컬럼 활용 (필수)](#1-엔티티-조인-컬럼-활용-필수)
|
1. [컴포넌트별 테이블 설정 (핵심 원칙)](#1-컴포넌트별-테이블-설정-핵심-원칙)
|
||||||
2. [폼 데이터 관리](#2-폼-데이터-관리)
|
2. [엔티티 조인 컬럼 활용 (필수)](#2-엔티티-조인-컬럼-활용-필수)
|
||||||
3. [다국어 지원](#3-다국어-지원)
|
3. [폼 데이터 관리](#3-폼-데이터-관리)
|
||||||
4. [컬럼 설정 패널 구현](#4-컬럼-설정-패널-구현)
|
4. [다국어 지원](#4-다국어-지원)
|
||||||
5. [체크리스트](#5-체크리스트)
|
5. [컬럼 설정 패널 구현](#5-컬럼-설정-패널-구현)
|
||||||
|
6. [체크리스트](#6-체크리스트)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 엔티티 조인 컬럼 활용 (필수)
|
## 1. 컴포넌트별 테이블 설정 (핵심 원칙)
|
||||||
|
|
||||||
|
### 핵심 원칙
|
||||||
|
|
||||||
|
**하나의 화면에서 여러 테이블을 다룰 수 있습니다.**
|
||||||
|
|
||||||
|
화면 생성 시 "메인 테이블"을 필수로 지정하지 않으며, 컴포넌트별로 사용할 테이블을 지정할 수 있습니다.
|
||||||
|
|
||||||
|
### 왜 필요한가?
|
||||||
|
|
||||||
|
일반적인 ERP 화면에서는 여러 테이블이 동시에 필요합니다:
|
||||||
|
|
||||||
|
| 예시: 입고 화면 | 테이블 | 용도 |
|
||||||
|
| --------------- | ----------------------- | ------------------------------- |
|
||||||
|
| 메인 폼 | `receiving_mng` | 입고 마스터 정보 입력/저장 |
|
||||||
|
| 조회 리스트 | `purchase_order_detail` | 발주 상세 목록 조회 (읽기 전용) |
|
||||||
|
| 입력 리피터 | `receiving_detail` | 입고 상세 항목 입력/저장 |
|
||||||
|
|
||||||
|
### 컴포넌트 설정 패턴
|
||||||
|
|
||||||
|
#### 1. 테이블 리스트 (조회용)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TableListConfig {
|
||||||
|
// 조회용 테이블 (화면 메인 테이블과 다를 수 있음)
|
||||||
|
customTableName?: string; // 사용할 테이블명
|
||||||
|
useCustomTable?: boolean; // true: customTableName 사용
|
||||||
|
isReadOnly?: boolean; // true: 조회만, 저장 안 함
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 리피터 (입력/저장용)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UnifiedRepeaterConfig {
|
||||||
|
// 저장 대상 테이블 (화면 메인 테이블과 다를 수 있음)
|
||||||
|
mainTableName?: string; // 저장할 테이블명
|
||||||
|
useCustomTable?: boolean; // true: mainTableName 사용
|
||||||
|
|
||||||
|
// FK 자동 연결 (마스터-디테일 관계)
|
||||||
|
foreignKeyColumn?: string; // 이 테이블의 FK 컬럼 (예: receiving_id)
|
||||||
|
foreignKeySourceColumn?: string; // 마스터 테이블의 PK 컬럼 (예: id)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 저장 테이블 설정 UI 표준
|
||||||
|
|
||||||
|
리피터 등 저장 기능이 있는 컴포넌트의 ConfigPanel에서:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 1. 테이블 선택 Combobox
|
||||||
|
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className="w-full justify-between"
|
||||||
|
>
|
||||||
|
{selectedTableName || "테이블 선택..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." />
|
||||||
|
<CommandList>
|
||||||
|
{/* 그룹 1: 현재 화면 테이블 (기본) */}
|
||||||
|
<CommandGroup heading="기본">
|
||||||
|
<CommandItem value={currentTableName}>
|
||||||
|
<Database className="h-3 w-3 text-blue-500" />
|
||||||
|
{currentTableName}
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
|
||||||
|
{/* 그룹 2: 연관 테이블 (FK 자동 설정) */}
|
||||||
|
{relatedTables.length > 0 && (
|
||||||
|
<CommandGroup heading="연관 테이블 (FK 자동 설정)">
|
||||||
|
{relatedTables.map((table) => (
|
||||||
|
<CommandItem key={table.tableName} value={table.tableName}>
|
||||||
|
<Link2 className="h-3 w-3 text-green-500" />
|
||||||
|
{table.tableName}
|
||||||
|
<span className="text-xs text-muted-foreground ml-auto">
|
||||||
|
FK: {table.foreignKeyColumn}
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 그룹 3: 전체 테이블 */}
|
||||||
|
<CommandGroup heading="전체 테이블">
|
||||||
|
{allTables.map((table) => (
|
||||||
|
<CommandItem key={table.tableName} value={table.tableName}>
|
||||||
|
{table.displayName || table.tableName}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>;
|
||||||
|
|
||||||
|
// 2. 연관 테이블 선택 시 FK/PK 자동 설정
|
||||||
|
const handleSaveTableSelect = (tableName: string) => {
|
||||||
|
const relation = relatedTables.find((r) => r.tableName === tableName);
|
||||||
|
|
||||||
|
if (relation) {
|
||||||
|
// 엔티티 관계에서 자동으로 FK/PK 가져옴
|
||||||
|
updateConfig({
|
||||||
|
useCustomTable: true,
|
||||||
|
mainTableName: tableName,
|
||||||
|
foreignKeyColumn: relation.foreignKeyColumn,
|
||||||
|
foreignKeySourceColumn: relation.referenceColumn,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 연관 테이블이 아니면 수동 입력 필요
|
||||||
|
updateConfig({
|
||||||
|
useCustomTable: true,
|
||||||
|
mainTableName: tableName,
|
||||||
|
foreignKeyColumn: undefined,
|
||||||
|
foreignKeySourceColumn: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 연관 테이블 조회 API
|
||||||
|
|
||||||
|
엔티티 관계에서 현재 테이블을 참조하는 테이블 목록을 조회합니다:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// API 호출
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/api/table-management/columns/${currentTableName}/referenced-by`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 응답
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
tableName: "receiving_detail", // 참조하는 테이블
|
||||||
|
columnName: "receiving_id", // FK 컬럼
|
||||||
|
referenceColumn: "id", // 참조되는 컬럼 (PK)
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FK 자동 연결 동작
|
||||||
|
|
||||||
|
마스터 저장 후 디테일 저장 시 FK가 자동으로 설정됩니다:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. 마스터 저장 이벤트 발생 (ButtonConfigPanel에서)
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("repeaterSave", {
|
||||||
|
detail: {
|
||||||
|
masterRecordId: savedId, // 마스터 테이블에 저장된 ID
|
||||||
|
tableName: "receiving_mng",
|
||||||
|
mainFormData: formData,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. 리피터에서 이벤트 수신 및 FK 설정
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSaveEvent = (event: CustomEvent) => {
|
||||||
|
const { masterRecordId } = event.detail;
|
||||||
|
|
||||||
|
if (config.foreignKeyColumn && masterRecordId) {
|
||||||
|
// 모든 행에 FK 값 자동 설정
|
||||||
|
const updatedRows = rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
[config.foreignKeyColumn]: masterRecordId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 저장 실행
|
||||||
|
saveRows(updatedRows);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("repeaterSave", handleSaveEvent);
|
||||||
|
return () => window.removeEventListener("repeaterSave", handleSaveEvent);
|
||||||
|
}, [config.foreignKeyColumn, rows]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 저장 테이블 변경 시 컬럼 자동 로드
|
||||||
|
|
||||||
|
저장 테이블이 변경되면 해당 테이블의 컬럼이 자동으로 로드됩니다:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 저장 테이블 또는 화면 테이블 기준으로 컬럼 로드
|
||||||
|
const targetTableForColumns =
|
||||||
|
config.useCustomTable && config.mainTableName
|
||||||
|
? config.mainTableName
|
||||||
|
: currentTableName;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadColumns = async () => {
|
||||||
|
if (!targetTableForColumns) return;
|
||||||
|
|
||||||
|
const columnData = await tableTypeApi.getColumns(targetTableForColumns);
|
||||||
|
setCurrentTableColumns(columnData);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadColumns();
|
||||||
|
}, [targetTableForColumns]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 요약
|
||||||
|
|
||||||
|
| 상황 | 처리 방법 |
|
||||||
|
| ------------------------------------- | ----------------------------------- |
|
||||||
|
| 화면과 같은 테이블에 저장 | `useCustomTable: false` (기본값) |
|
||||||
|
| 다른 테이블에 저장 + 엔티티 관계 있음 | 연관 테이블 선택 → FK/PK 자동 설정 |
|
||||||
|
| 다른 테이블에 저장 + 엔티티 관계 없음 | 전체 테이블에서 선택 → FK 수동 입력 |
|
||||||
|
| 조회만 (저장 안 함) | `isReadOnly: true` 설정 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 엔티티 조인 컬럼 활용 (필수)
|
||||||
|
|
||||||
### 핵심 원칙
|
### 핵심 원칙
|
||||||
|
|
||||||
|
|
@ -283,7 +506,7 @@ const getEntityJoinValue = (item: any, columnName: string): any => {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 폼 데이터 관리
|
## 3. 폼 데이터 관리
|
||||||
|
|
||||||
### 통합 폼 시스템 (UnifiedFormContext)
|
### 통합 폼 시스템 (UnifiedFormContext)
|
||||||
|
|
||||||
|
|
@ -368,7 +591,7 @@ const handleChange = useCallback(
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 다국어 지원
|
## 4. 다국어 지원
|
||||||
|
|
||||||
### 타입 정의 시 다국어 필드 추가
|
### 타입 정의 시 다국어 필드 추가
|
||||||
|
|
||||||
|
|
@ -534,7 +757,7 @@ if (comp.componentType === "my-new-component") {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 컬럼 설정 패널 구현
|
## 5. 컬럼 설정 패널 구현
|
||||||
|
|
||||||
### 필수 구조
|
### 필수 구조
|
||||||
|
|
||||||
|
|
@ -639,10 +862,19 @@ export const MyComponentConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 체크리스트
|
## 6. 체크리스트
|
||||||
|
|
||||||
새 컴포넌트 개발 시 다음 항목을 확인하세요:
|
새 컴포넌트 개발 시 다음 항목을 확인하세요:
|
||||||
|
|
||||||
|
### 컴포넌트별 테이블 설정 (핵심)
|
||||||
|
|
||||||
|
- [ ] 화면 메인 테이블과 다른 테이블을 사용할 수 있는지 확인
|
||||||
|
- [ ] `useCustomTable`, `mainTableName` (또는 `customTableName`) 설정 지원
|
||||||
|
- [ ] 연관 테이블 선택 시 FK/PK 자동 설정 (`/api/table-management/columns/:tableName/referenced-by` API 활용)
|
||||||
|
- [ ] 저장 테이블 변경 시 해당 테이블의 컬럼 자동 로드
|
||||||
|
- [ ] 테이블 선택 UI는 Combobox 형태로 그룹별 표시 (기본/연관/전체)
|
||||||
|
- [ ] FK 자동 연결: `repeaterSave` 이벤트에서 `masterRecordId` 수신 및 적용
|
||||||
|
|
||||||
### 엔티티 조인 (필수)
|
### 엔티티 조인 (필수)
|
||||||
|
|
||||||
- [ ] `entityJoinApi.getEntityJoinColumns()` 호출하여 조인 컬럼 로드
|
- [ ] `entityJoinApi.getEntityJoinColumns()` 호출하여 조인 컬럼 로드
|
||||||
|
|
|
||||||
|
|
@ -97,10 +97,10 @@ export async function getColumnList(
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableManagementService = new TableManagementService();
|
const tableManagementService = new TableManagementService();
|
||||||
|
|
||||||
// 🔥 캐시 버스팅: _t 파라미터가 있으면 캐시 무시
|
// 🔥 캐시 버스팅: _t 파라미터가 있으면 캐시 무시
|
||||||
const bustCache = !!req.query._t;
|
const bustCache = !!req.query._t;
|
||||||
|
|
||||||
const result = await tableManagementService.getColumnList(
|
const result = await tableManagementService.getColumnList(
|
||||||
tableName,
|
tableName,
|
||||||
parseInt(page as string),
|
parseInt(page as string),
|
||||||
|
|
@ -2376,3 +2376,90 @@ export async function getTableEntityRelations(
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 테이블을 참조(FK로 연결)하는 테이블 목록 조회
|
||||||
|
* GET /api/table-management/columns/:tableName/referenced-by
|
||||||
|
*
|
||||||
|
* column_labels에서 reference_table이 현재 테이블인 레코드를 찾아서
|
||||||
|
* 해당 테이블과 FK 컬럼 정보를 반환합니다.
|
||||||
|
*/
|
||||||
|
export async function getReferencedByTables(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`=== 테이블 참조 관계 조회 시작: ${tableName} 을 참조하는 테이블 ===`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "tableName 파라미터가 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_PARAMETERS",
|
||||||
|
details: "tableName 경로 파라미터가 필요합니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// column_labels에서 reference_table이 현재 테이블인 레코드 조회
|
||||||
|
// input_type이 'entity'인 것만 조회 (실제 FK 관계)
|
||||||
|
const sqlQuery = `
|
||||||
|
SELECT DISTINCT
|
||||||
|
cl.table_name,
|
||||||
|
cl.column_name,
|
||||||
|
cl.column_label,
|
||||||
|
cl.reference_table,
|
||||||
|
cl.reference_column,
|
||||||
|
cl.display_column,
|
||||||
|
cl.table_name as table_label
|
||||||
|
FROM column_labels cl
|
||||||
|
WHERE cl.reference_table = $1
|
||||||
|
AND cl.input_type = 'entity'
|
||||||
|
ORDER BY cl.table_name, cl.column_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await query(sqlQuery, [tableName]);
|
||||||
|
|
||||||
|
const referencedByTables = result.map((row: any) => ({
|
||||||
|
tableName: row.table_name,
|
||||||
|
tableLabel: row.table_label,
|
||||||
|
columnName: row.column_name,
|
||||||
|
columnLabel: row.column_label,
|
||||||
|
referenceTable: row.reference_table,
|
||||||
|
referenceColumn: row.reference_column || "id",
|
||||||
|
displayColumn: row.display_column,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`테이블 참조 관계 조회 완료: ${referencedByTables.length}개 발견`
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: ApiResponse<any> = {
|
||||||
|
success: true,
|
||||||
|
message: `${referencedByTables.length}개의 테이블이 ${tableName}을 참조합니다.`,
|
||||||
|
data: referencedByTables,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("테이블 참조 관계 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블 참조 관계 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "REFERENCED_BY_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
||||||
multiTableSave, // 🆕 범용 다중 테이블 저장
|
multiTableSave, // 🆕 범용 다중 테이블 저장
|
||||||
getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회
|
getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회
|
||||||
|
getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회
|
||||||
} from "../controllers/tableManagementController";
|
} from "../controllers/tableManagementController";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
@ -54,6 +55,14 @@ router.get("/tables/entity-relations", getTableEntityRelations);
|
||||||
*/
|
*/
|
||||||
router.get("/tables/:tableName/columns", getColumnList);
|
router.get("/tables/:tableName/columns", getColumnList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 테이블을 참조하는 테이블 목록 조회
|
||||||
|
* GET /api/table-management/columns/:tableName/referenced-by
|
||||||
|
*
|
||||||
|
* 리피터 컴포넌트에서 저장 테이블 선택 시 FK 관계를 자동으로 가져오기 위해 사용
|
||||||
|
*/
|
||||||
|
router.get("/columns/:tableName/referenced-by", getReferencedByTables);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블 라벨 설정
|
* 테이블 라벨 설정
|
||||||
* PUT /api/table-management/tables/:tableName/label
|
* PUT /api/table-management/tables/:tableName/label
|
||||||
|
|
|
||||||
|
|
@ -185,16 +185,18 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||||
}
|
}
|
||||||
}, [open, screenCode]);
|
}, [open, screenCode]);
|
||||||
|
|
||||||
|
// 테이블 선택은 선택 사항 - 컴포넌트별로 테이블을 설정할 수 있음
|
||||||
const isValid = useMemo(() => {
|
const isValid = useMemo(() => {
|
||||||
const baseValid = screenName.trim().length > 0 && screenCode.trim().length > 0;
|
const baseValid = screenName.trim().length > 0 && screenCode.trim().length > 0;
|
||||||
|
|
||||||
if (dataSourceType === "database") {
|
if (dataSourceType === "database") {
|
||||||
return baseValid && tableName.trim().length > 0;
|
// 테이블 선택은 선택 사항 (비워두면 컴포넌트별로 테이블 설정)
|
||||||
|
return baseValid;
|
||||||
} else {
|
} else {
|
||||||
// REST API: 연결 선택 필수
|
// REST API: 연결 선택 필수
|
||||||
return baseValid && selectedRestApiId !== null;
|
return baseValid && selectedRestApiId !== null;
|
||||||
}
|
}
|
||||||
}, [screenName, screenCode, tableName, dataSourceType, selectedRestApiId]);
|
}, [screenName, screenCode, dataSourceType, selectedRestApiId]);
|
||||||
|
|
||||||
// 테이블 필터링 (내부 DB용)
|
// 테이블 필터링 (내부 DB용)
|
||||||
const filteredTables = useMemo(() => {
|
const filteredTables = useMemo(() => {
|
||||||
|
|
@ -230,8 +232,8 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dataSourceType === "database") {
|
if (dataSourceType === "database") {
|
||||||
// 데이터베이스 소스
|
// 데이터베이스 소스 - 테이블 선택은 선택 사항
|
||||||
createData.tableName = tableName.trim();
|
createData.tableName = tableName.trim() || null; // 비어있으면 null
|
||||||
createData.dbSourceType = selectedDbSource === "internal" ? "internal" : "external";
|
createData.dbSourceType = selectedDbSource === "internal" ? "internal" : "external";
|
||||||
createData.dbConnectionId = selectedDbSource === "internal" ? undefined : Number(selectedDbSource);
|
createData.dbConnectionId = selectedDbSource === "internal" ? undefined : Number(selectedDbSource);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -507,7 +509,10 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||||
{/* 테이블 선택 (데이터베이스 모드일 때만) */}
|
{/* 테이블 선택 (데이터베이스 모드일 때만) */}
|
||||||
{dataSourceType === "database" && (
|
{dataSourceType === "database" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="tableName">테이블 *</Label>
|
<Label htmlFor="tableName">기본 테이블 (선택)</Label>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
비워두면 화면 디자이너에서 컴포넌트별로 테이블을 설정할 수 있습니다.
|
||||||
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={tableName}
|
value={tableName}
|
||||||
onValueChange={setTableName}
|
onValueChange={setTableName}
|
||||||
|
|
@ -521,7 +526,7 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder={loadingExternalTables ? "로딩 중..." : "테이블을 선택하세요"} />
|
<SelectValue placeholder={loadingExternalTables ? "로딩 중..." : "(선택 사항) 기본 테이블 선택"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-80">
|
<SelectContent className="max-h-80">
|
||||||
{/* 검색 입력 필드 */}
|
{/* 검색 입력 필드 */}
|
||||||
|
|
|
||||||
|
|
@ -1011,6 +1011,94 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
loadScreenDataSource();
|
loadScreenDataSource();
|
||||||
}, [selectedScreen?.tableName, selectedScreen?.screenName, selectedScreen?.dataSourceType, selectedScreen?.restApiConnectionId, selectedScreen?.restApiEndpoint, selectedScreen?.restApiJsonPath]);
|
}, [selectedScreen?.tableName, selectedScreen?.screenName, selectedScreen?.dataSourceType, selectedScreen?.restApiConnectionId, selectedScreen?.restApiEndpoint, selectedScreen?.restApiJsonPath]);
|
||||||
|
|
||||||
|
// 테이블 선택 핸들러 - 사이드바에서 테이블 선택 시 호출
|
||||||
|
const handleTableSelect = useCallback(async (tableName: string) => {
|
||||||
|
console.log("📊 테이블 선택:", tableName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 테이블 라벨 조회
|
||||||
|
const tableListResponse = await tableManagementApi.getTableList();
|
||||||
|
const currentTable =
|
||||||
|
tableListResponse.success && tableListResponse.data
|
||||||
|
? tableListResponse.data.find((t: any) => (t.tableName || t.table_name) === tableName)
|
||||||
|
: null;
|
||||||
|
const tableLabel = currentTable?.displayName || currentTable?.table_label || tableName;
|
||||||
|
|
||||||
|
// 테이블 컬럼 정보 조회
|
||||||
|
const columnsResponse = await tableTypeApi.getColumns(tableName, true);
|
||||||
|
|
||||||
|
const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => {
|
||||||
|
const inputType = col.inputType || col.input_type;
|
||||||
|
const widgetType = inputType || col.widgetType || col.widget_type || col.webType || col.web_type;
|
||||||
|
|
||||||
|
let detailSettings = col.detailSettings || col.detail_settings;
|
||||||
|
if (typeof detailSettings === "string") {
|
||||||
|
try {
|
||||||
|
detailSettings = JSON.parse(detailSettings);
|
||||||
|
} catch (e) {
|
||||||
|
detailSettings = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tableName: col.tableName || tableName,
|
||||||
|
columnName: col.columnName || col.column_name,
|
||||||
|
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||||
|
dataType: col.dataType || col.data_type || col.dbType,
|
||||||
|
webType: col.webType || col.web_type,
|
||||||
|
input_type: inputType,
|
||||||
|
inputType: inputType,
|
||||||
|
widgetType,
|
||||||
|
isNullable: col.isNullable || col.is_nullable,
|
||||||
|
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
||||||
|
columnDefault: col.columnDefault || col.column_default,
|
||||||
|
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
||||||
|
codeCategory: col.codeCategory || col.code_category,
|
||||||
|
codeValue: col.codeValue || col.code_value,
|
||||||
|
referenceTable: detailSettings?.referenceTable || col.referenceTable || col.reference_table,
|
||||||
|
referenceColumn: detailSettings?.referenceColumn || col.referenceColumn || col.reference_column,
|
||||||
|
displayColumn: detailSettings?.displayColumn || col.displayColumn || col.display_column,
|
||||||
|
detailSettings,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableInfo: TableInfo = {
|
||||||
|
tableName,
|
||||||
|
tableLabel,
|
||||||
|
columns,
|
||||||
|
};
|
||||||
|
|
||||||
|
setTables([tableInfo]);
|
||||||
|
toast.success(`테이블 "${tableLabel}" 선택됨`);
|
||||||
|
|
||||||
|
// 기존 테이블과 다른 테이블 선택 시, 기존 컴포넌트 중 다른 테이블 컬럼은 제거
|
||||||
|
if (tables.length > 0 && tables[0].tableName !== tableName) {
|
||||||
|
setLayout((prev) => {
|
||||||
|
const newComponents = prev.components.filter((comp) => {
|
||||||
|
// 테이블 컬럼 기반 컴포넌트인지 확인
|
||||||
|
if (comp.tableName && comp.tableName !== tableName) {
|
||||||
|
console.log("🗑️ 다른 테이블 컴포넌트 제거:", comp.tableName, comp.columnName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newComponents.length < prev.components.length) {
|
||||||
|
toast.info(`이전 테이블(${tables[0].tableName})의 컴포넌트가 ${prev.components.length - newComponents.length}개 제거되었습니다.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
components: newComponents,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("테이블 정보 로드 실패:", error);
|
||||||
|
toast.error("테이블 정보를 불러오는데 실패했습니다.");
|
||||||
|
}
|
||||||
|
}, [tables]);
|
||||||
|
|
||||||
// 화면 레이아웃 로드
|
// 화면 레이아웃 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedScreen?.screenId) {
|
if (selectedScreen?.screenId) {
|
||||||
|
|
@ -4418,8 +4506,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
};
|
};
|
||||||
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
|
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
|
||||||
}}
|
}}
|
||||||
selectedTableName={selectedScreen.tableName}
|
selectedTableName={selectedScreen?.tableName}
|
||||||
placedColumns={placedColumns}
|
placedColumns={placedColumns}
|
||||||
|
onTableSelect={handleTableSelect}
|
||||||
|
showTableSelector={true}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ interface ComponentsPanelProps {
|
||||||
onTableDragStart?: (e: React.DragEvent, table: TableInfo, column?: ColumnInfo) => void;
|
onTableDragStart?: (e: React.DragEvent, table: TableInfo, column?: ColumnInfo) => void;
|
||||||
selectedTableName?: string;
|
selectedTableName?: string;
|
||||||
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합
|
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합
|
||||||
|
// 테이블 선택 관련 props
|
||||||
|
onTableSelect?: (tableName: string) => void; // 테이블 선택 콜백
|
||||||
|
showTableSelector?: boolean; // 테이블 선택 UI 표시 여부 (기본: 테이블 없으면 표시)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComponentsPanel({
|
export function ComponentsPanel({
|
||||||
|
|
@ -28,6 +31,8 @@ export function ComponentsPanel({
|
||||||
onTableDragStart,
|
onTableDragStart,
|
||||||
selectedTableName,
|
selectedTableName,
|
||||||
placedColumns,
|
placedColumns,
|
||||||
|
onTableSelect,
|
||||||
|
showTableSelector = true,
|
||||||
}: ComponentsPanelProps) {
|
}: ComponentsPanelProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
|
@ -272,24 +277,16 @@ export function ComponentsPanel({
|
||||||
|
|
||||||
{/* 테이블 컬럼 탭 */}
|
{/* 테이블 컬럼 탭 */}
|
||||||
<TabsContent value="tables" className="mt-0 flex-1 overflow-y-auto">
|
<TabsContent value="tables" className="mt-0 flex-1 overflow-y-auto">
|
||||||
{tables.length > 0 && onTableDragStart ? (
|
<TablesPanel
|
||||||
<TablesPanel
|
tables={tables}
|
||||||
tables={tables}
|
searchTerm={searchTerm}
|
||||||
searchTerm={searchTerm}
|
onSearchChange={onSearchChange || (() => {})}
|
||||||
onSearchChange={onSearchChange || (() => {})}
|
onDragStart={onTableDragStart || (() => {})}
|
||||||
onDragStart={onTableDragStart}
|
selectedTableName={selectedTableName}
|
||||||
selectedTableName={selectedTableName}
|
placedColumns={placedColumns}
|
||||||
placedColumns={placedColumns}
|
onTableSelect={onTableSelect}
|
||||||
/>
|
showTableSelector={showTableSelector}
|
||||||
) : (
|
/>
|
||||||
<div className="flex h-32 items-center justify-center text-center">
|
|
||||||
<div className="p-6">
|
|
||||||
<Database className="text-muted-foreground/40 mx-auto mb-2 h-10 w-10" />
|
|
||||||
<p className="text-muted-foreground text-xs font-medium">테이블이 없습니다</p>
|
|
||||||
<p className="text-muted-foreground/60 mt-1 text-xs">화면에 테이블을 선택해주세요</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 컴포넌트 탭 */}
|
{/* 컴포넌트 탭 */}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Database,
|
Database,
|
||||||
Type,
|
Type,
|
||||||
|
|
@ -16,9 +24,13 @@ import {
|
||||||
Link2,
|
Link2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TableInfo, WebType } from "@/types/screen";
|
import { TableInfo, WebType } from "@/types/screen";
|
||||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
|
|
||||||
interface EntityJoinColumn {
|
interface EntityJoinColumn {
|
||||||
columnName: string;
|
columnName: string;
|
||||||
|
|
@ -41,6 +53,9 @@ interface TablesPanelProps {
|
||||||
onDragStart: (e: React.DragEvent, table: TableInfo, column?: any) => void;
|
onDragStart: (e: React.DragEvent, table: TableInfo, column?: any) => void;
|
||||||
selectedTableName?: string;
|
selectedTableName?: string;
|
||||||
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합 (tableName.columnName 형식)
|
placedColumns?: Set<string>; // 이미 배치된 컬럼명 집합 (tableName.columnName 형식)
|
||||||
|
// 테이블 선택 관련 props
|
||||||
|
onTableSelect?: (tableName: string) => void; // 테이블 선택 콜백
|
||||||
|
showTableSelector?: boolean; // 테이블 선택 UI 표시 여부
|
||||||
}
|
}
|
||||||
|
|
||||||
// 위젯 타입별 아이콘
|
// 위젯 타입별 아이콘
|
||||||
|
|
@ -81,12 +96,20 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
searchTerm,
|
searchTerm,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
placedColumns = new Set(),
|
placedColumns = new Set(),
|
||||||
|
onTableSelect,
|
||||||
|
showTableSelector = false,
|
||||||
}) => {
|
}) => {
|
||||||
// 엔티티 조인 컬럼 상태
|
// 엔티티 조인 컬럼 상태
|
||||||
const [entityJoinTables, setEntityJoinTables] = useState<EntityJoinTable[]>([]);
|
const [entityJoinTables, setEntityJoinTables] = useState<EntityJoinTable[]>([]);
|
||||||
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
||||||
const [expandedJoinTables, setExpandedJoinTables] = useState<Set<string>>(new Set());
|
const [expandedJoinTables, setExpandedJoinTables] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// 전체 테이블 목록 (테이블 선택용)
|
||||||
|
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
||||||
|
const [loadingAllTables, setLoadingAllTables] = useState(false);
|
||||||
|
const [tableSearchTerm, setTableSearchTerm] = useState("");
|
||||||
|
const [showTableSelectDropdown, setShowTableSelectDropdown] = useState(false);
|
||||||
|
|
||||||
// 시스템 컬럼 목록 (숨김 처리)
|
// 시스템 컬럼 목록 (숨김 처리)
|
||||||
const systemColumns = new Set([
|
const systemColumns = new Set([
|
||||||
"id",
|
"id",
|
||||||
|
|
@ -96,6 +119,42 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
"company_code",
|
"company_code",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 전체 테이블 목록 로드
|
||||||
|
const loadAllTables = useCallback(async () => {
|
||||||
|
if (allTables.length > 0) return; // 이미 로드됨
|
||||||
|
|
||||||
|
setLoadingAllTables(true);
|
||||||
|
try {
|
||||||
|
const response = await tableManagementApi.getTableList();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setAllTables(response.data.map((t: any) => ({
|
||||||
|
tableName: t.tableName || t.table_name,
|
||||||
|
displayName: t.displayName || t.table_label || t.tableName || t.table_name,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("테이블 목록 조회 오류:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingAllTables(false);
|
||||||
|
}
|
||||||
|
}, [allTables.length]);
|
||||||
|
|
||||||
|
// 테이블 선택 시 호출
|
||||||
|
const handleTableSelect = (tableName: string) => {
|
||||||
|
setShowTableSelectDropdown(false);
|
||||||
|
setTableSearchTerm("");
|
||||||
|
onTableSelect?.(tableName);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필터링된 테이블 목록
|
||||||
|
const filteredAllTables = tableSearchTerm
|
||||||
|
? allTables.filter(
|
||||||
|
(t) =>
|
||||||
|
t.tableName.toLowerCase().includes(tableSearchTerm.toLowerCase()) ||
|
||||||
|
t.displayName.toLowerCase().includes(tableSearchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
: allTables;
|
||||||
|
|
||||||
// 메인 테이블명 추출
|
// 메인 테이블명 추출
|
||||||
const mainTableName = tables[0]?.tableName;
|
const mainTableName = tables[0]?.tableName;
|
||||||
|
|
||||||
|
|
@ -209,6 +268,91 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
|
{/* 테이블 선택 버튼 (메인 테이블이 없을 때 또는 showTableSelector가 true일 때) */}
|
||||||
|
{(showTableSelector || tables.length === 0) && (
|
||||||
|
<div className="border-b p-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-between"
|
||||||
|
onClick={() => {
|
||||||
|
setShowTableSelectDropdown(!showTableSelectDropdown);
|
||||||
|
if (!showTableSelectDropdown) {
|
||||||
|
loadAllTables();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
{tables.length > 0 ? "테이블 추가/변경" : "테이블 선택"}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 드롭다운 */}
|
||||||
|
{showTableSelectDropdown && (
|
||||||
|
<div className="absolute top-full left-0 z-50 mt-1 w-full rounded-md border bg-white shadow-lg">
|
||||||
|
{/* 검색 */}
|
||||||
|
<div className="border-b p-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="테이블 검색..."
|
||||||
|
value={tableSearchTerm}
|
||||||
|
onChange={(e) => setTableSearchTerm(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
className="w-full rounded-md border px-8 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{tableSearchTerm && (
|
||||||
|
<button
|
||||||
|
onClick={() => setTableSearchTerm("")}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 목록 */}
|
||||||
|
<div className="max-h-60 overflow-y-auto">
|
||||||
|
{loadingAllTables ? (
|
||||||
|
<div className="p-4 text-center text-sm text-gray-500">로드 중...</div>
|
||||||
|
) : filteredAllTables.length === 0 ? (
|
||||||
|
<div className="p-4 text-center text-sm text-gray-500">
|
||||||
|
{tableSearchTerm ? "검색 결과 없음" : "테이블 없음"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredAllTables.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.tableName}
|
||||||
|
onClick={() => handleTableSelect(t.tableName)}
|
||||||
|
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<Database className="h-3.5 w-3.5 text-blue-600" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate font-medium">{t.displayName}</div>
|
||||||
|
<div className="truncate text-xs text-gray-500">{t.tableName}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 현재 테이블 정보 */}
|
||||||
|
{tables.length > 0 && (
|
||||||
|
<div className="mt-2 text-xs text-muted-foreground">
|
||||||
|
현재: {tables[0]?.tableLabel || tables[0]?.tableName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 테이블과 컬럼 평면 목록 */}
|
{/* 테이블과 컬럼 평면 목록 */}
|
||||||
<div className="flex-1 overflow-y-auto p-3">
|
<div className="flex-1 overflow-y-auto p-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,24 @@ import {
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Wand2,
|
Wand2,
|
||||||
|
Check,
|
||||||
|
ChevronsUpDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
import { getAvailableNumberingRules, getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule";
|
import { getAvailableNumberingRules, getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule";
|
||||||
import { NumberingRuleConfig } from "@/types/numbering-rule";
|
import { NumberingRuleConfig } from "@/types/numbering-rule";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -42,6 +58,14 @@ import {
|
||||||
MODAL_SIZE_OPTIONS,
|
MODAL_SIZE_OPTIONS,
|
||||||
} from "@/types/unified-repeater";
|
} from "@/types/unified-repeater";
|
||||||
|
|
||||||
|
// 테이블 엔티티 관계 정보
|
||||||
|
interface TableRelation {
|
||||||
|
tableName: string;
|
||||||
|
tableLabel: string;
|
||||||
|
foreignKeyColumn: string; // 저장 테이블의 FK 컬럼
|
||||||
|
referenceColumn: string; // 마스터 테이블의 PK 컬럼
|
||||||
|
}
|
||||||
|
|
||||||
interface UnifiedRepeaterConfigPanelProps {
|
interface UnifiedRepeaterConfigPanelProps {
|
||||||
config: UnifiedRepeaterConfig;
|
config: UnifiedRepeaterConfig;
|
||||||
onChange: (config: UnifiedRepeaterConfig) => void;
|
onChange: (config: UnifiedRepeaterConfig) => void;
|
||||||
|
|
@ -117,6 +141,13 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
||||||
const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);
|
const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);
|
||||||
|
|
||||||
|
// 저장 테이블 관련 상태
|
||||||
|
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
||||||
|
const [loadingTables, setLoadingTables] = useState(false);
|
||||||
|
const [relatedTables, setRelatedTables] = useState<TableRelation[]>([]); // 현재 테이블과 연관된 테이블 목록
|
||||||
|
const [loadingRelations, setLoadingRelations] = useState(false);
|
||||||
|
const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // 테이블 Combobox 열림 상태
|
||||||
|
|
||||||
// 🆕 확장된 컬럼 (상세 설정 표시용)
|
// 🆕 확장된 컬럼 (상세 설정 표시용)
|
||||||
const [expandedColumn, setExpandedColumn] = useState<string | null>(null);
|
const [expandedColumn, setExpandedColumn] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -199,6 +230,60 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
loadNumberingRules();
|
loadNumberingRules();
|
||||||
}, [selectedMenuObjid]);
|
}, [selectedMenuObjid]);
|
||||||
|
|
||||||
|
// 전체 테이블 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTables = async () => {
|
||||||
|
setLoadingTables(true);
|
||||||
|
try {
|
||||||
|
const response = await tableManagementApi.getTableList();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setAllTables(response.data.map((t: any) => ({
|
||||||
|
tableName: t.tableName || t.table_name,
|
||||||
|
displayName: t.displayName || t.table_label || t.tableName || t.table_name,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("테이블 목록 로드 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingTables(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadTables();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 현재 테이블과 연관된 테이블 목록 로드 (엔티티 관계 기반)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRelatedTables = async () => {
|
||||||
|
if (!currentTableName) {
|
||||||
|
setRelatedTables([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingRelations(true);
|
||||||
|
try {
|
||||||
|
// column_labels에서 현재 테이블을 reference_table로 참조하는 테이블 찾기
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
const relations: TableRelation[] = response.data.data.map((rel: any) => ({
|
||||||
|
tableName: rel.tableName || rel.table_name,
|
||||||
|
tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
|
||||||
|
foreignKeyColumn: rel.columnName || rel.column_name, // FK 컬럼
|
||||||
|
referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼
|
||||||
|
}));
|
||||||
|
setRelatedTables(relations);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("연관 테이블 로드 실패:", error);
|
||||||
|
setRelatedTables([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingRelations(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadRelatedTables();
|
||||||
|
}, [currentTableName]);
|
||||||
|
|
||||||
// 설정 업데이트 헬퍼
|
// 설정 업데이트 헬퍼
|
||||||
const updateConfig = useCallback(
|
const updateConfig = useCallback(
|
||||||
(updates: Partial<UnifiedRepeaterConfig>) => {
|
(updates: Partial<UnifiedRepeaterConfig>) => {
|
||||||
|
|
@ -234,10 +319,50 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
[config.features, updateConfig],
|
[config.features, updateConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 현재 화면 테이블 컬럼 로드 + 엔티티 컬럼 감지
|
// 저장 테이블 선택 핸들러 - 엔티티 관계에서 FK/PK 자동 설정
|
||||||
|
const handleSaveTableSelect = useCallback((tableName: string) => {
|
||||||
|
// 빈 값 선택 시 (현재 테이블로 복원)
|
||||||
|
if (!tableName || tableName === currentTableName) {
|
||||||
|
updateConfig({
|
||||||
|
useCustomTable: false,
|
||||||
|
mainTableName: undefined,
|
||||||
|
foreignKeyColumn: undefined,
|
||||||
|
foreignKeySourceColumn: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연관 테이블에서 FK 관계 찾기
|
||||||
|
const relation = relatedTables.find(r => r.tableName === tableName);
|
||||||
|
|
||||||
|
if (relation) {
|
||||||
|
// 엔티티 관계가 있으면 자동으로 FK/PK 설정
|
||||||
|
updateConfig({
|
||||||
|
useCustomTable: true,
|
||||||
|
mainTableName: tableName,
|
||||||
|
foreignKeyColumn: relation.foreignKeyColumn,
|
||||||
|
foreignKeySourceColumn: relation.referenceColumn,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 엔티티 관계가 없으면 직접 입력 필요
|
||||||
|
updateConfig({
|
||||||
|
useCustomTable: true,
|
||||||
|
mainTableName: tableName,
|
||||||
|
foreignKeyColumn: undefined,
|
||||||
|
foreignKeySourceColumn: "id",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentTableName, relatedTables, updateConfig]);
|
||||||
|
|
||||||
|
// 저장 테이블 컬럼 로드 (저장 테이블이 설정되면 해당 테이블, 아니면 현재 화면 테이블)
|
||||||
|
// 실제 저장할 테이블의 컬럼을 보여줘야 함
|
||||||
|
const targetTableForColumns = config.useCustomTable && config.mainTableName
|
||||||
|
? config.mainTableName
|
||||||
|
: currentTableName;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCurrentTableColumns = async () => {
|
const loadCurrentTableColumns = async () => {
|
||||||
if (!currentTableName) {
|
if (!targetTableForColumns) {
|
||||||
setCurrentTableColumns([]);
|
setCurrentTableColumns([]);
|
||||||
setEntityColumns([]);
|
setEntityColumns([]);
|
||||||
return;
|
return;
|
||||||
|
|
@ -245,7 +370,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
|
|
||||||
setLoadingColumns(true);
|
setLoadingColumns(true);
|
||||||
try {
|
try {
|
||||||
const columnData = await tableTypeApi.getColumns(currentTableName);
|
const columnData = await tableTypeApi.getColumns(targetTableForColumns);
|
||||||
const cols: ColumnOption[] = [];
|
const cols: ColumnOption[] = [];
|
||||||
const entityCols: EntityColumnOption[] = [];
|
const entityCols: EntityColumnOption[] = [];
|
||||||
|
|
||||||
|
|
@ -297,7 +422,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
setCurrentTableColumns(cols);
|
setCurrentTableColumns(cols);
|
||||||
setEntityColumns(entityCols);
|
setEntityColumns(entityCols);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("현재 테이블 컬럼 로드 실패:", error);
|
console.error("저장 테이블 컬럼 로드 실패:", error);
|
||||||
setCurrentTableColumns([]);
|
setCurrentTableColumns([]);
|
||||||
setEntityColumns([]);
|
setEntityColumns([]);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -305,7 +430,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadCurrentTableColumns();
|
loadCurrentTableColumns();
|
||||||
}, [currentTableName]);
|
}, [targetTableForColumns]);
|
||||||
|
|
||||||
// 소스(엔티티) 테이블 컬럼 로드 (모달 모드일 때)
|
// 소스(엔티티) 테이블 컬럼 로드 (모달 모드일 때)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -529,97 +654,185 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* 저장 대상 테이블 설정 */}
|
{/* 저장 대상 테이블 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-medium">저장 대상 테이블</Label>
|
<Label className="text-xs font-medium">저장 테이블</Label>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
화면 메인 테이블과 다른 테이블에 저장할 경우 설정
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
{/* 현재 선택된 테이블 표시 (기존 테이블 UI와 동일한 스타일) */}
|
||||||
<Checkbox
|
<div className={cn(
|
||||||
id="useCustomTable"
|
"rounded-lg border p-3",
|
||||||
checked={config.useCustomTable || false}
|
config.useCustomTable && config.mainTableName
|
||||||
onCheckedChange={(checked) => {
|
? "border-orange-300 bg-orange-50"
|
||||||
if (!checked) {
|
: "border-blue-300 bg-blue-50"
|
||||||
updateConfig({
|
)}>
|
||||||
useCustomTable: false,
|
<div className="flex items-center gap-2">
|
||||||
mainTableName: undefined,
|
<Database className={cn(
|
||||||
foreignKeyColumn: undefined,
|
"h-4 w-4",
|
||||||
foreignKeySourceColumn: undefined,
|
config.useCustomTable && config.mainTableName
|
||||||
});
|
? "text-orange-600"
|
||||||
} else {
|
: "text-blue-600"
|
||||||
updateConfig({ useCustomTable: true });
|
)} />
|
||||||
}
|
<div className="flex-1">
|
||||||
}}
|
<p className={cn(
|
||||||
/>
|
"text-sm font-medium",
|
||||||
<label htmlFor="useCustomTable" className="text-xs">다른 테이블에 저장</label>
|
config.useCustomTable && config.mainTableName
|
||||||
|
? "text-orange-700"
|
||||||
|
: "text-blue-700"
|
||||||
|
)}>
|
||||||
|
{config.useCustomTable && config.mainTableName
|
||||||
|
? (allTables.find(t => t.tableName === config.mainTableName)?.displayName || config.mainTableName)
|
||||||
|
: (currentTableName || "미설정")
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
{config.useCustomTable && config.mainTableName && config.foreignKeyColumn && (
|
||||||
|
<p className="text-[10px] text-orange-600 mt-0.5">
|
||||||
|
FK: {config.foreignKeyColumn} → {currentTableName}.{config.foreignKeySourceColumn || "id"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!config.useCustomTable && currentTableName && (
|
||||||
|
<p className="text-[10px] text-blue-600 mt-0.5">화면 메인 테이블</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.useCustomTable && (
|
{/* 테이블 변경 Combobox */}
|
||||||
<div className="space-y-2 rounded border border-orange-200 bg-orange-50 p-2">
|
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
|
||||||
{/* 저장 테이블 선택 */}
|
<PopoverTrigger asChild>
|
||||||
<div className="space-y-1">
|
<Button
|
||||||
<Label className="text-[10px]">저장 테이블 *</Label>
|
variant="outline"
|
||||||
<Select
|
role="combobox"
|
||||||
value={config.mainTableName || ""}
|
aria-expanded={tableComboboxOpen}
|
||||||
onValueChange={(value) => updateConfig({ mainTableName: value })}
|
disabled={loadingTables || loadingRelations}
|
||||||
>
|
className="h-8 w-full justify-between text-xs"
|
||||||
<SelectTrigger className="h-7 text-xs">
|
>
|
||||||
<SelectValue placeholder="테이블 선택..." />
|
{loadingTables ? "로딩 중..." : "다른 테이블 선택..."}
|
||||||
</SelectTrigger>
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
<SelectContent>
|
</Button>
|
||||||
{currentTableColumns.length === 0 ? (
|
</PopoverTrigger>
|
||||||
<div className="p-2 text-xs text-gray-500">테이블 목록 로딩 중...</div>
|
<PopoverContent
|
||||||
) : (
|
className="p-0"
|
||||||
<SelectItem value={currentTableName || ""} disabled>
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||||
<span className="text-gray-400">(화면 메인 테이블을 선택하세요 - 별도 로드 필요)</span>
|
align="start"
|
||||||
</SelectItem>
|
>
|
||||||
)}
|
<Command>
|
||||||
</SelectContent>
|
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||||
</Select>
|
<CommandList className="max-h-60">
|
||||||
<Input
|
<CommandEmpty className="text-xs py-3 text-center">
|
||||||
value={config.mainTableName || ""}
|
테이블을 찾을 수 없습니다.
|
||||||
onChange={(e) => updateConfig({ mainTableName: e.target.value })}
|
</CommandEmpty>
|
||||||
placeholder="테이블명 직접 입력 (예: receiving_detail)"
|
|
||||||
className="h-7 text-xs"
|
{/* 현재 테이블 (기본) */}
|
||||||
/>
|
{currentTableName && (
|
||||||
</div>
|
<CommandGroup heading="기본">
|
||||||
|
<CommandItem
|
||||||
{/* FK 컬럼 설정 */}
|
value={currentTableName}
|
||||||
|
onSelect={() => {
|
||||||
|
handleSaveTableSelect(currentTableName);
|
||||||
|
setTableComboboxOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
!config.useCustomTable || !config.mainTableName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Database className="mr-2 h-3 w-3 text-blue-500" />
|
||||||
|
<span>{currentTableName}</span>
|
||||||
|
<span className="ml-1 text-[10px] text-muted-foreground">(기본)</span>
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 연관 테이블 (엔티티 관계) */}
|
||||||
|
{relatedTables.length > 0 && (
|
||||||
|
<CommandGroup heading="연관 테이블 (FK 자동 설정)">
|
||||||
|
{relatedTables.map((rel) => (
|
||||||
|
<CommandItem
|
||||||
|
key={rel.tableName}
|
||||||
|
value={`${rel.tableName} ${rel.tableLabel}`}
|
||||||
|
onSelect={() => {
|
||||||
|
handleSaveTableSelect(rel.tableName);
|
||||||
|
setTableComboboxOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
config.mainTableName === rel.tableName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Link2 className="mr-2 h-3 w-3 text-orange-500" />
|
||||||
|
<span>{rel.tableLabel}</span>
|
||||||
|
<span className="ml-1 text-[10px] text-muted-foreground">
|
||||||
|
({rel.foreignKeyColumn})
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 전체 테이블 목록 */}
|
||||||
|
<CommandGroup heading="전체 테이블 (FK 직접 입력)">
|
||||||
|
{allTables
|
||||||
|
.filter(t => t.tableName !== currentTableName && !relatedTables.some(r => r.tableName === t.tableName))
|
||||||
|
.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.tableName}
|
||||||
|
value={`${table.tableName} ${table.displayName}`}
|
||||||
|
onSelect={() => {
|
||||||
|
handleSaveTableSelect(table.tableName);
|
||||||
|
setTableComboboxOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
config.mainTableName === table.tableName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Database className="mr-2 h-3 w-3 text-gray-400" />
|
||||||
|
<span>{table.displayName}</span>
|
||||||
|
</CommandItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* FK 직접 입력 (연관 테이블이 아닌 경우만) */}
|
||||||
|
{config.useCustomTable && config.mainTableName &&
|
||||||
|
!relatedTables.some(r => r.tableName === config.mainTableName) && (
|
||||||
|
<div className="space-y-2 rounded border border-amber-200 bg-amber-50 p-2">
|
||||||
|
<p className="text-[10px] text-amber-700">
|
||||||
|
엔티티 관계가 설정되지 않은 테이블입니다. FK 컬럼을 직접 입력하세요.
|
||||||
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">FK 컬럼 (저장 테이블)</Label>
|
<Label className="text-[10px]">FK 컬럼</Label>
|
||||||
<Input
|
<Input
|
||||||
value={config.foreignKeyColumn || ""}
|
value={config.foreignKeyColumn || ""}
|
||||||
onChange={(e) => updateConfig({ foreignKeyColumn: e.target.value })}
|
onChange={(e) => updateConfig({ foreignKeyColumn: e.target.value })}
|
||||||
placeholder="예: receiving_id"
|
placeholder="예: master_id"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px]">PK 컬럼 (마스터 테이블)</Label>
|
<Label className="text-[10px]">PK 컬럼</Label>
|
||||||
<Input
|
<Input
|
||||||
value={config.foreignKeySourceColumn || "id"}
|
value={config.foreignKeySourceColumn || "id"}
|
||||||
onChange={(e) => updateConfig({ foreignKeySourceColumn: e.target.value })}
|
onChange={(e) => updateConfig({ foreignKeySourceColumn: e.target.value })}
|
||||||
placeholder="예: id"
|
placeholder="id"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.mainTableName && config.foreignKeyColumn && (
|
|
||||||
<div className="text-[10px] text-orange-700">
|
|
||||||
<strong>저장 흐름:</strong> {config.mainTableName}.{config.foreignKeyColumn} →
|
|
||||||
{currentTableName}.{config.foreignKeySourceColumn || "id"} (자동 연결)
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!config.useCustomTable && (
|
|
||||||
<div className="text-[10px] text-muted-foreground">
|
|
||||||
현재: 화면 메인 테이블 ({currentTableName || "미설정"})에 저장
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -809,10 +1022,10 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 현재 테이블 컬럼 (입력용) */}
|
{/* 저장 테이블 컬럼 (입력용) */}
|
||||||
<div className="text-[10px] font-medium text-gray-600 mt-3 mb-1 flex items-center gap-1">
|
<div className="text-[10px] font-medium text-gray-600 mt-3 mb-1 flex items-center gap-1">
|
||||||
<Database className="h-3 w-3" />
|
<Database className="h-3 w-3" />
|
||||||
현재 테이블 ({currentTableName || "미선택"}) - 입력용
|
저장 테이블 ({targetTableForColumns || "미선택"}) - 입력용
|
||||||
</div>
|
</div>
|
||||||
{loadingColumns ? (
|
{loadingColumns ? (
|
||||||
<p className="text-muted-foreground py-2 text-xs">로딩 중...</p>
|
<p className="text-muted-foreground py-2 text-xs">로딩 중...</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue