엔티티 즉시저장기능 추가

This commit is contained in:
kjs
2025-12-16 14:38:03 +09:00
parent d8329d31e4
commit f7e3c1924c
17 changed files with 1969 additions and 34 deletions

View File

@@ -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: "저장 중 오류가 발생했습니다.",
},
};