리피터 케이블 설정 구현
This commit is contained in:
@@ -13,15 +13,238 @@ alwaysApply: false
|
||||
|
||||
## 목차
|
||||
|
||||
1. [엔티티 조인 컬럼 활용 (필수)](#1-엔티티-조인-컬럼-활용-필수)
|
||||
2. [폼 데이터 관리](#2-폼-데이터-관리)
|
||||
3. [다국어 지원](#3-다국어-지원)
|
||||
4. [컬럼 설정 패널 구현](#4-컬럼-설정-패널-구현)
|
||||
5. [체크리스트](#5-체크리스트)
|
||||
1. [컴포넌트별 테이블 설정 (핵심 원칙)](#1-컴포넌트별-테이블-설정-핵심-원칙)
|
||||
2. [엔티티 조인 컬럼 활용 (필수)](#2-엔티티-조인-컬럼-활용-필수)
|
||||
3. [폼 데이터 관리](#3-폼-데이터-관리)
|
||||
4. [다국어 지원](#4-다국어-지원)
|
||||
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)
|
||||
|
||||
@@ -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()` 호출하여 조인 컬럼 로드
|
||||
|
||||
Reference in New Issue
Block a user