Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into ycshin-node
This commit is contained in:
@@ -40,6 +40,8 @@ export interface MenuItem {
|
||||
TRANSLATED_NAME?: string;
|
||||
translated_desc?: string;
|
||||
TRANSLATED_DESC?: string;
|
||||
menu_icon?: string;
|
||||
MENU_ICON?: string;
|
||||
}
|
||||
|
||||
export interface MenuFormData {
|
||||
@@ -52,8 +54,9 @@ export interface MenuFormData {
|
||||
menuType: string;
|
||||
status: string;
|
||||
companyCode: string;
|
||||
langKey?: string; // 다국어 키 추가
|
||||
screenCode?: string; // 화면 코드 추가
|
||||
langKey?: string;
|
||||
screenCode?: string;
|
||||
menuIcon?: string;
|
||||
}
|
||||
|
||||
export interface LangKey {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
@@ -954,7 +955,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ 데이터 전달 실패:", error);
|
||||
toast.error(error.message || "데이터 전달 중 오류가 발생했습니다.");
|
||||
showErrorToast("데이터 전달에 실패했습니다", error, { guidance: "대상 화면 설정과 데이터를 확인해 주세요." });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { uploadFiles, downloadFile, deleteFile, getComponentFiles } from "@/lib/api/file";
|
||||
import { GlobalFileManager } from "@/lib/api/globalFile";
|
||||
import { formatFileSize } from "@/lib/utils";
|
||||
@@ -881,7 +882,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
console.error("파일 업로드 오류:", error);
|
||||
setUploadStatus("error");
|
||||
toast.dismiss("file-upload");
|
||||
toast.error(`파일 업로드 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`);
|
||||
showErrorToast("파일 업로드에 실패했습니다", error, { guidance: "파일 크기와 형식을 확인하고 다시 시도해 주세요." });
|
||||
}
|
||||
},
|
||||
[safeComponentConfig, uploadedFiles, onFormDataChange, component.columnName, component.id, formData],
|
||||
@@ -1006,7 +1007,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
toast.success(`${fileName} 삭제 완료`);
|
||||
} catch (error) {
|
||||
console.error("파일 삭제 오류:", error);
|
||||
toast.error("파일 삭제에 실패했습니다.");
|
||||
showErrorToast("파일 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
|
||||
}
|
||||
},
|
||||
[uploadedFiles, onUpdate, component.id, isRecordMode, onFormDataChange, recordTableName, recordId, columnName, getUniqueKey],
|
||||
|
||||
@@ -45,6 +45,7 @@ import { FileText, ChevronRightIcon, Search } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { tableDisplayStore } from "@/stores/tableDisplayStore";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -2491,7 +2492,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
console.log("✅ 배치 저장 완료:", pendingChanges.size, "개");
|
||||
} catch (error) {
|
||||
console.error("❌ 배치 저장 실패:", error);
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
showErrorToast("데이터 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." });
|
||||
}
|
||||
}, [pendingChanges, tableConfig.selectedTable, tableConfig.primaryKey]);
|
||||
|
||||
@@ -2709,7 +2710,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
console.log("✅ Excel 내보내기 완료:", fileName);
|
||||
} catch (error) {
|
||||
console.error("❌ Excel 내보내기 실패:", error);
|
||||
toast.error("Excel 내보내기 중 오류가 발생했습니다.");
|
||||
showErrorToast("Excel 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." });
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -3623,7 +3624,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
console.log("✅ 행 순서 변경:", { from: draggedRowIndex, to: targetIndex });
|
||||
} catch (error) {
|
||||
console.error("❌ 행 순서 변경 실패:", error);
|
||||
toast.error("순서 변경 중 오류가 발생했습니다.");
|
||||
showErrorToast("행 순서 변경에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
|
||||
}
|
||||
|
||||
handleRowDragEnd();
|
||||
@@ -3737,7 +3738,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ PDF 내보내기 실패:", error);
|
||||
toast.error("PDF 내보내기 중 오류가 발생했습니다.");
|
||||
showErrorToast("PDF 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." });
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -6644,7 +6645,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
handleRefresh();
|
||||
} catch (error) {
|
||||
console.error("삭제 오류:", error);
|
||||
toast.error("삭제 중 오류가 발생했습니다");
|
||||
showErrorToast("데이터 삭제에 실패했습니다", error, { guidance: "삭제 대상을 확인하고 다시 시도해 주세요." });
|
||||
}
|
||||
}
|
||||
closeContextMenu();
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
@@ -1068,7 +1069,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ 데이터 전달 실패:", error);
|
||||
toast.error(error.message || "데이터 전달 중 오류가 발생했습니다.");
|
||||
showErrorToast("데이터 전달에 실패했습니다", error, { guidance: "대상 화면 설정과 데이터를 확인해 주세요." });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -181,6 +181,7 @@ import { FileText, ChevronRightIcon } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { tableDisplayStore } from "@/stores/tableDisplayStore";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -2577,7 +2578,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
|
||||
toast.success(`${pendingChanges.size}개의 변경사항이 저장되었습니다.`);
|
||||
} catch (error) {
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
showErrorToast("데이터 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." });
|
||||
}
|
||||
}, [pendingChanges, tableConfig.selectedTable, tableConfig.primaryKey]);
|
||||
|
||||
@@ -2765,7 +2766,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
|
||||
toast.success(`${exportData.length}개 행이 Excel로 내보내기 되었습니다.`);
|
||||
} catch (error) {
|
||||
toast.error("Excel 내보내기 중 오류가 발생했습니다.");
|
||||
showErrorToast("Excel 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." });
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -3216,7 +3217,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
|
||||
toast.success(`${copyData.length}행 복사됨`);
|
||||
} catch (error) {
|
||||
toast.error("복사 실패");
|
||||
showErrorToast("클립보드 복사에 실패했습니다", error, { guidance: "브라우저 권한을 확인해 주세요." });
|
||||
}
|
||||
}, [selectedRows, filteredData, focusedCell, visibleColumns, columnLabels, getRowKey]);
|
||||
|
||||
@@ -3771,7 +3772,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ PDF 내보내기 실패:", error);
|
||||
toast.error("PDF 내보내기 중 오류가 발생했습니다.");
|
||||
showErrorToast("PDF 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." });
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -4519,7 +4520,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
setSearchValues({});
|
||||
} catch (error) {
|
||||
console.error("필터 설정 저장 실패:", error);
|
||||
toast.error("설정 저장에 실패했습니다");
|
||||
showErrorToast("필터 설정 저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
|
||||
}
|
||||
}, [filterSettingKey, visibleFilterColumns]);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { dataApi } from "@/lib/api/data";
|
||||
import { executePopAction } from "@/hooks/pop/executePopAction";
|
||||
import { usePopEvent } from "@/hooks/pop/usePopEvent";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import type {
|
||||
PopStringListConfig,
|
||||
CardGridConfig,
|
||||
@@ -146,10 +147,10 @@ export function PopStringListComponent({
|
||||
if (result.success) {
|
||||
toast.success("작업이 완료되었습니다.");
|
||||
} else {
|
||||
toast.error(result.error || "작업에 실패했습니다.");
|
||||
showErrorToast("작업에 실패했습니다", result.error, { guidance: "잠시 후 다시 시도해 주세요." });
|
||||
}
|
||||
} catch {
|
||||
toast.error("알 수 없는 오류가 발생했습니다.");
|
||||
showErrorToast("예기치 않은 오류가 발생했습니다", null, { guidance: "잠시 후 다시 시도해 주세요." });
|
||||
} finally {
|
||||
setLoadingRowIdx(-1);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { DynamicFormApi } from "@/lib/api/dynamicForm";
|
||||
import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor";
|
||||
@@ -456,7 +457,11 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("버튼 액션 실행 오류:", error);
|
||||
toast.error(config.errorMessage || "작업 중 오류가 발생했습니다.");
|
||||
showErrorToast(
|
||||
config.errorMessage || `'${config.label || config.type}' 버튼 실행에 실패했습니다`,
|
||||
error,
|
||||
{ guidance: "설정을 확인하거나 잠시 후 다시 시도해 주세요." }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2652,7 +2657,9 @@ export class ButtonActionExecutor {
|
||||
return { handled: true, success: true };
|
||||
} catch (error: any) {
|
||||
console.error("❌ [handleUniversalFormModalTableSectionSave] 저장 오류:", error);
|
||||
toast.error(error.message || "저장 중 오류가 발생했습니다.");
|
||||
showErrorToast("테이블 섹션 데이터 저장에 실패했습니다", error, {
|
||||
guidance: "입력값을 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
return { handled: true, success: false };
|
||||
}
|
||||
}
|
||||
@@ -2894,7 +2901,9 @@ export class ButtonActionExecutor {
|
||||
if (failCount === 0) {
|
||||
toast.success(`${successCount}개 항목이 저장되었습니다.`);
|
||||
} else if (successCount === 0) {
|
||||
toast.error(`저장 실패: ${errors.join(", ")}`);
|
||||
showErrorToast(`${errors.length}개 항목 저장에 모두 실패했습니다`, errors.join("\n"), {
|
||||
guidance: "입력 데이터를 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
toast.warning(`${successCount}개 성공, ${failCount}개 실패: ${errors.join(", ")}`);
|
||||
@@ -2911,7 +2920,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error("배치 저장 오류:", error);
|
||||
toast.error(`저장 오류: ${error.message}`);
|
||||
showErrorToast("배치 저장 중 오류가 발생했습니다", error, {
|
||||
guidance: "저장 대상 데이터를 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3263,7 +3274,9 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 데이터 확인 실패:", error);
|
||||
toast.error("데이터 확인 중 오류가 발생했습니다.");
|
||||
showErrorToast("상위 데이터 조회에 실패했습니다", error, {
|
||||
guidance: "데이터 소스 연결 상태를 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -3436,7 +3449,9 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 데이터 확인 실패:", error);
|
||||
toast.error("데이터 확인 중 오류가 발생했습니다.");
|
||||
showErrorToast("모달 데이터 확인에 실패했습니다", error, {
|
||||
guidance: "데이터 소스를 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3997,7 +4012,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error("❌ 복사 액션 실행 중 오류:", error);
|
||||
toast.error(`복사 중 오류가 발생했습니다: ${error.message || "알 수 없는 오류"}`);
|
||||
showErrorToast("데이터 복사에 실패했습니다", error, {
|
||||
guidance: "복사 대상 데이터를 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4228,12 +4245,18 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} else {
|
||||
console.error("❌ 노드 플로우 실행 실패:", result);
|
||||
toast.error(config.errorMessage || result.message || "플로우 실행 중 오류가 발생했습니다.");
|
||||
showErrorToast(
|
||||
config.errorMessage || "플로우 실행에 실패했습니다",
|
||||
result.message,
|
||||
{ guidance: "플로우 설정과 데이터를 확인해 주세요." }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 노드 플로우 실행 오류:", error);
|
||||
toast.error("플로우 실행 중 오류가 발생했습니다.");
|
||||
showErrorToast("플로우 실행 중 오류가 발생했습니다", error, {
|
||||
guidance: "플로우 연결 상태와 데이터를 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} else if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) {
|
||||
@@ -4277,7 +4300,11 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} else {
|
||||
console.error("❌ 관계 실행 실패:", executionResult);
|
||||
toast.error(config.errorMessage || "관계 실행 중 오류가 발생했습니다.");
|
||||
showErrorToast(
|
||||
config.errorMessage || "관계 실행에 실패했습니다",
|
||||
executionResult.message || executionResult.error,
|
||||
{ guidance: "관계 설정과 데이터를 확인해 주세요." }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -4292,7 +4319,9 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("제어 조건 검증 중 오류:", error);
|
||||
toast.error("제어 조건 검증 중 오류가 발생했습니다.");
|
||||
showErrorToast("제어 조건 검증에 실패했습니다", error, {
|
||||
guidance: "제어 설정을 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4420,7 +4449,12 @@ export class ButtonActionExecutor {
|
||||
if (allSuccess) {
|
||||
toast.success(`${successCount}개 제어 실행 완료`);
|
||||
} else {
|
||||
toast.error(`제어 실행 중 오류 발생 (${successCount}/${results.length} 성공)`);
|
||||
const failedNames = results.filter((r) => !r.success).map((r) => r.flowName || r.flowId).join(", ");
|
||||
showErrorToast(
|
||||
`제어 실행 중 일부 실패 (${successCount}/${results.length} 성공)`,
|
||||
failedNames ? `실패 항목: ${failedNames}` : undefined,
|
||||
{ guidance: "실패한 제어 플로우 설정을 확인해 주세요." }
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -4486,11 +4520,15 @@ export class ButtonActionExecutor {
|
||||
toast.success("제어 로직 실행이 완료되었습니다.");
|
||||
} else {
|
||||
console.error("❌ 저장 후 노드 플로우 실행 실패:", result);
|
||||
toast.error("저장은 완료되었으나 제어 실행 중 오류가 발생했습니다.");
|
||||
showErrorToast("저장은 완료되었으나 후속 제어 실행에 실패했습니다", result.message, {
|
||||
guidance: "제어 플로우 설정을 확인해 주세요. 데이터는 정상 저장되었습니다.",
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ 저장 후 노드 플로우 실행 오류:", error);
|
||||
toast.error(`제어 실행 오류: ${error.message || "알 수 없는 오류"}`);
|
||||
showErrorToast("저장은 완료되었으나 후속 제어 실행에 실패했습니다", error, {
|
||||
guidance: "제어 플로우 설정을 확인해 주세요. 데이터는 정상 저장되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return; // 노드 플로우 실행 후 종료
|
||||
@@ -4521,7 +4559,9 @@ export class ButtonActionExecutor {
|
||||
// 성공 토스트는 save 액션에서 이미 표시했으므로 추가로 표시하지 않음
|
||||
} else {
|
||||
console.error("❌ 저장 후 제어 실행 실패:", executionResult);
|
||||
toast.error("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
|
||||
showErrorToast("저장은 완료되었으나 연결된 제어 실행에 실패했습니다", executionResult.message || executionResult.error, {
|
||||
guidance: "제어 관계 설정을 확인해 주세요. 데이터는 정상 저장되었습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4565,9 +4605,10 @@ export class ButtonActionExecutor {
|
||||
const actionType = action.actionType || action.type;
|
||||
console.error(`❌ 액션 ${i + 1}/${actions.length} 실행 실패:`, action.name, error);
|
||||
|
||||
// 실패 토스트
|
||||
toast.error(
|
||||
`${action.name || `액션 ${i + 1}`} 실행 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
|
||||
showErrorToast(
|
||||
`'${action.name || `액션 ${i + 1}`}' 실행에 실패했습니다`,
|
||||
error,
|
||||
{ guidance: `전체 ${actions.length}개 액션 중 ${i + 1}번째에서 중단되었습니다.` }
|
||||
);
|
||||
|
||||
// 🚨 순차 실행 중단: 하나라도 실패하면 전체 중단
|
||||
@@ -4635,9 +4676,11 @@ export class ButtonActionExecutor {
|
||||
} else {
|
||||
throw new Error(result.message || "저장 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("❌ 저장 실패:", error);
|
||||
toast.error(`저장 실패: ${error.message}`);
|
||||
showErrorToast(`'${context.tableName}' 테이블 저장에 실패했습니다`, error, {
|
||||
guidance: "입력 데이터를 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -4724,9 +4767,11 @@ export class ButtonActionExecutor {
|
||||
} else {
|
||||
throw new Error(result.message || "업데이트 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("❌ 업데이트 실패:", error);
|
||||
toast.error(`업데이트 실패: ${error.message}`);
|
||||
showErrorToast(`'${context.tableName}' 데이터 수정에 실패했습니다`, error, {
|
||||
guidance: "수정 데이터를 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -4780,9 +4825,11 @@ export class ButtonActionExecutor {
|
||||
} else {
|
||||
throw new Error(result.message || "삭제 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("❌ 삭제 실패:", error);
|
||||
toast.error(`삭제 실패: ${error.message}`);
|
||||
showErrorToast(`'${context.tableName}' 데이터 삭제에 실패했습니다`, error, {
|
||||
guidance: "삭제 대상을 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -4863,7 +4910,9 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 삽입 실패:", error);
|
||||
toast.error(`삽입 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`);
|
||||
showErrorToast("데이터 삽입에 실패했습니다", error, {
|
||||
guidance: "필수 입력 항목과 데이터 형식을 확인해 주세요.",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -4964,7 +5013,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ 이력 모달 열기 실패:", error);
|
||||
toast.error("이력 조회 중 오류가 발생했습니다.");
|
||||
showErrorToast("이력 조회에 실패했습니다", error, {
|
||||
guidance: "이력 테이블 설정을 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -5004,7 +5055,9 @@ export class ButtonActionExecutor {
|
||||
columnLabels![col] = downloadResponse.data.headers[index] || col;
|
||||
});
|
||||
} else {
|
||||
toast.error("마스터-디테일 데이터 조회에 실패했습니다.");
|
||||
showErrorToast("마스터-디테일 데이터 조회에 실패했습니다", null, {
|
||||
guidance: "데이터 소스 설정을 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5094,12 +5147,16 @@ export class ButtonActionExecutor {
|
||||
dataToExport = response.data;
|
||||
} else {
|
||||
console.error("❌ 예상치 못한 응답 형식:", response);
|
||||
toast.error("데이터를 가져오는데 실패했습니다.");
|
||||
showErrorToast("엑셀 데이터 조회에 실패했습니다", null, {
|
||||
guidance: "서버 응답 형식이 예상과 다릅니다. 관리자에게 문의해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("엑셀 다운로드: 데이터 조회 실패:", error);
|
||||
toast.error("데이터를 가져오는데 실패했습니다.");
|
||||
showErrorToast("엑셀 다운로드용 데이터 조회에 실패했습니다", error, {
|
||||
guidance: "네트워크 연결을 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -5109,7 +5166,9 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
// 테이블명도 없고 폼 데이터도 없으면 에러
|
||||
else {
|
||||
toast.error("다운로드할 데이터 소스가 없습니다.");
|
||||
toast.error("다운로드할 데이터 소스가 없습니다.", {
|
||||
description: "테이블 또는 폼 데이터가 설정되어 있지 않습니다. 버튼 설정을 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5118,13 +5177,17 @@ export class ButtonActionExecutor {
|
||||
if (typeof dataToExport === "object" && dataToExport !== null) {
|
||||
dataToExport = [dataToExport];
|
||||
} else {
|
||||
toast.error("다운로드할 데이터 형식이 올바르지 않습니다.");
|
||||
toast.error("다운로드할 데이터 형식이 올바르지 않습니다.", {
|
||||
description: "서버에서 받은 데이터 형식이 예상과 다릅니다. 관리자에게 문의해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (dataToExport.length === 0) {
|
||||
toast.error("다운로드할 데이터가 없습니다.");
|
||||
toast.error("다운로드할 데이터가 없습니다.", {
|
||||
description: "조회 조건에 맞는 데이터가 없습니다. 검색 조건을 변경해 보세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5399,7 +5462,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ 엑셀 다운로드 실패:", error);
|
||||
toast.error(config.errorMessage || "엑셀 다운로드 중 오류가 발생했습니다.");
|
||||
showErrorToast(config.errorMessage || "엑셀 파일 다운로드에 실패했습니다", error, {
|
||||
guidance: "데이터를 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -5503,7 +5568,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ 엑셀 업로드 모달 열기 실패:", error);
|
||||
toast.error(config.errorMessage || "엑셀 업로드 중 오류가 발생했습니다.");
|
||||
showErrorToast(config.errorMessage || "엑셀 업로드 화면을 열 수 없습니다", error, {
|
||||
guidance: "잠시 후 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -5564,7 +5631,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ 바코드 스캔 모달 열기 실패:", error);
|
||||
toast.error("바코드 스캔 중 오류가 발생했습니다.");
|
||||
showErrorToast("바코드 스캔 화면을 열 수 없습니다", error, {
|
||||
guidance: "카메라 권한을 확인하고 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -5744,13 +5813,15 @@ export class ButtonActionExecutor {
|
||||
|
||||
return true;
|
||||
} else {
|
||||
toast.error(response.data.message || "코드 병합에 실패했습니다.");
|
||||
showErrorToast("코드 병합에 실패했습니다", response.data.message, {
|
||||
guidance: "병합 대상 코드를 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ 코드 병합 실패:", error);
|
||||
toast.dismiss();
|
||||
toast.error(error.response?.data?.message || "코드 병합 중 오류가 발생했습니다.");
|
||||
showErrorToast("코드 병합 중 오류가 발생했습니다", error.response?.data?.message || error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -5877,7 +5948,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error("❌ 위치 추적 시작 실패:", error);
|
||||
toast.error(config.errorMessage || "위치 추적 시작 중 오류가 발생했습니다.");
|
||||
showErrorToast(config.errorMessage || "위치 추적을 시작할 수 없습니다", error, {
|
||||
guidance: "위치 권한 설정과 GPS 상태를 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6131,7 +6204,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error("❌ 위치 추적 종료 실패:", error);
|
||||
toast.error(config.errorMessage || "위치 추적 종료 중 오류가 발생했습니다.");
|
||||
showErrorToast(config.errorMessage || "위치 추적 종료에 실패했습니다", error, {
|
||||
guidance: "잠시 후 다시 시도해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6425,7 +6500,9 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ 데이터 전달 실패:", error);
|
||||
toast.error(error.message || "데이터 전달 중 오류가 발생했습니다.");
|
||||
showErrorToast("데이터 전달에 실패했습니다", error, {
|
||||
guidance: "대상 화면 설정과 데이터를 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6555,7 +6632,9 @@ export class ButtonActionExecutor {
|
||||
toast.success(config.successMessage || "공차 등록이 완료되었습니다. 위치 추적을 시작합니다.");
|
||||
} catch (saveError) {
|
||||
console.error("❌ 위치정보 자동 저장 실패:", saveError);
|
||||
toast.error("위치 정보 저장에 실패했습니다.");
|
||||
showErrorToast("위치 정보 저장에 실패했습니다", saveError, {
|
||||
guidance: "네트워크 연결을 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -6585,10 +6664,14 @@ export class ButtonActionExecutor {
|
||||
toast.error("위치 정보 요청 시간이 초과되었습니다.\n다시 시도해주세요.");
|
||||
break;
|
||||
default:
|
||||
toast.error(config.errorMessage || "위치 정보를 가져오는 중 오류가 발생했습니다.");
|
||||
showErrorToast(config.errorMessage || "위치 정보를 가져올 수 없습니다", null, {
|
||||
guidance: "브라우저 설정에서 위치 권한을 확인해 주세요.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
toast.error(config.errorMessage || "위치 정보를 가져오는 중 오류가 발생했습니다.");
|
||||
showErrorToast(config.errorMessage || "위치 정보를 가져올 수 없습니다", error, {
|
||||
guidance: "브라우저 설정에서 위치 권한을 확인해 주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -6760,7 +6843,9 @@ export class ButtonActionExecutor {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ 필드 값 교환 오류:", error);
|
||||
toast.error(config.errorMessage || "값 교환 중 오류가 발생했습니다.");
|
||||
showErrorToast(config.errorMessage || "필드 값 교환에 실패했습니다", error, {
|
||||
guidance: "교환 대상 필드 설정을 확인해 주세요.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { ExtendedButtonTypeConfig, ButtonDataflowConfig } from "@/types/control-management";
|
||||
import { ButtonActionType } from "@/types/v2-core";
|
||||
@@ -383,7 +384,9 @@ export class ImprovedButtonActionExecutor {
|
||||
if (result.success) {
|
||||
toast.success(`관계 '${config.relationshipName}' 실행 완료`);
|
||||
} else {
|
||||
toast.error(`관계 '${config.relationshipName}' 실행 실패: ${result.message}`);
|
||||
showErrorToast(`관계 '${config.relationshipName}' 실행에 실패했습니다`, result.message, {
|
||||
guidance: "관계 설정과 대상 데이터를 확인해 주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -396,7 +399,9 @@ export class ImprovedButtonActionExecutor {
|
||||
error: error.message,
|
||||
};
|
||||
|
||||
toast.error(errorResult.message);
|
||||
showErrorToast(`관계 '${config.relationshipName}' 실행에 실패했습니다`, error, {
|
||||
guidance: "관계 설정과 연결 상태를 확인해 주세요.",
|
||||
});
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
@@ -1057,7 +1062,8 @@ export class ImprovedButtonActionExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// 오류 토스트 표시
|
||||
toast.error(error.message || "작업 중 오류가 발생했습니다.");
|
||||
showErrorToast("버튼 액션 실행 중 오류가 발생했습니다", error, {
|
||||
guidance: "잠시 후 다시 시도해 주세요. 문제가 계속되면 관리자에게 문의해 주세요.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
82
frontend/lib/utils/toastUtils.ts
Normal file
82
frontend/lib/utils/toastUtils.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* 서버/catch 에러에서 사용자에게 보여줄 메시지를 추출
|
||||
*/
|
||||
function extractErrorMessage(error: unknown): string | null {
|
||||
if (!error) return null;
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const obj = error as Record<string, any>;
|
||||
return (
|
||||
obj.response?.data?.message ||
|
||||
obj.response?.data?.error ||
|
||||
obj.message ||
|
||||
obj.error ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 친절한 에러 토스트를 표시합니다.
|
||||
*
|
||||
* @param title - 어떤 작업에서 문제가 발생했는지 (예: "메뉴 저장에 실패했습니다")
|
||||
* @param error - catch 블록의 error 객체 또는 에러 메시지 문자열
|
||||
* @param options - 추가 옵션
|
||||
* @param options.guidance - 사용자에게 안내할 해결 방법 (예: "네트워크 연결을 확인해 주세요")
|
||||
* @param options.duration - 토스트 표시 시간 (ms)
|
||||
*/
|
||||
export function showErrorToast(
|
||||
title: string,
|
||||
error?: unknown,
|
||||
options?: {
|
||||
guidance?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
const errorMessage = extractErrorMessage(error);
|
||||
const guidance = options?.guidance;
|
||||
|
||||
const descriptionParts: string[] = [];
|
||||
if (errorMessage) descriptionParts.push(errorMessage);
|
||||
if (guidance) descriptionParts.push(guidance);
|
||||
|
||||
const description =
|
||||
descriptionParts.length > 0 ? descriptionParts.join("\n") : undefined;
|
||||
|
||||
toast.error(title, {
|
||||
description,
|
||||
duration: options?.duration || 5000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 기반 에러 토스트
|
||||
* API 호출 실패 시 응답 메시지를 포함하여 친절하게 표시합니다.
|
||||
*/
|
||||
export function showApiErrorToast(
|
||||
action: string,
|
||||
response?: { message?: string; error?: string } | null,
|
||||
fallbackError?: unknown
|
||||
) {
|
||||
const apiMessage = response?.message || response?.error;
|
||||
const errorMessage = apiMessage || extractErrorMessage(fallbackError);
|
||||
|
||||
const description = errorMessage || "잠시 후 다시 시도해 주세요.";
|
||||
|
||||
toast.error(`${action}에 실패했습니다`, {
|
||||
description,
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { V2_EVENTS } from "../events/types";
|
||||
import type { ScheduleType, V2ScheduleGenerateRequestEvent, V2ScheduleGenerateApplyEvent } from "../events/types";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
|
||||
// ============================================================================
|
||||
// 타입 정의
|
||||
@@ -230,7 +231,8 @@ export function useScheduleGenerator(scheduleConfig?: ScheduleGenerationConfig |
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[ScheduleGeneratorService] 미리보기 오류:", error);
|
||||
toast.error("스케줄 생성 중 오류가 발생했습니다.", { id: "schedule-generate" });
|
||||
toast.dismiss("schedule-generate");
|
||||
showErrorToast("스케줄 미리보기 생성에 실패했습니다", error, { guidance: "스케줄 설정을 확인하고 다시 시도해 주세요." });
|
||||
v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, {
|
||||
requestId: payload.requestId,
|
||||
error: error.message,
|
||||
@@ -295,7 +297,8 @@ export function useScheduleGenerator(scheduleConfig?: ScheduleGenerationConfig |
|
||||
setPreviewResult(null);
|
||||
} catch (error: any) {
|
||||
console.error("[ScheduleGeneratorService] 적용 오류:", error);
|
||||
toast.error("스케줄 적용 중 오류가 발생했습니다.", { id: "schedule-apply" });
|
||||
toast.dismiss("schedule-apply");
|
||||
showErrorToast("스케줄 적용에 실패했습니다", error, { guidance: "스케줄 설정과 데이터를 확인하고 다시 시도해 주세요." });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user