엔티티 즉시저장기능 추가
This commit is contained in:
parent
d8329d31e4
commit
f7e3c1924c
|
|
@ -1751,7 +1751,7 @@ export class ScreenManagementService {
|
|||
// 기타
|
||||
label: "text-display",
|
||||
code: "select-basic",
|
||||
entity: "select-basic",
|
||||
entity: "entity-search-input", // 엔티티는 entity-search-input 사용
|
||||
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 () => {
|
||||
try {
|
||||
const actionType = config?.actionType || "save";
|
||||
|
|
@ -604,6 +817,9 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
case "custom":
|
||||
await handleCustomAction();
|
||||
break;
|
||||
case "quickInsert":
|
||||
await handleQuickInsertAction();
|
||||
break;
|
||||
default:
|
||||
// console.log("🔘 기본 버튼 클릭");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { apiClient } from "@/lib/api/client";
|
|||
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
|
||||
import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel";
|
||||
import { FlowVisibilityConfigPanel } from "./FlowVisibilityConfigPanel";
|
||||
import { QuickInsertConfigSection } from "./QuickInsertConfigSection";
|
||||
|
||||
// 🆕 제목 블록 타입
|
||||
interface TitleBlock {
|
||||
|
|
@ -642,9 +643,10 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
<SelectItem value="edit">편집</SelectItem>
|
||||
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||
<SelectItem value="transferData">📦 데이터 전달</SelectItem>
|
||||
<SelectItem value="openModalWithData">데이터 전달 + 모달 열기 🆕</SelectItem>
|
||||
<SelectItem value="transferData">데이터 전달</SelectItem>
|
||||
<SelectItem value="openModalWithData">데이터 전달 + 모달 열기</SelectItem>
|
||||
<SelectItem value="modal">모달 열기</SelectItem>
|
||||
<SelectItem value="quickInsert">즉시 저장</SelectItem>
|
||||
<SelectItem value="control">제어 흐름</SelectItem>
|
||||
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
||||
<SelectItem value="excel_download">엑셀 다운로드</SelectItem>
|
||||
|
|
@ -3068,6 +3070,16 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
</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">
|
||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||
|
|
|
|||
|
|
@ -189,6 +189,33 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
<div className="space-y-3">
|
||||
<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">
|
||||
<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
|
||||
userName, // 🆕 사용자 이름
|
||||
companyCode, // 🆕 회사 코드
|
||||
mode,
|
||||
// 🆕 화면 모드 (edit/view)와 컴포넌트 UI 모드 구분
|
||||
screenMode: mode,
|
||||
// componentConfig.mode가 있으면 유지 (entity-search-input의 UI 모드)
|
||||
mode: component.componentConfig?.mode || mode,
|
||||
isInModal,
|
||||
readonly: component.readonly,
|
||||
// 🆕 disabledFields 체크 또는 기존 readonly
|
||||
|
|
|
|||
|
|
@ -964,6 +964,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
componentConfigs,
|
||||
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
||||
splitPanelParentData,
|
||||
// 🆕 분할 패널 컨텍스트 (quickInsert 등에서 좌측 패널 데이터 접근용)
|
||||
splitPanelContext: splitPanelContext ? {
|
||||
selectedLeftData: splitPanelContext.selectedLeftData,
|
||||
refreshRightPanel: splitPanelContext.refreshRightPanel,
|
||||
} : undefined,
|
||||
} as ButtonActionContext;
|
||||
|
||||
// 확인이 필요한 액션인지 확인
|
||||
|
|
|
|||
|
|
@ -68,6 +68,23 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
// 필터 상태 (검색 필터 위젯에서 전달받은 필터)
|
||||
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[]) => {
|
||||
setFiltersInternal(newFilters);
|
||||
|
|
@ -357,7 +374,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
};
|
||||
|
||||
loadTableData();
|
||||
}, [isDesignMode, tableName, component.componentConfig?.tableName, splitPanelContext?.selectedLeftData, splitPanelPosition]);
|
||||
}, [isDesignMode, tableName, component.componentConfig?.tableName, splitPanelContext?.selectedLeftData, splitPanelPosition, refreshKey]);
|
||||
|
||||
// 컴포넌트 설정 (기본값 보장)
|
||||
const componentConfig = {
|
||||
|
|
|
|||
|
|
@ -3,17 +3,32 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 { EntitySearchInputProps, EntitySearchResult } from "./types";
|
||||
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({
|
||||
tableName,
|
||||
displayField,
|
||||
valueField,
|
||||
searchFields = [displayField],
|
||||
mode = "combo",
|
||||
mode: modeProp,
|
||||
uiMode, // EntityConfigPanel에서 저장되는 값
|
||||
placeholder = "검색...",
|
||||
disabled = false,
|
||||
filterCondition = {},
|
||||
|
|
@ -24,31 +39,99 @@ export function EntitySearchInputComponent({
|
|||
showAdditionalInfo = false,
|
||||
additionalFields = [],
|
||||
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 [selectOpen, setSelectOpen] = useState(false);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
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가 변경되면 표시값 업데이트
|
||||
useEffect(() => {
|
||||
if (value && selectedData) {
|
||||
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("");
|
||||
setSelectedData(null);
|
||||
}
|
||||
}, [value, displayField]);
|
||||
}, [value, displayField, options, mode, valueField]);
|
||||
|
||||
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
|
||||
setSelectedData(fullData);
|
||||
setDisplayValue(fullData[displayField] || "");
|
||||
onChange?.(newValue, fullData);
|
||||
|
||||
// 🆕 onFormDataChange 호출 (formData에 값 저장)
|
||||
if (isInteractive && onFormDataChange && component?.columnName) {
|
||||
onFormDataChange(component.columnName, newValue);
|
||||
console.log("📤 EntitySearchInput -> onFormDataChange:", component.columnName, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setDisplayValue("");
|
||||
setSelectedData(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 = () => {
|
||||
|
|
@ -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 (
|
||||
<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">
|
||||
<Input
|
||||
value={displayValue}
|
||||
|
|
@ -68,7 +246,8 @@ export function EntitySearchInputComponent({
|
|||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
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 && (
|
||||
<Button
|
||||
|
|
@ -83,12 +262,14 @@ export function EntitySearchInputComponent({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* 모달 버튼: modal 또는 combo 모드일 때만 표시 */}
|
||||
{(mode === "modal" || mode === "combo") && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleOpenModal}
|
||||
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" />
|
||||
</Button>
|
||||
|
|
@ -97,7 +278,7 @@ export function EntitySearchInputComponent({
|
|||
|
||||
{/* 추가 정보 표시 */}
|
||||
{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) => (
|
||||
<div key={field} className="flex gap-2">
|
||||
<span className="font-medium">{field}:</span>
|
||||
|
|
@ -107,19 +288,21 @@ export function EntitySearchInputComponent({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 검색 모달 */}
|
||||
<EntitySearchModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
tableName={tableName}
|
||||
displayField={displayField}
|
||||
valueField={valueField}
|
||||
searchFields={searchFields}
|
||||
filterCondition={filterCondition}
|
||||
modalTitle={modalTitle}
|
||||
modalColumns={modalColumns}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
{/* 검색 모달: modal 또는 combo 모드일 때만 렌더링 */}
|
||||
{(mode === "modal" || mode === "combo") && (
|
||||
<EntitySearchModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
tableName={tableName}
|
||||
displayField={displayField}
|
||||
valueField={valueField}
|
||||
searchFields={searchFields}
|
||||
filterCondition={filterCondition}
|
||||
modalTitle={modalTitle}
|
||||
modalColumns={modalColumns}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ export function EntitySearchInputConfigPanel({
|
|||
<Label className="text-xs sm:text-sm">UI 모드</Label>
|
||||
<Select
|
||||
value={localConfig.mode || "combo"}
|
||||
onValueChange={(value: "autocomplete" | "modal" | "combo") =>
|
||||
onValueChange={(value: "select" | "autocomplete" | "modal" | "combo") =>
|
||||
updateConfig({ mode: value })
|
||||
}
|
||||
>
|
||||
|
|
@ -310,11 +310,18 @@ export function EntitySearchInputConfigPanel({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="select">드롭다운 (검색 가능)</SelectItem>
|
||||
<SelectItem value="combo">콤보 (입력 + 모달)</SelectItem>
|
||||
<SelectItem value="modal">모달만</SelectItem>
|
||||
<SelectItem value="autocomplete">자동완성만</SelectItem>
|
||||
</SelectContent>
|
||||
</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 className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export interface EntitySearchInputConfig {
|
|||
valueField: string;
|
||||
searchFields?: string[];
|
||||
filterCondition?: Record<string, any>;
|
||||
mode?: "autocomplete" | "modal" | "combo";
|
||||
mode?: "select" | "autocomplete" | "modal" | "combo";
|
||||
placeholder?: string;
|
||||
modalTitle?: string;
|
||||
modalColumns?: string[];
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ export interface EntitySearchInputProps {
|
|||
searchFields?: string[]; // 검색 대상 필드들 (기본: [displayField])
|
||||
|
||||
// UI 모드
|
||||
mode?: "autocomplete" | "modal" | "combo"; // 기본: "combo"
|
||||
// - select: 드롭다운 선택 (검색 가능한 콤보박스)
|
||||
// - modal: 모달 팝업에서 선택
|
||||
// - combo: 입력 + 모달 버튼 (기본)
|
||||
// - autocomplete: 입력하면서 자동완성
|
||||
mode?: "select" | "autocomplete" | "modal" | "combo"; // 기본: "combo"
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
|
||||
|
|
@ -33,6 +37,7 @@ export interface EntitySearchInputProps {
|
|||
|
||||
// 스타일
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface EntitySearchResult {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ export type ButtonActionType =
|
|||
// | "empty_vehicle" // 공차등록 (위치 수집 + 상태 변경) - 운행알림으로 통합
|
||||
| "operation_control" // 운행알림 및 종료 (위치 수집 + 상태 변경 + 연속 추적)
|
||||
| "swap_fields" // 필드 값 교환 (출발지 ↔ 목적지)
|
||||
| "transferData"; // 데이터 전달 (컴포넌트 간 or 화면 간)
|
||||
| "transferData" // 데이터 전달 (컴포넌트 간 or 화면 간)
|
||||
| "quickInsert"; // 즉시 저장 (선택한 데이터를 특정 테이블에 즉시 INSERT)
|
||||
|
||||
/**
|
||||
* 버튼 액션 설정
|
||||
|
|
@ -211,6 +212,31 @@ export interface ButtonActionConfig {
|
|||
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>;
|
||||
|
||||
// 🆕 분할 패널 컨텍스트 (quickInsert 등에서 좌측 패널 데이터 접근용)
|
||||
splitPanelContext?: {
|
||||
selectedLeftData?: Record<string, any>;
|
||||
refreshRightPanel?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -365,6 +397,9 @@ export class ButtonActionExecutor {
|
|||
case "swap_fields":
|
||||
return await this.handleSwapFields(config, context);
|
||||
|
||||
case "quickInsert":
|
||||
return await this.handleQuickInsert(config, context);
|
||||
|
||||
default:
|
||||
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
|
||||
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로 변경)
|
||||
* 🆕 위치정보 수집 기능 추가
|
||||
|
|
@ -5643,4 +5985,9 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
|
|||
successMessage: "필드 값이 교환되었습니다.",
|
||||
errorMessage: "필드 값 교환 중 오류가 발생했습니다.",
|
||||
},
|
||||
quickInsert: {
|
||||
type: "quickInsert",
|
||||
successMessage: "저장되었습니다.",
|
||||
errorMessage: "저장 중 오류가 발생했습니다.",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export const WEB_TYPE_COMPONENT_MAPPING: Record<string, string> = {
|
|||
// 기타
|
||||
label: "text-display",
|
||||
code: "select-basic", // 코드 타입은 선택상자 사용
|
||||
entity: "select-basic", // 엔티티 타입은 선택상자 사용
|
||||
entity: "entity-search-input", // 엔티티 타입은 전용 검색 입력 사용
|
||||
category: "select-basic", // 카테고리 타입은 선택상자 사용
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -363,6 +363,8 @@ export interface EntityTypeConfig {
|
|||
placeholder?: string;
|
||||
displayFormat?: "simple" | "detailed" | "custom"; // 표시 형식
|
||||
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
||||
// UI 모드
|
||||
uiMode?: "select" | "modal" | "combo" | "autocomplete"; // 기본: "combo"
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -428,6 +430,111 @@ export interface ButtonTypeConfig {
|
|||
// 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"
|
||||
// 데이터 전달
|
||||
| "transferData"; // 선택된 데이터를 다른 컴포넌트/화면으로 전달
|
||||
| "transferData" // 선택된 데이터를 다른 컴포넌트/화면으로 전달
|
||||
// 즉시 저장
|
||||
| "quickInsert"; // 선택한 데이터를 특정 테이블에 즉시 INSERT
|
||||
|
||||
/**
|
||||
* 컴포넌트 타입 정의
|
||||
|
|
@ -328,6 +330,7 @@ export const isButtonActionType = (value: string): value is ButtonActionType =>
|
|||
"newWindow",
|
||||
"control",
|
||||
"transferData",
|
||||
"quickInsert",
|
||||
];
|
||||
return actionTypes.includes(value as ButtonActionType);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue