엔티티 즉시저장기능 추가
This commit is contained in:
parent
d8329d31e4
commit
f7e3c1924c
|
|
@ -1751,7 +1751,7 @@ export class ScreenManagementService {
|
||||||
// 기타
|
// 기타
|
||||||
label: "text-display",
|
label: "text-display",
|
||||||
code: "select-basic",
|
code: "select-basic",
|
||||||
entity: "select-basic",
|
entity: "entity-search-input", // 엔티티는 entity-search-input 사용
|
||||||
category: "select-basic",
|
category: "select-basic",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,345 @@
|
||||||
|
# 즉시 저장(quickInsert) 버튼 액션 구현 계획서
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 목적
|
||||||
|
화면에서 entity 타입 선택박스로 데이터를 선택한 후, 버튼 클릭 시 특정 테이블에 즉시 INSERT하는 기능 구현
|
||||||
|
|
||||||
|
### 1.2 사용 사례
|
||||||
|
- **공정별 설비 관리**: 좌측에서 공정 선택 → 우측에서 설비 선택 → "설비 추가" 버튼 클릭 → `process_equipment` 테이블에 즉시 저장
|
||||||
|
|
||||||
|
### 1.3 화면 구성 예시
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ [entity 선택박스] [버튼: quickInsert] │
|
||||||
|
│ ┌─────────────────────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ MCT-01 - 머시닝센터 #1 ▼ │ │ + 설비 추가 │ │
|
||||||
|
│ └─────────────────────────────┘ └──────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 기술 설계
|
||||||
|
|
||||||
|
### 2.1 버튼 액션 타입 추가
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// types/screen-management.ts
|
||||||
|
type ButtonActionType =
|
||||||
|
| "save"
|
||||||
|
| "cancel"
|
||||||
|
| "delete"
|
||||||
|
| "edit"
|
||||||
|
| "add"
|
||||||
|
| "search"
|
||||||
|
| "reset"
|
||||||
|
| "submit"
|
||||||
|
| "close"
|
||||||
|
| "popup"
|
||||||
|
| "navigate"
|
||||||
|
| "custom"
|
||||||
|
| "quickInsert" // 🆕 즉시 저장
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 quickInsert 설정 구조
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface QuickInsertColumnMapping {
|
||||||
|
targetColumn: string; // 저장할 테이블의 컬럼명
|
||||||
|
sourceType: "component" | "leftPanel" | "fixed" | "currentUser";
|
||||||
|
|
||||||
|
// sourceType별 추가 설정
|
||||||
|
sourceComponentId?: string; // component: 값을 가져올 컴포넌트 ID
|
||||||
|
sourceColumn?: string; // leftPanel: 좌측 선택 데이터의 컬럼명
|
||||||
|
fixedValue?: any; // fixed: 고정값
|
||||||
|
userField?: string; // currentUser: 사용자 정보 필드 (userId, userName, companyCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuickInsertConfig {
|
||||||
|
targetTable: string; // 저장할 테이블명
|
||||||
|
columnMappings: QuickInsertColumnMapping[];
|
||||||
|
|
||||||
|
// 저장 후 동작
|
||||||
|
afterInsert?: {
|
||||||
|
refreshRightPanel?: boolean; // 우측 패널 새로고침
|
||||||
|
clearComponents?: string[]; // 초기화할 컴포넌트 ID 목록
|
||||||
|
showSuccessMessage?: boolean; // 성공 메시지 표시
|
||||||
|
successMessage?: string; // 커스텀 성공 메시지
|
||||||
|
};
|
||||||
|
|
||||||
|
// 중복 체크 (선택사항)
|
||||||
|
duplicateCheck?: {
|
||||||
|
enabled: boolean;
|
||||||
|
columns: string[]; // 중복 체크할 컬럼들
|
||||||
|
errorMessage?: string; // 중복 시 에러 메시지
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonComponentConfig {
|
||||||
|
// 기존 설정들...
|
||||||
|
actionType: ButtonActionType;
|
||||||
|
|
||||||
|
// 🆕 quickInsert 전용 설정
|
||||||
|
quickInsertConfig?: QuickInsertConfig;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 데이터 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 사용자가 entity 선택박스에서 설비 선택
|
||||||
|
└─ equipment_code = "EQ-001" (내부값)
|
||||||
|
└─ 표시: "MCT-01 - 머시닝센터 #1"
|
||||||
|
|
||||||
|
2. 사용자가 "설비 추가" 버튼 클릭
|
||||||
|
|
||||||
|
3. quickInsert 핸들러 실행
|
||||||
|
├─ columnMappings 순회
|
||||||
|
│ ├─ equipment_code: component에서 값 가져오기 → "EQ-001"
|
||||||
|
│ └─ process_code: leftPanel에서 값 가져오기 → "PRC-001"
|
||||||
|
│
|
||||||
|
└─ INSERT 데이터 구성
|
||||||
|
{
|
||||||
|
equipment_code: "EQ-001",
|
||||||
|
process_code: "PRC-001",
|
||||||
|
company_code: "COMPANY_7", // 자동 추가
|
||||||
|
writer: "wace" // 자동 추가
|
||||||
|
}
|
||||||
|
|
||||||
|
4. API 호출: POST /api/table-management/tables/process_equipment/add
|
||||||
|
|
||||||
|
5. 성공 시
|
||||||
|
├─ 성공 메시지 표시
|
||||||
|
├─ 우측 패널(카드/테이블) 새로고침
|
||||||
|
└─ 선택박스 초기화
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 구현 계획
|
||||||
|
|
||||||
|
### 3.1 Phase 1: 타입 정의 및 설정 UI
|
||||||
|
|
||||||
|
| 작업 | 파일 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1-1 | `frontend/types/screen-management.ts` | QuickInsertConfig 타입 추가 |
|
||||||
|
| 1-2 | `frontend/components/screen/config-panels/ButtonConfigPanel.tsx` | quickInsert 설정 UI 추가 |
|
||||||
|
|
||||||
|
### 3.2 Phase 2: 버튼 액션 핸들러 구현
|
||||||
|
|
||||||
|
| 작업 | 파일 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| 2-1 | `frontend/components/screen/InteractiveScreenViewerDynamic.tsx` | quickInsert 핸들러 추가 |
|
||||||
|
| 2-2 | 컴포넌트 값 수집 로직 | 같은 화면의 다른 컴포넌트에서 값 가져오기 |
|
||||||
|
|
||||||
|
### 3.3 Phase 3: 테스트 및 검증
|
||||||
|
|
||||||
|
| 작업 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 3-1 | 공정별 설비 화면에서 테스트 |
|
||||||
|
| 3-2 | 중복 저장 방지 테스트 |
|
||||||
|
| 3-3 | 에러 처리 테스트 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 상세 구현
|
||||||
|
|
||||||
|
### 4.1 ButtonConfigPanel 설정 UI
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 버튼 액션 타입 │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 즉시 저장 (quickInsert) ▼ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ─────────────── 즉시 저장 설정 ─────────────── │
|
||||||
|
│ │
|
||||||
|
│ 대상 테이블 * │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ process_equipment ▼ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 컬럼 매핑 [+ 추가] │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 매핑 #1 [삭제] │ │
|
||||||
|
│ │ 대상 컬럼: equipment_code │ │
|
||||||
|
│ │ 값 소스: 컴포넌트 선택 │ │
|
||||||
|
│ │ 컴포넌트: [equipment-select ▼] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 매핑 #2 [삭제] │ │
|
||||||
|
│ │ 대상 컬럼: process_code │ │
|
||||||
|
│ │ 값 소스: 좌측 패널 데이터 │ │
|
||||||
|
│ │ 소스 컬럼: process_code │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ─────────────── 저장 후 동작 ─────────────── │
|
||||||
|
│ │
|
||||||
|
│ ☑ 우측 패널 새로고침 │
|
||||||
|
│ ☑ 선택박스 초기화 │
|
||||||
|
│ ☑ 성공 메시지 표시 │
|
||||||
|
│ │
|
||||||
|
│ ─────────────── 중복 체크 (선택) ─────────────── │
|
||||||
|
│ │
|
||||||
|
│ ☐ 중복 체크 활성화 │
|
||||||
|
│ 체크 컬럼: equipment_code, process_code │
|
||||||
|
│ 에러 메시지: 이미 등록된 설비입니다. │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 핸들러 구현 (의사 코드)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleQuickInsert = async (config: QuickInsertConfig) => {
|
||||||
|
// 1. 컬럼 매핑에서 값 수집
|
||||||
|
const insertData: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const mapping of config.columnMappings) {
|
||||||
|
let value: any;
|
||||||
|
|
||||||
|
switch (mapping.sourceType) {
|
||||||
|
case "component":
|
||||||
|
// 같은 화면의 컴포넌트에서 값 가져오기
|
||||||
|
value = getComponentValue(mapping.sourceComponentId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "leftPanel":
|
||||||
|
// 분할 패널 좌측 선택 데이터에서 값 가져오기
|
||||||
|
value = splitPanelContext?.selectedLeftData?.[mapping.sourceColumn];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "fixed":
|
||||||
|
value = mapping.fixedValue;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "currentUser":
|
||||||
|
value = user?.[mapping.userField];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
insertData[mapping.targetColumn] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 필수값 검증
|
||||||
|
if (Object.keys(insertData).length === 0) {
|
||||||
|
toast.error("저장할 데이터가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 중복 체크 (설정된 경우)
|
||||||
|
if (config.duplicateCheck?.enabled) {
|
||||||
|
const isDuplicate = await checkDuplicate(
|
||||||
|
config.targetTable,
|
||||||
|
config.duplicateCheck.columns,
|
||||||
|
insertData
|
||||||
|
);
|
||||||
|
if (isDuplicate) {
|
||||||
|
toast.error(config.duplicateCheck.errorMessage || "이미 존재하는 데이터입니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. API 호출
|
||||||
|
try {
|
||||||
|
await tableTypeApi.addTableData(config.targetTable, insertData);
|
||||||
|
|
||||||
|
// 5. 성공 후 동작
|
||||||
|
if (config.afterInsert?.showSuccessMessage) {
|
||||||
|
toast.success(config.afterInsert.successMessage || "저장되었습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.afterInsert?.refreshRightPanel) {
|
||||||
|
// 우측 패널 새로고침 트리거
|
||||||
|
onRefresh?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.afterInsert?.clearComponents) {
|
||||||
|
// 지정된 컴포넌트 초기화
|
||||||
|
for (const componentId of config.afterInsert.clearComponents) {
|
||||||
|
clearComponentValue(componentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("저장에 실패했습니다.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 컴포넌트 간 통신 방안
|
||||||
|
|
||||||
|
### 5.1 문제점
|
||||||
|
- 버튼 컴포넌트에서 같은 화면의 entity 선택박스 값을 가져와야 함
|
||||||
|
- 현재는 각 컴포넌트가 독립적으로 동작
|
||||||
|
|
||||||
|
### 5.2 해결 방안: formData 활용
|
||||||
|
|
||||||
|
현재 `InteractiveScreenViewerDynamic`에서 `formData` 상태로 모든 입력값을 관리하고 있음.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// InteractiveScreenViewerDynamic.tsx
|
||||||
|
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
|
// entity 선택박스에서 값 변경 시
|
||||||
|
const handleFormDataChange = (fieldName: string, value: any) => {
|
||||||
|
setLocalFormData(prev => ({ ...prev, [fieldName]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 버튼 클릭 시 formData에서 값 가져오기
|
||||||
|
const getComponentValue = (componentId: string) => {
|
||||||
|
// componentId로 컴포넌트의 columnName 찾기
|
||||||
|
const component = allComponents.find(c => c.id === componentId);
|
||||||
|
if (component?.columnName) {
|
||||||
|
return formData[component.columnName];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 테스트 시나리오
|
||||||
|
|
||||||
|
### 6.1 정상 케이스
|
||||||
|
1. 좌측 테이블에서 공정 "PRC-001" 선택
|
||||||
|
2. 우측 설비 선택박스에서 "MCT-01" 선택
|
||||||
|
3. "설비 추가" 버튼 클릭
|
||||||
|
4. `process_equipment` 테이블에 데이터 저장 확인
|
||||||
|
5. 우측 카드/테이블에 새 항목 표시 확인
|
||||||
|
|
||||||
|
### 6.2 에러 케이스
|
||||||
|
1. 좌측 미선택 상태에서 버튼 클릭 → "좌측에서 항목을 선택해주세요" 메시지
|
||||||
|
2. 설비 미선택 상태에서 버튼 클릭 → "설비를 선택해주세요" 메시지
|
||||||
|
3. 중복 데이터 저장 시도 → "이미 등록된 설비입니다" 메시지
|
||||||
|
|
||||||
|
### 6.3 엣지 케이스
|
||||||
|
1. 동일 설비 연속 추가 시도
|
||||||
|
2. 네트워크 오류 시 재시도
|
||||||
|
3. 권한 없는 사용자의 저장 시도
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 일정
|
||||||
|
|
||||||
|
| Phase | 작업 | 예상 시간 |
|
||||||
|
|-------|------|----------|
|
||||||
|
| Phase 1 | 타입 정의 및 설정 UI | 1시간 |
|
||||||
|
| Phase 2 | 버튼 액션 핸들러 구현 | 1시간 |
|
||||||
|
| Phase 3 | 테스트 및 검증 | 30분 |
|
||||||
|
| **합계** | | **2시간 30분** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 향후 확장 가능성
|
||||||
|
|
||||||
|
1. **다중 행 추가**: 여러 설비를 한 번에 선택하여 추가
|
||||||
|
2. **수정 모드**: 기존 데이터 수정 기능
|
||||||
|
3. **조건부 저장**: 특정 조건 만족 시에만 저장
|
||||||
|
4. **연쇄 저장**: 한 번의 클릭으로 여러 테이블에 저장
|
||||||
|
|
||||||
|
|
@ -584,6 +584,219 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 즉시 저장(quickInsert) 액션 핸들러
|
||||||
|
const handleQuickInsertAction = async () => {
|
||||||
|
// componentConfig에서 quickInsertConfig 가져오기
|
||||||
|
const quickInsertConfig = (comp as any).componentConfig?.action?.quickInsertConfig;
|
||||||
|
|
||||||
|
if (!quickInsertConfig?.targetTable) {
|
||||||
|
toast.error("대상 테이블이 설정되지 않았습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 대상 테이블의 컬럼 목록 조회 (자동 매핑용)
|
||||||
|
let targetTableColumns: string[] = [];
|
||||||
|
try {
|
||||||
|
const { default: apiClient } = await import("@/lib/api/client");
|
||||||
|
const columnsResponse = await apiClient.get(
|
||||||
|
`/table-management/tables/${quickInsertConfig.targetTable}/columns`
|
||||||
|
);
|
||||||
|
if (columnsResponse.data?.success && columnsResponse.data?.data) {
|
||||||
|
const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data;
|
||||||
|
targetTableColumns = columnsData.map((col: any) => col.columnName || col.column_name || col.name);
|
||||||
|
console.log("📍 대상 테이블 컬럼 목록:", targetTableColumns);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("대상 테이블 컬럼 조회 실패:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 컬럼 매핑에서 값 수집
|
||||||
|
const insertData: Record<string, any> = {};
|
||||||
|
const columnMappings = quickInsertConfig.columnMappings || [];
|
||||||
|
|
||||||
|
for (const mapping of columnMappings) {
|
||||||
|
let value: any;
|
||||||
|
|
||||||
|
switch (mapping.sourceType) {
|
||||||
|
case "component":
|
||||||
|
// 같은 화면의 컴포넌트에서 값 가져오기
|
||||||
|
// 방법1: sourceColumnName 사용
|
||||||
|
if (mapping.sourceColumnName && formData[mapping.sourceColumnName] !== undefined) {
|
||||||
|
value = formData[mapping.sourceColumnName];
|
||||||
|
console.log(`📍 컴포넌트 값 (sourceColumnName): ${mapping.sourceColumnName} = ${value}`);
|
||||||
|
}
|
||||||
|
// 방법2: sourceComponentId로 컴포넌트 찾아서 columnName 사용
|
||||||
|
else if (mapping.sourceComponentId) {
|
||||||
|
const sourceComp = allComponents.find((c: any) => c.id === mapping.sourceComponentId);
|
||||||
|
if (sourceComp) {
|
||||||
|
const fieldName = (sourceComp as any).columnName || sourceComp.id;
|
||||||
|
value = formData[fieldName];
|
||||||
|
console.log(`📍 컴포넌트 값 (컴포넌트 조회): ${fieldName} = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "leftPanel":
|
||||||
|
// 분할 패널 좌측 선택 데이터에서 값 가져오기
|
||||||
|
if (mapping.sourceColumn && splitPanelContext?.selectedLeftData) {
|
||||||
|
value = splitPanelContext.selectedLeftData[mapping.sourceColumn];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "fixed":
|
||||||
|
value = mapping.fixedValue;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "currentUser":
|
||||||
|
if (mapping.userField) {
|
||||||
|
switch (mapping.userField) {
|
||||||
|
case "userId":
|
||||||
|
value = user?.userId;
|
||||||
|
break;
|
||||||
|
case "userName":
|
||||||
|
value = userName;
|
||||||
|
break;
|
||||||
|
case "companyCode":
|
||||||
|
value = user?.companyCode;
|
||||||
|
break;
|
||||||
|
case "deptCode":
|
||||||
|
value = authUser?.deptCode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
insertData[mapping.targetColumn] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 좌측 패널 선택 데이터에서 자동 매핑 (컬럼명이 같고 대상 테이블에 있는 경우)
|
||||||
|
if (splitPanelContext?.selectedLeftData && targetTableColumns.length > 0) {
|
||||||
|
const leftData = splitPanelContext.selectedLeftData;
|
||||||
|
console.log("📍 좌측 패널 자동 매핑 시작:", leftData);
|
||||||
|
|
||||||
|
for (const [key, val] of Object.entries(leftData)) {
|
||||||
|
// 이미 매핑된 컬럼은 스킵
|
||||||
|
if (insertData[key] !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대상 테이블에 해당 컬럼이 없으면 스킵
|
||||||
|
if (!targetTableColumns.includes(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시스템 컬럼 제외
|
||||||
|
const systemColumns = ['id', 'created_date', 'updated_date', 'writer', 'writer_name'];
|
||||||
|
if (systemColumns.includes(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// _label, _name 으로 끝나는 표시용 컬럼 제외
|
||||||
|
if (key.endsWith('_label') || key.endsWith('_name')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 값이 있으면 자동 추가
|
||||||
|
if (val !== undefined && val !== null && val !== '') {
|
||||||
|
insertData[key] = val;
|
||||||
|
console.log(`📍 자동 매핑 추가: ${key} = ${val}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🚀 quickInsert 최종 데이터:", insertData);
|
||||||
|
|
||||||
|
// 4. 필수값 검증
|
||||||
|
if (Object.keys(insertData).length === 0) {
|
||||||
|
toast.error("저장할 데이터가 없습니다. 값을 선택해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 중복 체크 (설정된 경우)
|
||||||
|
if (quickInsertConfig.duplicateCheck?.enabled && quickInsertConfig.duplicateCheck?.columns?.length > 0) {
|
||||||
|
try {
|
||||||
|
const { default: apiClient } = await import("@/lib/api/client");
|
||||||
|
|
||||||
|
// 중복 체크를 위한 검색 조건 구성
|
||||||
|
const searchConditions: Record<string, any> = {};
|
||||||
|
for (const col of quickInsertConfig.duplicateCheck.columns) {
|
||||||
|
if (insertData[col] !== undefined) {
|
||||||
|
searchConditions[col] = { value: insertData[col], operator: "equals" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📍 중복 체크 조건:", searchConditions);
|
||||||
|
|
||||||
|
// 기존 데이터 조회
|
||||||
|
const checkResponse = await apiClient.post(
|
||||||
|
`/table-management/tables/${quickInsertConfig.targetTable}/data`,
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
search: searchConditions,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("📍 중복 체크 응답:", checkResponse.data);
|
||||||
|
|
||||||
|
// data 배열이 있고 길이가 0보다 크면 중복
|
||||||
|
const existingData = checkResponse.data?.data?.data || checkResponse.data?.data || [];
|
||||||
|
if (Array.isArray(existingData) && existingData.length > 0) {
|
||||||
|
toast.error(quickInsertConfig.duplicateCheck.errorMessage || "이미 존재하는 데이터입니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("중복 체크 오류:", error);
|
||||||
|
// 중복 체크 실패 시 계속 진행
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. API 호출
|
||||||
|
try {
|
||||||
|
const { default: apiClient } = await import("@/lib/api/client");
|
||||||
|
|
||||||
|
const response = await apiClient.post(
|
||||||
|
`/table-management/tables/${quickInsertConfig.targetTable}/add`,
|
||||||
|
insertData
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
// 7. 성공 후 동작
|
||||||
|
if (quickInsertConfig.afterInsert?.showSuccessMessage !== false) {
|
||||||
|
toast.success(quickInsertConfig.afterInsert?.successMessage || "저장되었습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터 새로고침 (테이블리스트, 카드 디스플레이)
|
||||||
|
if (quickInsertConfig.afterInsert?.refreshData !== false) {
|
||||||
|
console.log("📍 데이터 새로고침 이벤트 발송");
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||||
|
window.dispatchEvent(new CustomEvent("refreshCardDisplay"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 지정된 컴포넌트 초기화
|
||||||
|
if (quickInsertConfig.afterInsert?.clearComponents?.length > 0) {
|
||||||
|
for (const componentId of quickInsertConfig.afterInsert.clearComponents) {
|
||||||
|
const targetComp = allComponents.find((c: any) => c.id === componentId);
|
||||||
|
if (targetComp) {
|
||||||
|
const fieldName = (targetComp as any).columnName || targetComp.id;
|
||||||
|
onFormDataChange?.(fieldName, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(response.data?.message || "저장에 실패했습니다.");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("quickInsert 오류:", error);
|
||||||
|
toast.error(error.response?.data?.message || error.message || "저장 중 오류가 발생했습니다.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
try {
|
try {
|
||||||
const actionType = config?.actionType || "save";
|
const actionType = config?.actionType || "save";
|
||||||
|
|
@ -604,6 +817,9 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
case "custom":
|
case "custom":
|
||||||
await handleCustomAction();
|
await handleCustomAction();
|
||||||
break;
|
break;
|
||||||
|
case "quickInsert":
|
||||||
|
await handleQuickInsertAction();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// console.log("🔘 기본 버튼 클릭");
|
// console.log("🔘 기본 버튼 클릭");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { apiClient } from "@/lib/api/client";
|
||||||
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
|
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
|
||||||
import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel";
|
import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel";
|
||||||
import { FlowVisibilityConfigPanel } from "./FlowVisibilityConfigPanel";
|
import { FlowVisibilityConfigPanel } from "./FlowVisibilityConfigPanel";
|
||||||
|
import { QuickInsertConfigSection } from "./QuickInsertConfigSection";
|
||||||
|
|
||||||
// 🆕 제목 블록 타입
|
// 🆕 제목 블록 타입
|
||||||
interface TitleBlock {
|
interface TitleBlock {
|
||||||
|
|
@ -642,9 +643,10 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
<SelectItem value="edit">편집</SelectItem>
|
<SelectItem value="edit">편집</SelectItem>
|
||||||
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
||||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||||
<SelectItem value="transferData">📦 데이터 전달</SelectItem>
|
<SelectItem value="transferData">데이터 전달</SelectItem>
|
||||||
<SelectItem value="openModalWithData">데이터 전달 + 모달 열기 🆕</SelectItem>
|
<SelectItem value="openModalWithData">데이터 전달 + 모달 열기</SelectItem>
|
||||||
<SelectItem value="modal">모달 열기</SelectItem>
|
<SelectItem value="modal">모달 열기</SelectItem>
|
||||||
|
<SelectItem value="quickInsert">즉시 저장</SelectItem>
|
||||||
<SelectItem value="control">제어 흐름</SelectItem>
|
<SelectItem value="control">제어 흐름</SelectItem>
|
||||||
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
||||||
<SelectItem value="excel_download">엑셀 다운로드</SelectItem>
|
<SelectItem value="excel_download">엑셀 다운로드</SelectItem>
|
||||||
|
|
@ -3068,6 +3070,16 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 🆕 즉시 저장(quickInsert) 액션 설정 */}
|
||||||
|
{component.componentConfig?.action?.type === "quickInsert" && (
|
||||||
|
<QuickInsertConfigSection
|
||||||
|
component={component}
|
||||||
|
onUpdateProperty={onUpdateProperty}
|
||||||
|
allComponents={allComponents}
|
||||||
|
currentTableName={currentTableName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 제어 기능 섹션 */}
|
{/* 제어 기능 섹션 */}
|
||||||
<div className="mt-8 border-t border-border pt-6">
|
<div className="mt-8 border-t border-border pt-6">
|
||||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,33 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-medium">기본 설정</h4>
|
<h4 className="text-sm font-medium">기본 설정</h4>
|
||||||
|
|
||||||
|
{/* UI 모드 선택 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="uiMode" className="text-xs">
|
||||||
|
UI 모드
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={(localConfig as any).uiMode || "combo"}
|
||||||
|
onValueChange={(value) => updateConfig("uiMode" as any, value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="text-xs">
|
||||||
|
<SelectValue placeholder="모드 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="select">드롭다운 (Select)</SelectItem>
|
||||||
|
<SelectItem value="modal">모달 팝업 (Modal)</SelectItem>
|
||||||
|
<SelectItem value="combo">입력 + 모달 버튼 (Combo)</SelectItem>
|
||||||
|
<SelectItem value="autocomplete">자동완성 (Autocomplete)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
{(localConfig as any).uiMode === "select" && "검색 가능한 드롭다운 형태로 표시됩니다."}
|
||||||
|
{(localConfig as any).uiMode === "modal" && "모달 팝업에서 데이터를 검색하고 선택합니다."}
|
||||||
|
{((localConfig as any).uiMode === "combo" || !(localConfig as any).uiMode) && "입력 필드와 검색 버튼이 함께 표시됩니다."}
|
||||||
|
{(localConfig as any).uiMode === "autocomplete" && "입력하면서 자동완성 목록이 표시됩니다."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="entityType" className="text-xs">
|
<Label htmlFor="entityType" className="text-xs">
|
||||||
엔티티 타입
|
엔티티 타입
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,658 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { Check, ChevronsUpDown, Plus, X, Search } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ComponentData } from "@/types/screen";
|
||||||
|
import { QuickInsertConfig, QuickInsertColumnMapping } from "@/types/screen-management";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
|
interface QuickInsertConfigSectionProps {
|
||||||
|
component: ComponentData;
|
||||||
|
onUpdateProperty: (path: string, value: any) => void;
|
||||||
|
allComponents?: ComponentData[];
|
||||||
|
currentTableName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableOption {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColumnOption {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuickInsertConfigSection: React.FC<QuickInsertConfigSectionProps> = ({
|
||||||
|
component,
|
||||||
|
onUpdateProperty,
|
||||||
|
allComponents = [],
|
||||||
|
currentTableName,
|
||||||
|
}) => {
|
||||||
|
// 현재 설정 가져오기
|
||||||
|
const config: QuickInsertConfig = component.componentConfig?.action?.quickInsertConfig || {
|
||||||
|
targetTable: "",
|
||||||
|
columnMappings: [],
|
||||||
|
afterInsert: {
|
||||||
|
refreshData: true,
|
||||||
|
clearComponents: [],
|
||||||
|
showSuccessMessage: true,
|
||||||
|
successMessage: "저장되었습니다.",
|
||||||
|
},
|
||||||
|
duplicateCheck: {
|
||||||
|
enabled: false,
|
||||||
|
columns: [],
|
||||||
|
errorMessage: "이미 존재하는 데이터입니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 테이블 목록 상태
|
||||||
|
const [tables, setTables] = useState<TableOption[]>([]);
|
||||||
|
const [tablesLoading, setTablesLoading] = useState(false);
|
||||||
|
const [tablePopoverOpen, setTablePopoverOpen] = useState(false);
|
||||||
|
const [tableSearch, setTableSearch] = useState("");
|
||||||
|
|
||||||
|
// 대상 테이블 컬럼 목록 상태
|
||||||
|
const [targetColumns, setTargetColumns] = useState<ColumnOption[]>([]);
|
||||||
|
const [targetColumnsLoading, setTargetColumnsLoading] = useState(false);
|
||||||
|
|
||||||
|
// 매핑별 Popover 상태
|
||||||
|
const [targetColumnPopoverOpen, setTargetColumnPopoverOpen] = useState<Record<number, boolean>>({});
|
||||||
|
const [targetColumnSearch, setTargetColumnSearch] = useState<Record<number, string>>({});
|
||||||
|
const [sourceComponentPopoverOpen, setSourceComponentPopoverOpen] = useState<Record<number, boolean>>({});
|
||||||
|
const [sourceComponentSearch, setSourceComponentSearch] = useState<Record<number, string>>({});
|
||||||
|
|
||||||
|
// 테이블 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTables = async () => {
|
||||||
|
setTablesLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get("/table-management/tables");
|
||||||
|
if (response.data?.success && response.data?.data) {
|
||||||
|
setTables(
|
||||||
|
response.data.data.map((t: any) => ({
|
||||||
|
name: t.tableName,
|
||||||
|
label: t.displayName || t.tableName,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("테이블 목록 로드 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setTablesLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadTables();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 대상 테이블 선택 시 컬럼 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTargetColumns = async () => {
|
||||||
|
if (!config.targetTable) {
|
||||||
|
setTargetColumns([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTargetColumnsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/table-management/tables/${config.targetTable}/columns`);
|
||||||
|
if (response.data?.success && response.data?.data) {
|
||||||
|
// columns가 배열인지 확인 (data.columns 또는 data 직접)
|
||||||
|
const columns = response.data.data.columns || response.data.data;
|
||||||
|
setTargetColumns(
|
||||||
|
(Array.isArray(columns) ? columns : []).map((col: any) => ({
|
||||||
|
name: col.columnName || col.column_name,
|
||||||
|
label: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("컬럼 목록 로드 실패:", error);
|
||||||
|
setTargetColumns([]);
|
||||||
|
} finally {
|
||||||
|
setTargetColumnsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadTargetColumns();
|
||||||
|
}, [config.targetTable]);
|
||||||
|
|
||||||
|
// 설정 업데이트 헬퍼
|
||||||
|
const updateConfig = useCallback(
|
||||||
|
(updates: Partial<QuickInsertConfig>) => {
|
||||||
|
const newConfig = { ...config, ...updates };
|
||||||
|
onUpdateProperty("componentConfig.action.quickInsertConfig", newConfig);
|
||||||
|
},
|
||||||
|
[config, onUpdateProperty]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 컬럼 매핑 추가
|
||||||
|
const addMapping = () => {
|
||||||
|
const newMapping: QuickInsertColumnMapping = {
|
||||||
|
targetColumn: "",
|
||||||
|
sourceType: "component",
|
||||||
|
sourceComponentId: "",
|
||||||
|
};
|
||||||
|
updateConfig({
|
||||||
|
columnMappings: [...(config.columnMappings || []), newMapping],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컬럼 매핑 삭제
|
||||||
|
const removeMapping = (index: number) => {
|
||||||
|
const newMappings = [...(config.columnMappings || [])];
|
||||||
|
newMappings.splice(index, 1);
|
||||||
|
updateConfig({ columnMappings: newMappings });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컬럼 매핑 업데이트
|
||||||
|
const updateMapping = (index: number, updates: Partial<QuickInsertColumnMapping>) => {
|
||||||
|
const newMappings = [...(config.columnMappings || [])];
|
||||||
|
newMappings[index] = { ...newMappings[index], ...updates };
|
||||||
|
updateConfig({ columnMappings: newMappings });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필터링된 테이블 목록
|
||||||
|
const filteredTables = tables.filter(
|
||||||
|
(t) =>
|
||||||
|
t.name.toLowerCase().includes(tableSearch.toLowerCase()) ||
|
||||||
|
t.label.toLowerCase().includes(tableSearch.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
// 컴포넌트 목록 (entity 타입 우선)
|
||||||
|
const availableComponents = allComponents.filter((comp: any) => {
|
||||||
|
// entity 타입 또는 select 타입 컴포넌트 필터링
|
||||||
|
const widgetType = comp.widgetType || comp.componentType || "";
|
||||||
|
return widgetType === "entity" || widgetType === "select" || widgetType === "text";
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 space-y-4 rounded-lg border bg-green-50 p-4 dark:bg-green-950/20">
|
||||||
|
<h4 className="text-sm font-medium text-foreground">즉시 저장 설정</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
화면에서 선택한 데이터를 버튼 클릭 시 특정 테이블에 즉시 저장합니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 대상 테이블 선택 */}
|
||||||
|
<div>
|
||||||
|
<Label>대상 테이블 *</Label>
|
||||||
|
<Popover open={tablePopoverOpen} onOpenChange={setTablePopoverOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={tablePopoverOpen}
|
||||||
|
className="h-8 w-full justify-between text-xs"
|
||||||
|
disabled={tablesLoading}
|
||||||
|
>
|
||||||
|
{config.targetTable
|
||||||
|
? tables.find((t) => t.name === config.targetTable)?.label || config.targetTable
|
||||||
|
: "테이블을 선택하세요..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="테이블 검색..."
|
||||||
|
value={tableSearch}
|
||||||
|
onValueChange={setTableSearch}
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{filteredTables.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.name}
|
||||||
|
value={`${table.label} ${table.name}`}
|
||||||
|
onSelect={() => {
|
||||||
|
updateConfig({ targetTable: table.name, columnMappings: [] });
|
||||||
|
setTablePopoverOpen(false);
|
||||||
|
setTableSearch("");
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn("mr-2 h-4 w-4", config.targetTable === table.name ? "opacity-100" : "opacity-0")}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{table.label}</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">{table.name}</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 컬럼 매핑 */}
|
||||||
|
{config.targetTable && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>컬럼 매핑</Label>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={addMapping} className="h-6 text-xs">
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
매핑 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(config.columnMappings || []).length === 0 ? (
|
||||||
|
<div className="rounded border-2 border-dashed py-4 text-center text-xs text-muted-foreground">
|
||||||
|
컬럼 매핑을 추가하세요
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(config.columnMappings || []).map((mapping, index) => (
|
||||||
|
<Card key={index} className="p-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-medium">매핑 #{index + 1}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeMapping(index)}
|
||||||
|
className="h-5 w-5 p-0 text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 대상 컬럼 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">대상 컬럼 (저장할 컬럼)</Label>
|
||||||
|
<Popover
|
||||||
|
open={targetColumnPopoverOpen[index] || false}
|
||||||
|
onOpenChange={(open) => setTargetColumnPopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className="h-7 w-full justify-between text-xs"
|
||||||
|
disabled={targetColumnsLoading}
|
||||||
|
>
|
||||||
|
{mapping.targetColumn
|
||||||
|
? targetColumns.find((c) => c.name === mapping.targetColumn)?.label || mapping.targetColumn
|
||||||
|
: "컬럼 선택..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="컬럼 검색..."
|
||||||
|
value={targetColumnSearch[index] || ""}
|
||||||
|
onValueChange={(v) => setTargetColumnSearch((prev) => ({ ...prev, [index]: v }))}
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{targetColumns
|
||||||
|
.filter(
|
||||||
|
(c) =>
|
||||||
|
c.name.toLowerCase().includes((targetColumnSearch[index] || "").toLowerCase()) ||
|
||||||
|
c.label.toLowerCase().includes((targetColumnSearch[index] || "").toLowerCase())
|
||||||
|
)
|
||||||
|
.map((col) => (
|
||||||
|
<CommandItem
|
||||||
|
key={col.name}
|
||||||
|
value={`${col.label} ${col.name}`}
|
||||||
|
onSelect={() => {
|
||||||
|
updateMapping(index, { targetColumn: col.name });
|
||||||
|
setTargetColumnPopoverOpen((prev) => ({ ...prev, [index]: false }));
|
||||||
|
setTargetColumnSearch((prev) => ({ ...prev, [index]: "" }));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
mapping.targetColumn === col.name ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>{col.label}</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">{col.name}</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 값 소스 타입 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">값 소스</Label>
|
||||||
|
<Select
|
||||||
|
value={mapping.sourceType}
|
||||||
|
onValueChange={(value: "component" | "leftPanel" | "fixed" | "currentUser") => {
|
||||||
|
updateMapping(index, {
|
||||||
|
sourceType: value,
|
||||||
|
sourceComponentId: undefined,
|
||||||
|
sourceColumn: undefined,
|
||||||
|
fixedValue: undefined,
|
||||||
|
userField: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="component" className="text-xs">
|
||||||
|
컴포넌트 선택값
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="leftPanel" className="text-xs">
|
||||||
|
좌측 패널 선택 데이터
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="fixed" className="text-xs">
|
||||||
|
고정값
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="currentUser" className="text-xs">
|
||||||
|
현재 사용자 정보
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 소스 타입별 추가 설정 */}
|
||||||
|
{mapping.sourceType === "component" && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">소스 컴포넌트</Label>
|
||||||
|
<Popover
|
||||||
|
open={sourceComponentPopoverOpen[index] || false}
|
||||||
|
onOpenChange={(open) => setSourceComponentPopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" role="combobox" className="h-7 w-full justify-between text-xs">
|
||||||
|
{mapping.sourceComponentId
|
||||||
|
? (() => {
|
||||||
|
const comp = allComponents.find((c: any) => c.id === mapping.sourceComponentId);
|
||||||
|
return comp?.label || comp?.columnName || mapping.sourceComponentId;
|
||||||
|
})()
|
||||||
|
: "컴포넌트 선택..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="컴포넌트 검색..."
|
||||||
|
value={sourceComponentSearch[index] || ""}
|
||||||
|
onValueChange={(v) => setSourceComponentSearch((prev) => ({ ...prev, [index]: v }))}
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="text-xs">컴포넌트를 찾을 수 없습니다.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{availableComponents
|
||||||
|
.filter((comp: any) => {
|
||||||
|
const search = (sourceComponentSearch[index] || "").toLowerCase();
|
||||||
|
const label = (comp.label || "").toLowerCase();
|
||||||
|
const colName = (comp.columnName || "").toLowerCase();
|
||||||
|
return label.includes(search) || colName.includes(search);
|
||||||
|
})
|
||||||
|
.map((comp: any) => (
|
||||||
|
<CommandItem
|
||||||
|
key={comp.id}
|
||||||
|
value={comp.id}
|
||||||
|
onSelect={() => {
|
||||||
|
// sourceComponentId와 함께 sourceColumnName도 저장 (formData 접근용)
|
||||||
|
updateMapping(index, {
|
||||||
|
sourceComponentId: comp.id,
|
||||||
|
sourceColumnName: comp.columnName || undefined,
|
||||||
|
});
|
||||||
|
setSourceComponentPopoverOpen((prev) => ({ ...prev, [index]: false }));
|
||||||
|
setSourceComponentSearch((prev) => ({ ...prev, [index]: "" }));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
mapping.sourceComponentId === comp.id ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>{comp.label || comp.columnName || comp.id}</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{comp.widgetType || comp.componentType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mapping.sourceType === "leftPanel" && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">좌측 패널 컬럼명</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="예: process_code"
|
||||||
|
value={mapping.sourceColumn || ""}
|
||||||
|
onChange={(e) => updateMapping(index, { sourceColumn: e.target.value })}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||||
|
분할 패널 좌측에서 선택된 데이터의 컬럼명을 입력하세요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mapping.sourceType === "fixed" && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">고정값</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="고정값 입력"
|
||||||
|
value={mapping.fixedValue || ""}
|
||||||
|
onChange={(e) => updateMapping(index, { fixedValue: e.target.value })}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mapping.sourceType === "currentUser" && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">사용자 정보 필드</Label>
|
||||||
|
<Select
|
||||||
|
value={mapping.userField || ""}
|
||||||
|
onValueChange={(value: "userId" | "userName" | "companyCode" | "deptCode") => {
|
||||||
|
updateMapping(index, { userField: value });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
<SelectValue placeholder="필드 선택..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="userId" className="text-xs">
|
||||||
|
사용자 ID
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="userName" className="text-xs">
|
||||||
|
사용자 이름
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="companyCode" className="text-xs">
|
||||||
|
회사 코드
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="deptCode" className="text-xs">
|
||||||
|
부서 코드
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 저장 후 동작 설정 */}
|
||||||
|
{config.targetTable && (
|
||||||
|
<div className="space-y-3 rounded border bg-background p-3">
|
||||||
|
<Label className="text-xs font-medium">저장 후 동작</Label>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs font-normal">데이터 새로고침</Label>
|
||||||
|
<Switch
|
||||||
|
checked={config.afterInsert?.refreshData ?? true}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateConfig({
|
||||||
|
afterInsert: { ...config.afterInsert, refreshData: checked },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground -mt-2">
|
||||||
|
테이블리스트, 카드 디스플레이 컴포넌트를 새로고침합니다
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs font-normal">성공 메시지 표시</Label>
|
||||||
|
<Switch
|
||||||
|
checked={config.afterInsert?.showSuccessMessage ?? true}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateConfig({
|
||||||
|
afterInsert: { ...config.afterInsert, showSuccessMessage: checked },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.afterInsert?.showSuccessMessage && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">성공 메시지</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="저장되었습니다."
|
||||||
|
value={config.afterInsert?.successMessage || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateConfig({
|
||||||
|
afterInsert: { ...config.afterInsert, successMessage: e.target.value },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 중복 체크 설정 */}
|
||||||
|
{config.targetTable && (
|
||||||
|
<div className="space-y-3 rounded border bg-background p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs font-medium">중복 체크</Label>
|
||||||
|
<Switch
|
||||||
|
checked={config.duplicateCheck?.enabled ?? false}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateConfig({
|
||||||
|
duplicateCheck: { ...config.duplicateCheck, enabled: checked },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.duplicateCheck?.enabled && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">중복 체크 컬럼</Label>
|
||||||
|
<div className="mt-1 max-h-40 overflow-y-auto rounded border bg-background p-2">
|
||||||
|
{targetColumns.length === 0 ? (
|
||||||
|
<p className="text-[10px] text-muted-foreground">컬럼을 불러오는 중...</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{targetColumns.map((col) => {
|
||||||
|
const isChecked = (config.duplicateCheck?.columns || []).includes(col.name);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={col.name}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded px-1 py-0.5 hover:bg-muted"
|
||||||
|
onClick={() => {
|
||||||
|
const currentColumns = config.duplicateCheck?.columns || [];
|
||||||
|
const newColumns = isChecked
|
||||||
|
? currentColumns.filter((c) => c !== col.name)
|
||||||
|
: [...currentColumns, col.name];
|
||||||
|
updateConfig({
|
||||||
|
duplicateCheck: { ...config.duplicateCheck, columns: newColumns },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={() => {}}
|
||||||
|
className="h-3 w-3 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="flex-1 text-xs whitespace-nowrap">
|
||||||
|
{col.label}{col.label !== col.name && ` (${col.name})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||||
|
선택한 컬럼들의 조합으로 중복 여부를 체크합니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">중복 시 에러 메시지</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="이미 존재하는 데이터입니다."
|
||||||
|
value={config.duplicateCheck?.errorMessage || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateConfig({
|
||||||
|
duplicateCheck: { ...config.duplicateCheck, errorMessage: e.target.value },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 사용 안내 */}
|
||||||
|
<div className="rounded-md bg-green-100 p-3 dark:bg-green-900/30">
|
||||||
|
<p className="text-xs text-green-900 dark:text-green-100">
|
||||||
|
<strong>사용 방법:</strong>
|
||||||
|
<br />
|
||||||
|
1. 저장할 대상 테이블을 선택합니다
|
||||||
|
<br />
|
||||||
|
2. 컬럼 매핑을 추가하여 각 컬럼에 어떤 값을 저장할지 설정합니다
|
||||||
|
<br />
|
||||||
|
3. 버튼 클릭 시 설정된 값들이 대상 테이블에 즉시 저장됩니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickInsertConfigSection;
|
||||||
|
|
||||||
|
|
@ -365,7 +365,10 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
userId, // 🆕 사용자 ID
|
userId, // 🆕 사용자 ID
|
||||||
userName, // 🆕 사용자 이름
|
userName, // 🆕 사용자 이름
|
||||||
companyCode, // 🆕 회사 코드
|
companyCode, // 🆕 회사 코드
|
||||||
mode,
|
// 🆕 화면 모드 (edit/view)와 컴포넌트 UI 모드 구분
|
||||||
|
screenMode: mode,
|
||||||
|
// componentConfig.mode가 있으면 유지 (entity-search-input의 UI 모드)
|
||||||
|
mode: component.componentConfig?.mode || mode,
|
||||||
isInModal,
|
isInModal,
|
||||||
readonly: component.readonly,
|
readonly: component.readonly,
|
||||||
// 🆕 disabledFields 체크 또는 기존 readonly
|
// 🆕 disabledFields 체크 또는 기존 readonly
|
||||||
|
|
|
||||||
|
|
@ -964,6 +964,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
componentConfigs,
|
componentConfigs,
|
||||||
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
||||||
splitPanelParentData,
|
splitPanelParentData,
|
||||||
|
// 🆕 분할 패널 컨텍스트 (quickInsert 등에서 좌측 패널 데이터 접근용)
|
||||||
|
splitPanelContext: splitPanelContext ? {
|
||||||
|
selectedLeftData: splitPanelContext.selectedLeftData,
|
||||||
|
refreshRightPanel: splitPanelContext.refreshRightPanel,
|
||||||
|
} : undefined,
|
||||||
} as ButtonActionContext;
|
} as ButtonActionContext;
|
||||||
|
|
||||||
// 확인이 필요한 액션인지 확인
|
// 확인이 필요한 액션인지 확인
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,23 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
// 필터 상태 (검색 필터 위젯에서 전달받은 필터)
|
// 필터 상태 (검색 필터 위젯에서 전달받은 필터)
|
||||||
const [filters, setFiltersInternal] = useState<TableFilter[]>([]);
|
const [filters, setFiltersInternal] = useState<TableFilter[]>([]);
|
||||||
|
|
||||||
|
// 새로고침 트리거 (refreshCardDisplay 이벤트 수신 시 증가)
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
|
||||||
|
// refreshCardDisplay 이벤트 리스너
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRefreshCardDisplay = () => {
|
||||||
|
console.log("📍 [CardDisplay] refreshCardDisplay 이벤트 수신 - 데이터 새로고침");
|
||||||
|
setRefreshKey((prev) => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("refreshCardDisplay", handleRefreshCardDisplay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("refreshCardDisplay", handleRefreshCardDisplay);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 필터 상태 변경 래퍼
|
// 필터 상태 변경 래퍼
|
||||||
const setFilters = useCallback((newFilters: TableFilter[]) => {
|
const setFilters = useCallback((newFilters: TableFilter[]) => {
|
||||||
setFiltersInternal(newFilters);
|
setFiltersInternal(newFilters);
|
||||||
|
|
@ -357,7 +374,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadTableData();
|
loadTableData();
|
||||||
}, [isDesignMode, tableName, component.componentConfig?.tableName, splitPanelContext?.selectedLeftData, splitPanelPosition]);
|
}, [isDesignMode, tableName, component.componentConfig?.tableName, splitPanelContext?.selectedLeftData, splitPanelPosition, refreshKey]);
|
||||||
|
|
||||||
// 컴포넌트 설정 (기본값 보장)
|
// 컴포넌트 설정 (기본값 보장)
|
||||||
const componentConfig = {
|
const componentConfig = {
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,32 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search, X } from "lucide-react";
|
import { Search, X, Check, ChevronsUpDown } from "lucide-react";
|
||||||
import { EntitySearchModal } from "./EntitySearchModal";
|
import { EntitySearchModal } from "./EntitySearchModal";
|
||||||
import { EntitySearchInputProps, EntitySearchResult } from "./types";
|
import { EntitySearchInputProps, EntitySearchResult } from "./types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import { dynamicFormApi } from "@/lib/api/dynamicForm";
|
||||||
|
|
||||||
export function EntitySearchInputComponent({
|
export function EntitySearchInputComponent({
|
||||||
tableName,
|
tableName,
|
||||||
displayField,
|
displayField,
|
||||||
valueField,
|
valueField,
|
||||||
searchFields = [displayField],
|
searchFields = [displayField],
|
||||||
mode = "combo",
|
mode: modeProp,
|
||||||
|
uiMode, // EntityConfigPanel에서 저장되는 값
|
||||||
placeholder = "검색...",
|
placeholder = "검색...",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
filterCondition = {},
|
filterCondition = {},
|
||||||
|
|
@ -24,31 +39,99 @@ export function EntitySearchInputComponent({
|
||||||
showAdditionalInfo = false,
|
showAdditionalInfo = false,
|
||||||
additionalFields = [],
|
additionalFields = [],
|
||||||
className,
|
className,
|
||||||
}: EntitySearchInputProps) {
|
style,
|
||||||
|
// 🆕 추가 props
|
||||||
|
component,
|
||||||
|
isInteractive,
|
||||||
|
onFormDataChange,
|
||||||
|
}: EntitySearchInputProps & {
|
||||||
|
uiMode?: string;
|
||||||
|
component?: any;
|
||||||
|
isInteractive?: boolean;
|
||||||
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
|
}) {
|
||||||
|
// uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo"
|
||||||
|
const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete";
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [selectOpen, setSelectOpen] = useState(false);
|
||||||
const [displayValue, setDisplayValue] = useState("");
|
const [displayValue, setDisplayValue] = useState("");
|
||||||
const [selectedData, setSelectedData] = useState<EntitySearchResult | null>(null);
|
const [selectedData, setSelectedData] = useState<EntitySearchResult | null>(null);
|
||||||
|
const [options, setOptions] = useState<EntitySearchResult[]>([]);
|
||||||
|
const [isLoadingOptions, setIsLoadingOptions] = useState(false);
|
||||||
|
const [optionsLoaded, setOptionsLoaded] = useState(false);
|
||||||
|
|
||||||
|
// filterCondition을 문자열로 변환하여 비교 (객체 참조 문제 해결)
|
||||||
|
const filterConditionKey = JSON.stringify(filterCondition || {});
|
||||||
|
|
||||||
|
// select 모드일 때 옵션 로드 (한 번만)
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === "select" && tableName && !optionsLoaded) {
|
||||||
|
loadOptions();
|
||||||
|
setOptionsLoaded(true);
|
||||||
|
}
|
||||||
|
}, [mode, tableName, filterConditionKey, optionsLoaded]);
|
||||||
|
|
||||||
|
const loadOptions = async () => {
|
||||||
|
if (!tableName) return;
|
||||||
|
|
||||||
|
setIsLoadingOptions(true);
|
||||||
|
try {
|
||||||
|
const response = await dynamicFormApi.getTableData(tableName, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100, // 최대 100개까지 로드
|
||||||
|
filters: filterCondition,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setOptions(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("옵션 로드 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingOptions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// value가 변경되면 표시값 업데이트
|
// value가 변경되면 표시값 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value && selectedData) {
|
if (value && selectedData) {
|
||||||
setDisplayValue(selectedData[displayField] || "");
|
setDisplayValue(selectedData[displayField] || "");
|
||||||
} else {
|
} else if (value && mode === "select" && options.length > 0) {
|
||||||
|
// select 모드에서 value가 있고 options가 로드된 경우
|
||||||
|
const found = options.find(opt => opt[valueField] === value);
|
||||||
|
if (found) {
|
||||||
|
setSelectedData(found);
|
||||||
|
setDisplayValue(found[displayField] || "");
|
||||||
|
}
|
||||||
|
} else if (!value) {
|
||||||
setDisplayValue("");
|
setDisplayValue("");
|
||||||
setSelectedData(null);
|
setSelectedData(null);
|
||||||
}
|
}
|
||||||
}, [value, displayField]);
|
}, [value, displayField, options, mode, valueField]);
|
||||||
|
|
||||||
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
|
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
|
||||||
setSelectedData(fullData);
|
setSelectedData(fullData);
|
||||||
setDisplayValue(fullData[displayField] || "");
|
setDisplayValue(fullData[displayField] || "");
|
||||||
onChange?.(newValue, fullData);
|
onChange?.(newValue, fullData);
|
||||||
|
|
||||||
|
// 🆕 onFormDataChange 호출 (formData에 값 저장)
|
||||||
|
if (isInteractive && onFormDataChange && component?.columnName) {
|
||||||
|
onFormDataChange(component.columnName, newValue);
|
||||||
|
console.log("📤 EntitySearchInput -> onFormDataChange:", component.columnName, newValue);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
setDisplayValue("");
|
setDisplayValue("");
|
||||||
setSelectedData(null);
|
setSelectedData(null);
|
||||||
onChange?.(null, null);
|
onChange?.(null, null);
|
||||||
|
|
||||||
|
// 🆕 onFormDataChange 호출 (formData에서 값 제거)
|
||||||
|
if (isInteractive && onFormDataChange && component?.columnName) {
|
||||||
|
onFormDataChange(component.columnName, null);
|
||||||
|
console.log("📤 EntitySearchInput -> onFormDataChange (clear):", component.columnName, null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
const handleOpenModal = () => {
|
||||||
|
|
@ -57,10 +140,105 @@ export function EntitySearchInputComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectOption = (option: EntitySearchResult) => {
|
||||||
|
handleSelect(option[valueField], option);
|
||||||
|
setSelectOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 높이 계산 (style에서 height가 있으면 사용, 없으면 기본값)
|
||||||
|
const componentHeight = style?.height;
|
||||||
|
const inputStyle: React.CSSProperties = componentHeight
|
||||||
|
? { height: componentHeight }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// select 모드: 검색 가능한 드롭다운
|
||||||
|
if (mode === "select") {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col", className)} style={style}>
|
||||||
|
<Popover open={selectOpen} onOpenChange={setSelectOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={selectOpen}
|
||||||
|
disabled={disabled || isLoadingOptions}
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between font-normal",
|
||||||
|
!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm",
|
||||||
|
!value && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
style={inputStyle}
|
||||||
|
>
|
||||||
|
{isLoadingOptions
|
||||||
|
? "로딩 중..."
|
||||||
|
: displayValue || placeholder}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="p-0"
|
||||||
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={`${displayField} 검색...`}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="text-xs sm:text-sm py-4 text-center">
|
||||||
|
항목을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<CommandItem
|
||||||
|
key={option[valueField] || index}
|
||||||
|
value={`${option[displayField] || ""}-${option[valueField] || ""}`}
|
||||||
|
onSelect={() => handleSelectOption(option)}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
value === option[valueField] ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{option[displayField]}</span>
|
||||||
|
{valueField !== displayField && (
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{option[valueField]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* 추가 정보 표시 */}
|
||||||
|
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
|
||||||
|
<div className="text-xs text-muted-foreground space-y-1 px-2 mt-1">
|
||||||
|
{additionalFields.map((field) => (
|
||||||
|
<div key={field} className="flex gap-2">
|
||||||
|
<span className="font-medium">{field}:</span>
|
||||||
|
<span>{selectedData[field] || "-"}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// modal, combo, autocomplete 모드
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-2", className)}>
|
<div className={cn("flex flex-col", className)} style={style}>
|
||||||
{/* 입력 필드 */}
|
{/* 입력 필드 */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 h-full">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Input
|
<Input
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
|
|
@ -68,7 +246,8 @@ export function EntitySearchInputComponent({
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readOnly={mode === "modal" || mode === "combo"}
|
readOnly={mode === "modal" || mode === "combo"}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm pr-8"
|
className={cn("w-full pr-8", !componentHeight && "h-8 text-xs sm:h-10 sm:text-sm")}
|
||||||
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
{displayValue && !disabled && (
|
{displayValue && !disabled && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -83,12 +262,14 @@ export function EntitySearchInputComponent({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 버튼: modal 또는 combo 모드일 때만 표시 */}
|
||||||
{(mode === "modal" || mode === "combo") && (
|
{(mode === "modal" || mode === "combo") && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenModal}
|
onClick={handleOpenModal}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className={cn(!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm")}
|
||||||
|
style={inputStyle}
|
||||||
>
|
>
|
||||||
<Search className="h-4 w-4" />
|
<Search className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -97,7 +278,7 @@ export function EntitySearchInputComponent({
|
||||||
|
|
||||||
{/* 추가 정보 표시 */}
|
{/* 추가 정보 표시 */}
|
||||||
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
|
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
|
||||||
<div className="text-xs text-muted-foreground space-y-1 px-2">
|
<div className="text-xs text-muted-foreground space-y-1 px-2 mt-1">
|
||||||
{additionalFields.map((field) => (
|
{additionalFields.map((field) => (
|
||||||
<div key={field} className="flex gap-2">
|
<div key={field} className="flex gap-2">
|
||||||
<span className="font-medium">{field}:</span>
|
<span className="font-medium">{field}:</span>
|
||||||
|
|
@ -107,19 +288,21 @@ export function EntitySearchInputComponent({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 검색 모달 */}
|
{/* 검색 모달: modal 또는 combo 모드일 때만 렌더링 */}
|
||||||
<EntitySearchModal
|
{(mode === "modal" || mode === "combo") && (
|
||||||
open={modalOpen}
|
<EntitySearchModal
|
||||||
onOpenChange={setModalOpen}
|
open={modalOpen}
|
||||||
tableName={tableName}
|
onOpenChange={setModalOpen}
|
||||||
displayField={displayField}
|
tableName={tableName}
|
||||||
valueField={valueField}
|
displayField={displayField}
|
||||||
searchFields={searchFields}
|
valueField={valueField}
|
||||||
filterCondition={filterCondition}
|
searchFields={searchFields}
|
||||||
modalTitle={modalTitle}
|
filterCondition={filterCondition}
|
||||||
modalColumns={modalColumns}
|
modalTitle={modalTitle}
|
||||||
onSelect={handleSelect}
|
modalColumns={modalColumns}
|
||||||
/>
|
onSelect={handleSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ export function EntitySearchInputConfigPanel({
|
||||||
<Label className="text-xs sm:text-sm">UI 모드</Label>
|
<Label className="text-xs sm:text-sm">UI 모드</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localConfig.mode || "combo"}
|
value={localConfig.mode || "combo"}
|
||||||
onValueChange={(value: "autocomplete" | "modal" | "combo") =>
|
onValueChange={(value: "select" | "autocomplete" | "modal" | "combo") =>
|
||||||
updateConfig({ mode: value })
|
updateConfig({ mode: value })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -310,11 +310,18 @@ export function EntitySearchInputConfigPanel({
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
<SelectItem value="select">드롭다운 (검색 가능)</SelectItem>
|
||||||
<SelectItem value="combo">콤보 (입력 + 모달)</SelectItem>
|
<SelectItem value="combo">콤보 (입력 + 모달)</SelectItem>
|
||||||
<SelectItem value="modal">모달만</SelectItem>
|
<SelectItem value="modal">모달만</SelectItem>
|
||||||
<SelectItem value="autocomplete">자동완성만</SelectItem>
|
<SelectItem value="autocomplete">자동완성만</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
{localConfig.mode === "select" && "검색 가능한 드롭다운 형태로 표시됩니다."}
|
||||||
|
{localConfig.mode === "modal" && "모달 팝업에서 데이터를 검색하고 선택합니다."}
|
||||||
|
{(localConfig.mode === "combo" || !localConfig.mode) && "입력 필드와 검색 버튼이 함께 표시됩니다."}
|
||||||
|
{localConfig.mode === "autocomplete" && "입력하면서 자동완성 목록이 표시됩니다."}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export interface EntitySearchInputConfig {
|
||||||
valueField: string;
|
valueField: string;
|
||||||
searchFields?: string[];
|
searchFields?: string[];
|
||||||
filterCondition?: Record<string, any>;
|
filterCondition?: Record<string, any>;
|
||||||
mode?: "autocomplete" | "modal" | "combo";
|
mode?: "select" | "autocomplete" | "modal" | "combo";
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
modalTitle?: string;
|
modalTitle?: string;
|
||||||
modalColumns?: string[];
|
modalColumns?: string[];
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ export interface EntitySearchInputProps {
|
||||||
searchFields?: string[]; // 검색 대상 필드들 (기본: [displayField])
|
searchFields?: string[]; // 검색 대상 필드들 (기본: [displayField])
|
||||||
|
|
||||||
// UI 모드
|
// UI 모드
|
||||||
mode?: "autocomplete" | "modal" | "combo"; // 기본: "combo"
|
// - select: 드롭다운 선택 (검색 가능한 콤보박스)
|
||||||
|
// - modal: 모달 팝업에서 선택
|
||||||
|
// - combo: 입력 + 모달 버튼 (기본)
|
||||||
|
// - autocomplete: 입력하면서 자동완성
|
||||||
|
mode?: "select" | "autocomplete" | "modal" | "combo"; // 기본: "combo"
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
|
|
@ -33,6 +37,7 @@ export interface EntitySearchInputProps {
|
||||||
|
|
||||||
// 스타일
|
// 스타일
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntitySearchResult {
|
export interface EntitySearchResult {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ export type ButtonActionType =
|
||||||
// | "empty_vehicle" // 공차등록 (위치 수집 + 상태 변경) - 운행알림으로 통합
|
// | "empty_vehicle" // 공차등록 (위치 수집 + 상태 변경) - 운행알림으로 통합
|
||||||
| "operation_control" // 운행알림 및 종료 (위치 수집 + 상태 변경 + 연속 추적)
|
| "operation_control" // 운행알림 및 종료 (위치 수집 + 상태 변경 + 연속 추적)
|
||||||
| "swap_fields" // 필드 값 교환 (출발지 ↔ 목적지)
|
| "swap_fields" // 필드 값 교환 (출발지 ↔ 목적지)
|
||||||
| "transferData"; // 데이터 전달 (컴포넌트 간 or 화면 간)
|
| "transferData" // 데이터 전달 (컴포넌트 간 or 화면 간)
|
||||||
|
| "quickInsert"; // 즉시 저장 (선택한 데이터를 특정 테이블에 즉시 INSERT)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 버튼 액션 설정
|
* 버튼 액션 설정
|
||||||
|
|
@ -211,6 +212,31 @@ export interface ButtonActionConfig {
|
||||||
maxSelection?: number; // 최대 선택 개수
|
maxSelection?: number; // 최대 선택 개수
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 즉시 저장 (Quick Insert) 관련
|
||||||
|
quickInsertConfig?: {
|
||||||
|
targetTable: string; // 저장할 테이블명
|
||||||
|
columnMappings: Array<{
|
||||||
|
targetColumn: string; // 대상 테이블의 컬럼명
|
||||||
|
sourceType: "component" | "leftPanel" | "fixed" | "currentUser"; // 값 소스 타입
|
||||||
|
sourceComponentId?: string; // 컴포넌트에서 값을 가져올 경우 컴포넌트 ID
|
||||||
|
sourceColumnName?: string; // 컴포넌트의 columnName (formData 접근용)
|
||||||
|
sourceColumn?: string; // 좌측 패널 또는 컴포넌트의 특정 컬럼
|
||||||
|
fixedValue?: any; // 고정값
|
||||||
|
userField?: "userId" | "userName" | "companyCode"; // currentUser 타입일 때 사용할 필드
|
||||||
|
}>;
|
||||||
|
duplicateCheck?: {
|
||||||
|
enabled: boolean; // 중복 체크 활성화 여부
|
||||||
|
columns?: string[]; // 중복 체크할 컬럼들
|
||||||
|
errorMessage?: string; // 중복 시 에러 메시지
|
||||||
|
};
|
||||||
|
afterInsert?: {
|
||||||
|
refreshData?: boolean; // 저장 후 데이터 새로고침 (테이블리스트, 카드 디스플레이)
|
||||||
|
clearComponents?: boolean; // 저장 후 컴포넌트 값 초기화
|
||||||
|
showSuccessMessage?: boolean; // 성공 메시지 표시 여부 (기본: true)
|
||||||
|
successMessage?: string; // 성공 메시지
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -265,6 +291,12 @@ export interface ButtonActionContext {
|
||||||
|
|
||||||
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
||||||
splitPanelParentData?: Record<string, any>;
|
splitPanelParentData?: Record<string, any>;
|
||||||
|
|
||||||
|
// 🆕 분할 패널 컨텍스트 (quickInsert 등에서 좌측 패널 데이터 접근용)
|
||||||
|
splitPanelContext?: {
|
||||||
|
selectedLeftData?: Record<string, any>;
|
||||||
|
refreshRightPanel?: () => void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -365,6 +397,9 @@ export class ButtonActionExecutor {
|
||||||
case "swap_fields":
|
case "swap_fields":
|
||||||
return await this.handleSwapFields(config, context);
|
return await this.handleSwapFields(config, context);
|
||||||
|
|
||||||
|
case "quickInsert":
|
||||||
|
return await this.handleQuickInsert(config, context);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
|
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -5190,6 +5225,313 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 즉시 저장 (Quick Insert) 액션 처리
|
||||||
|
* 화면에서 선택한 데이터를 특정 테이블에 즉시 저장
|
||||||
|
*/
|
||||||
|
private static async handleQuickInsert(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
console.log("⚡ Quick Insert 액션 실행:", { config, context });
|
||||||
|
|
||||||
|
const quickInsertConfig = config.quickInsertConfig;
|
||||||
|
if (!quickInsertConfig?.targetTable) {
|
||||||
|
toast.error("대상 테이블이 설정되지 않았습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formData, splitPanelContext, userId, userName, companyCode } = context;
|
||||||
|
|
||||||
|
console.log("⚡ Quick Insert 상세 정보:", {
|
||||||
|
targetTable: quickInsertConfig.targetTable,
|
||||||
|
columnMappings: quickInsertConfig.columnMappings,
|
||||||
|
formData: formData,
|
||||||
|
formDataKeys: Object.keys(formData || {}),
|
||||||
|
splitPanelContext: splitPanelContext,
|
||||||
|
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||||
|
allComponents: context.allComponents,
|
||||||
|
userId,
|
||||||
|
userName,
|
||||||
|
companyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컬럼 매핑에 따라 저장할 데이터 구성
|
||||||
|
const insertData: Record<string, any> = {};
|
||||||
|
const columnMappings = quickInsertConfig.columnMappings || [];
|
||||||
|
|
||||||
|
for (const mapping of columnMappings) {
|
||||||
|
console.log(`📍 매핑 처리 시작:`, mapping);
|
||||||
|
|
||||||
|
if (!mapping.targetColumn) {
|
||||||
|
console.log(`📍 targetColumn 없음, 스킵`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: any = undefined;
|
||||||
|
|
||||||
|
switch (mapping.sourceType) {
|
||||||
|
case "component":
|
||||||
|
console.log(`📍 component 타입 처리:`, {
|
||||||
|
sourceComponentId: mapping.sourceComponentId,
|
||||||
|
sourceColumnName: mapping.sourceColumnName,
|
||||||
|
targetColumn: mapping.targetColumn,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컴포넌트의 현재 값
|
||||||
|
if (mapping.sourceComponentId) {
|
||||||
|
// 1. sourceColumnName이 있으면 직접 사용 (가장 확실한 방법)
|
||||||
|
if (mapping.sourceColumnName) {
|
||||||
|
value = formData?.[mapping.sourceColumnName];
|
||||||
|
console.log(`📍 방법1 (sourceColumnName): ${mapping.sourceColumnName} = ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 없으면 컴포넌트 ID로 직접 찾기
|
||||||
|
if (value === undefined) {
|
||||||
|
value = formData?.[mapping.sourceComponentId];
|
||||||
|
console.log(`📍 방법2 (sourceComponentId): ${mapping.sourceComponentId} = ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 없으면 allComponents에서 컴포넌트를 찾아 columnName으로 시도
|
||||||
|
if (value === undefined && context.allComponents) {
|
||||||
|
const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId);
|
||||||
|
console.log(`📍 방법3 찾은 컴포넌트:`, comp);
|
||||||
|
if (comp?.columnName) {
|
||||||
|
value = formData?.[comp.columnName];
|
||||||
|
console.log(`📍 방법3 (allComponents): ${mapping.sourceComponentId} → ${comp.columnName} = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. targetColumn과 같은 이름의 키가 formData에 있으면 사용 (폴백)
|
||||||
|
if (value === undefined && mapping.targetColumn && formData?.[mapping.targetColumn] !== undefined) {
|
||||||
|
value = formData[mapping.targetColumn];
|
||||||
|
console.log(`📍 방법4 (targetColumn 폴백): ${mapping.targetColumn} = ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 그래도 없으면 formData의 모든 키를 확인하고 로깅
|
||||||
|
if (value === undefined) {
|
||||||
|
console.log("📍 방법5: formData에서 값을 찾지 못함. formData 키들:", Object.keys(formData || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceColumn이 지정된 경우 해당 속성 추출
|
||||||
|
if (mapping.sourceColumn && value && typeof value === "object") {
|
||||||
|
value = value[mapping.sourceColumn];
|
||||||
|
console.log(`📍 sourceColumn 추출: ${mapping.sourceColumn} = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "leftPanel":
|
||||||
|
console.log(`📍 leftPanel 타입 처리:`, {
|
||||||
|
sourceColumn: mapping.sourceColumn,
|
||||||
|
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||||
|
});
|
||||||
|
// 좌측 패널 선택 데이터
|
||||||
|
if (mapping.sourceColumn && splitPanelContext?.selectedLeftData) {
|
||||||
|
value = splitPanelContext.selectedLeftData[mapping.sourceColumn];
|
||||||
|
console.log(`📍 leftPanel 값: ${mapping.sourceColumn} = ${value}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "fixed":
|
||||||
|
console.log(`📍 fixed 타입 처리: fixedValue = ${mapping.fixedValue}`);
|
||||||
|
// 고정값
|
||||||
|
value = mapping.fixedValue;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "currentUser":
|
||||||
|
console.log(`📍 currentUser 타입 처리: userField = ${mapping.userField}`);
|
||||||
|
// 현재 사용자 정보
|
||||||
|
switch (mapping.userField) {
|
||||||
|
case "userId":
|
||||||
|
value = userId;
|
||||||
|
break;
|
||||||
|
case "userName":
|
||||||
|
value = userName;
|
||||||
|
break;
|
||||||
|
case "companyCode":
|
||||||
|
value = companyCode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
console.log(`📍 currentUser 값: ${value}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`📍 알 수 없는 sourceType: ${mapping.sourceType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📍 매핑 결과: targetColumn=${mapping.targetColumn}, value=${value}, type=${typeof value}`);
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
insertData[mapping.targetColumn] = value;
|
||||||
|
console.log(`📍 insertData에 추가됨: ${mapping.targetColumn} = ${value}`);
|
||||||
|
} else {
|
||||||
|
console.log(`📍 값이 비어있어서 insertData에 추가 안됨`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 좌측 패널 선택 데이터에서 자동 매핑 (대상 테이블에 존재하는 컬럼만)
|
||||||
|
if (splitPanelContext?.selectedLeftData) {
|
||||||
|
const leftData = splitPanelContext.selectedLeftData;
|
||||||
|
console.log("📍 좌측 패널 자동 매핑 시작:", leftData);
|
||||||
|
|
||||||
|
// 대상 테이블의 컬럼 목록 조회
|
||||||
|
let targetTableColumns: string[] = [];
|
||||||
|
try {
|
||||||
|
const columnsResponse = await apiClient.get(
|
||||||
|
`/table-management/tables/${quickInsertConfig.targetTable}/columns`
|
||||||
|
);
|
||||||
|
if (columnsResponse.data?.success && columnsResponse.data?.data) {
|
||||||
|
const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data;
|
||||||
|
targetTableColumns = columnsData.map((col: any) => col.columnName || col.column_name || col.name);
|
||||||
|
console.log("📍 대상 테이블 컬럼 목록:", targetTableColumns);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("대상 테이블 컬럼 조회 실패:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, val] of Object.entries(leftData)) {
|
||||||
|
// 이미 매핑된 컬럼은 스킵
|
||||||
|
if (insertData[key] !== undefined) {
|
||||||
|
console.log(`📍 자동 매핑 스킵 (이미 존재): ${key}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대상 테이블에 해당 컬럼이 없으면 스킵
|
||||||
|
if (targetTableColumns.length > 0 && !targetTableColumns.includes(key)) {
|
||||||
|
console.log(`📍 자동 매핑 스킵 (대상 테이블에 없는 컬럼): ${key}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시스템 컬럼 제외 (id, created_date, updated_date, writer 등)
|
||||||
|
const systemColumns = ['id', 'created_date', 'updated_date', 'writer', 'writer_name'];
|
||||||
|
if (systemColumns.includes(key)) {
|
||||||
|
console.log(`📍 자동 매핑 스킵 (시스템 컬럼): ${key}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// _label, _name 으로 끝나는 표시용 컬럼 제외
|
||||||
|
if (key.endsWith('_label') || key.endsWith('_name')) {
|
||||||
|
console.log(`📍 자동 매핑 스킵 (표시용 컬럼): ${key}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 값이 있으면 자동 추가
|
||||||
|
if (val !== undefined && val !== null && val !== '') {
|
||||||
|
insertData[key] = val;
|
||||||
|
console.log(`📍 자동 매핑 추가: ${key} = ${val}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("⚡ Quick Insert 최종 데이터:", insertData, "키 개수:", Object.keys(insertData).length);
|
||||||
|
|
||||||
|
// 필수 데이터 검증
|
||||||
|
if (Object.keys(insertData).length === 0) {
|
||||||
|
toast.error("저장할 데이터가 없습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 중복 체크
|
||||||
|
console.log("📍 중복 체크 설정:", {
|
||||||
|
enabled: quickInsertConfig.duplicateCheck?.enabled,
|
||||||
|
columns: quickInsertConfig.duplicateCheck?.columns,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (quickInsertConfig.duplicateCheck?.enabled && quickInsertConfig.duplicateCheck?.columns?.length > 0) {
|
||||||
|
const duplicateCheckData: Record<string, any> = {};
|
||||||
|
for (const col of quickInsertConfig.duplicateCheck.columns) {
|
||||||
|
if (insertData[col] !== undefined) {
|
||||||
|
// 백엔드가 { value, operator } 형식을 기대하므로 변환
|
||||||
|
duplicateCheckData[col] = { value: insertData[col], operator: "equals" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📍 중복 체크 조건:", duplicateCheckData);
|
||||||
|
|
||||||
|
if (Object.keys(duplicateCheckData).length > 0) {
|
||||||
|
try {
|
||||||
|
const checkResponse = await apiClient.post(
|
||||||
|
`/table-management/tables/${quickInsertConfig.targetTable}/data`,
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
search: duplicateCheckData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("📍 중복 체크 응답:", checkResponse.data);
|
||||||
|
|
||||||
|
// 응답 구조: { success: true, data: { data: [...], total: N } } 또는 { success: true, data: [...] }
|
||||||
|
const existingData = checkResponse.data?.data?.data || checkResponse.data?.data || [];
|
||||||
|
console.log("📍 기존 데이터:", existingData, "길이:", Array.isArray(existingData) ? existingData.length : 0);
|
||||||
|
|
||||||
|
if (Array.isArray(existingData) && existingData.length > 0) {
|
||||||
|
toast.error(quickInsertConfig.duplicateCheck.errorMessage || "이미 존재하는 데이터입니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("중복 체크 오류:", error);
|
||||||
|
// 중복 체크 실패해도 저장은 시도
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("📍 중복 체크 비활성화 또는 컬럼 미설정");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터 저장
|
||||||
|
const response = await apiClient.post(
|
||||||
|
`/table-management/tables/${quickInsertConfig.targetTable}/add`,
|
||||||
|
insertData
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
console.log("✅ Quick Insert 저장 성공");
|
||||||
|
|
||||||
|
// 저장 후 동작 설정 로그
|
||||||
|
console.log("📍 afterInsert 설정:", quickInsertConfig.afterInsert);
|
||||||
|
|
||||||
|
// 🆕 데이터 새로고침 (테이블리스트, 카드 디스플레이 컴포넌트 새로고침)
|
||||||
|
// refreshData가 명시적으로 false가 아니면 기본적으로 새로고침 실행
|
||||||
|
const shouldRefresh = quickInsertConfig.afterInsert?.refreshData !== false;
|
||||||
|
console.log("📍 데이터 새로고침 여부:", shouldRefresh);
|
||||||
|
|
||||||
|
if (shouldRefresh) {
|
||||||
|
console.log("📍 데이터 새로고침 이벤트 발송");
|
||||||
|
// 전역 이벤트로 테이블/카드 컴포넌트들에게 새로고침 알림
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||||
|
window.dispatchEvent(new CustomEvent("refreshCardDisplay"));
|
||||||
|
console.log("✅ refreshTable, refreshCardDisplay 이벤트 발송 완료");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컴포넌트 값 초기화
|
||||||
|
if (quickInsertConfig.afterInsert?.clearComponents && context.onFormDataChange) {
|
||||||
|
for (const mapping of columnMappings) {
|
||||||
|
if (mapping.sourceType === "component" && mapping.sourceComponentId) {
|
||||||
|
// sourceColumnName이 있으면 그것을 사용, 없으면 sourceComponentId 사용
|
||||||
|
const fieldName = mapping.sourceColumnName || mapping.sourceComponentId;
|
||||||
|
context.onFormDataChange(fieldName, null);
|
||||||
|
console.log(`📍 컴포넌트 값 초기화: ${fieldName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quickInsertConfig.afterInsert?.showSuccessMessage !== false) {
|
||||||
|
toast.success(quickInsertConfig.afterInsert?.successMessage || "저장되었습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
toast.error(response.data?.message || "저장에 실패했습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ Quick Insert 오류:", error);
|
||||||
|
toast.error(error.response?.data?.message || "저장 중 오류가 발생했습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 필드 값 변경 액션 처리 (예: status를 active로 변경)
|
* 필드 값 변경 액션 처리 (예: status를 active로 변경)
|
||||||
* 🆕 위치정보 수집 기능 추가
|
* 🆕 위치정보 수집 기능 추가
|
||||||
|
|
@ -5643,4 +5985,9 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
|
||||||
successMessage: "필드 값이 교환되었습니다.",
|
successMessage: "필드 값이 교환되었습니다.",
|
||||||
errorMessage: "필드 값 교환 중 오류가 발생했습니다.",
|
errorMessage: "필드 값 교환 중 오류가 발생했습니다.",
|
||||||
},
|
},
|
||||||
|
quickInsert: {
|
||||||
|
type: "quickInsert",
|
||||||
|
successMessage: "저장되었습니다.",
|
||||||
|
errorMessage: "저장 중 오류가 발생했습니다.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export const WEB_TYPE_COMPONENT_MAPPING: Record<string, string> = {
|
||||||
// 기타
|
// 기타
|
||||||
label: "text-display",
|
label: "text-display",
|
||||||
code: "select-basic", // 코드 타입은 선택상자 사용
|
code: "select-basic", // 코드 타입은 선택상자 사용
|
||||||
entity: "select-basic", // 엔티티 타입은 선택상자 사용
|
entity: "entity-search-input", // 엔티티 타입은 전용 검색 입력 사용
|
||||||
category: "select-basic", // 카테고리 타입은 선택상자 사용
|
category: "select-basic", // 카테고리 타입은 선택상자 사용
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,8 @@ export interface EntityTypeConfig {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
displayFormat?: "simple" | "detailed" | "custom"; // 표시 형식
|
displayFormat?: "simple" | "detailed" | "custom"; // 표시 형식
|
||||||
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
||||||
|
// UI 모드
|
||||||
|
uiMode?: "select" | "modal" | "combo" | "autocomplete"; // 기본: "combo"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -428,6 +430,111 @@ export interface ButtonTypeConfig {
|
||||||
// ButtonActionType과 관련된 설정은 control-management.ts에서 정의
|
// ButtonActionType과 관련된 설정은 control-management.ts에서 정의
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 즉시 저장(quickInsert) 설정 =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 즉시 저장 컬럼 매핑 설정
|
||||||
|
* 저장할 테이블의 각 컬럼에 대해 값을 어디서 가져올지 정의
|
||||||
|
*/
|
||||||
|
export interface QuickInsertColumnMapping {
|
||||||
|
/** 저장할 테이블의 대상 컬럼명 */
|
||||||
|
targetColumn: string;
|
||||||
|
|
||||||
|
/** 값 소스 타입 */
|
||||||
|
sourceType: "component" | "leftPanel" | "fixed" | "currentUser";
|
||||||
|
|
||||||
|
// sourceType별 추가 설정
|
||||||
|
/** component: 값을 가져올 컴포넌트 ID */
|
||||||
|
sourceComponentId?: string;
|
||||||
|
|
||||||
|
/** component: 컴포넌트의 columnName (formData 접근용) */
|
||||||
|
sourceColumnName?: string;
|
||||||
|
|
||||||
|
/** leftPanel: 좌측 선택 데이터의 컬럼명 */
|
||||||
|
sourceColumn?: string;
|
||||||
|
|
||||||
|
/** fixed: 고정값 */
|
||||||
|
fixedValue?: any;
|
||||||
|
|
||||||
|
/** currentUser: 사용자 정보 필드 */
|
||||||
|
userField?: "userId" | "userName" | "companyCode" | "deptCode";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 즉시 저장 후 동작 설정
|
||||||
|
*/
|
||||||
|
export interface QuickInsertAfterAction {
|
||||||
|
/** 데이터 새로고침 (테이블리스트, 카드 디스플레이 컴포넌트) */
|
||||||
|
refreshData?: boolean;
|
||||||
|
|
||||||
|
/** 초기화할 컴포넌트 ID 목록 */
|
||||||
|
clearComponents?: string[];
|
||||||
|
|
||||||
|
/** 성공 메시지 표시 여부 */
|
||||||
|
showSuccessMessage?: boolean;
|
||||||
|
|
||||||
|
/** 커스텀 성공 메시지 */
|
||||||
|
successMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 중복 체크 설정
|
||||||
|
*/
|
||||||
|
export interface QuickInsertDuplicateCheck {
|
||||||
|
/** 중복 체크 활성화 */
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
/** 중복 체크할 컬럼들 */
|
||||||
|
columns: string[];
|
||||||
|
|
||||||
|
/** 중복 시 에러 메시지 */
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 즉시 저장(quickInsert) 버튼 액션 설정
|
||||||
|
*
|
||||||
|
* 화면에서 entity 타입 선택박스로 데이터를 선택한 후,
|
||||||
|
* 버튼 클릭 시 특정 테이블에 즉시 INSERT하는 기능
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const config: QuickInsertConfig = {
|
||||||
|
* targetTable: "process_equipment",
|
||||||
|
* columnMappings: [
|
||||||
|
* {
|
||||||
|
* targetColumn: "equipment_code",
|
||||||
|
* sourceType: "component",
|
||||||
|
* sourceComponentId: "equipment-select"
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* targetColumn: "process_code",
|
||||||
|
* sourceType: "leftPanel",
|
||||||
|
* sourceColumn: "process_code"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* afterInsert: {
|
||||||
|
* refreshData: true,
|
||||||
|
* clearComponents: ["equipment-select"],
|
||||||
|
* showSuccessMessage: true
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface QuickInsertConfig {
|
||||||
|
/** 저장할 대상 테이블명 */
|
||||||
|
targetTable: string;
|
||||||
|
|
||||||
|
/** 컬럼 매핑 설정 */
|
||||||
|
columnMappings: QuickInsertColumnMapping[];
|
||||||
|
|
||||||
|
/** 저장 후 동작 설정 */
|
||||||
|
afterInsert?: QuickInsertAfterAction;
|
||||||
|
|
||||||
|
/** 중복 체크 설정 (선택사항) */
|
||||||
|
duplicateCheck?: QuickInsertDuplicateCheck;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 플로우 단계별 버튼 표시 설정
|
* 플로우 단계별 버튼 표시 설정
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ export type ButtonActionType =
|
||||||
// 제어관리 전용
|
// 제어관리 전용
|
||||||
| "control"
|
| "control"
|
||||||
// 데이터 전달
|
// 데이터 전달
|
||||||
| "transferData"; // 선택된 데이터를 다른 컴포넌트/화면으로 전달
|
| "transferData" // 선택된 데이터를 다른 컴포넌트/화면으로 전달
|
||||||
|
// 즉시 저장
|
||||||
|
| "quickInsert"; // 선택한 데이터를 특정 테이블에 즉시 INSERT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컴포넌트 타입 정의
|
* 컴포넌트 타입 정의
|
||||||
|
|
@ -328,6 +330,7 @@ export const isButtonActionType = (value: string): value is ButtonActionType =>
|
||||||
"newWindow",
|
"newWindow",
|
||||||
"control",
|
"control",
|
||||||
"transferData",
|
"transferData",
|
||||||
|
"quickInsert",
|
||||||
];
|
];
|
||||||
return actionTypes.includes(value as ButtonActionType);
|
return actionTypes.includes(value as ButtonActionType);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue