엔티티 즉시저장기능 추가
This commit is contained in:
@@ -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: "저장 중 오류가 발생했습니다.",
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user