From f04d224b098bd43b8edc2da821d7a35e62f9674f Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 3 Mar 2026 16:04:11 +0900 Subject: [PATCH] feat: Enhance error handling with showErrorToast utility - Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting. - Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios. - Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability. --- .../automaticMng/batchmngList/create/page.tsx | 5 +- .../admin/automaticMng/batchmngList/page.tsx | 5 +- .../automaticMng/exCallConfList/page.tsx | 21 ++- .../admin/batch-management-new/page.tsx | 9 +- .../(main)/admin/batch-management/page.tsx | 6 +- .../cascading-management/tabs/AutoFillTab.tsx | 5 +- .../tabs/CascadingRelationsTab.tsx | 7 +- .../tabs/ConditionTab.tsx | 5 +- .../tabs/HierarchyColumnTab.tsx | 5 +- .../admin/standards/[webType]/edit/page.tsx | 3 +- frontend/app/(main)/admin/standards/page.tsx | 3 +- .../collection-managementList/page.tsx | 5 +- .../(main)/admin/systemMng/dataflow/page.tsx | 3 +- .../admin/systemMng/tableMngList/page.tsx | 71 +++++-- .../app/(main)/screens/[screenId]/page.tsx | 3 +- .../app/(pop)/pop/screens/[screenId]/page.tsx | 3 +- frontend/components/GlobalFileViewer.tsx | 3 +- .../components/admin/AdvancedBatchModal.tsx | 5 +- frontend/components/admin/DDLLogViewer.tsx | 5 +- .../admin/ExternalCallConfigModal.tsx | 3 +- frontend/components/admin/LayoutFormModal.tsx | 5 +- frontend/components/admin/MenuCopyDialog.tsx | 5 +- frontend/components/common/ScreenModal.tsx | 13 +- frontend/components/dataflow/DataFlowList.tsx | 7 +- .../external-call/ExternalCallTestPanel.tsx | 9 +- .../numbering-rule/NumberingRuleDesigner.tsx | 9 +- .../pop/management/PopScreenSettingModal.tsx | 3 +- .../screen/InteractiveScreenViewer.tsx | 3 +- .../components/screen/MenuAssignmentModal.tsx | 3 +- .../components/screen/NodeSettingModal.tsx | 27 +-- frontend/components/screen/SaveModal.tsx | 9 +- .../components/screen/ScreenGroupModal.tsx | 5 +- .../components/screen/ScreenGroupTreeView.tsx | 9 +- frontend/components/screen/ScreenList.tsx | 3 +- .../screen/SimpleScreenDesigner.tsx | 3 +- .../screen/panels/FieldJoinPanel.tsx | 5 +- .../panels/FileComponentConfigPanel.tsx | 9 +- .../components/screen/widgets/FlowWidget.tsx | 19 +- frontend/hooks/pop/usePopAction.ts | 3 +- .../button-primary/ButtonPrimaryComponent.tsx | 3 +- .../file-upload/FileUploadComponent.tsx | 5 +- .../table-list/TableListComponent.tsx | 11 +- .../ButtonPrimaryComponent.tsx | 3 +- .../v2-table-list/TableListComponent.tsx | 11 +- .../PopStringListComponent.tsx | 5 +- frontend/lib/utils/buttonActions.ts | 173 +++++++++++++----- .../lib/utils/improvedButtonActionExecutor.ts | 14 +- frontend/lib/utils/toastUtils.ts | 82 +++++++++ .../services/ScheduleGeneratorService.ts | 7 +- 49 files changed, 458 insertions(+), 180 deletions(-) create mode 100644 frontend/lib/utils/toastUtils.ts diff --git a/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx b/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx index 948cb669..1647523e 100644 --- a/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx +++ b/frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx @@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Badge } from "@/components/ui/badge"; import { ArrowLeft, Save, RefreshCw, ArrowRight, Trash2 } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { useRouter } from "next/navigation"; import { BatchAPI, @@ -133,7 +134,7 @@ export default function BatchCreatePage() { setFromColumns(Array.isArray(columns) ? columns : []); } catch (error) { console.error("FROM 컬럼 목록 로드 실패:", error); - toast.error("컬럼 목록을 불러오는데 실패했습니다."); + showErrorToast("컬럼 목록을 불러오는 데 실패했습니다", error, { guidance: "테이블 정보를 확인해 주세요." }); } }; @@ -242,7 +243,7 @@ export default function BatchCreatePage() { router.push("/admin/batchmng"); } catch (error) { console.error("배치 설정 저장 실패:", error); - toast.error("배치 설정 저장에 실패했습니다."); + showErrorToast("배치 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } finally { setLoading(false); } diff --git a/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx b/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx index 46aedf1f..a384b645 100644 --- a/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx +++ b/frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx @@ -10,6 +10,7 @@ import { Database } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { useRouter } from "next/navigation"; import { BatchAPI, @@ -75,7 +76,9 @@ export default function BatchManagementPage() { } } catch (error) { console.error("배치 실행 실패:", error); - toast.error("배치 실행 중 오류가 발생했습니다."); + showErrorToast("배치 실행에 실패했습니다", error, { + guidance: "배치 설정을 확인하고 다시 시도해 주세요.", + }); } finally { setExecutingBatch(null); } diff --git a/frontend/app/(main)/admin/automaticMng/exCallConfList/page.tsx b/frontend/app/(main)/admin/automaticMng/exCallConfList/page.tsx index 7e433ec7..f98a98d1 100644 --- a/frontend/app/(main)/admin/automaticMng/exCallConfList/page.tsx +++ b/frontend/app/(main)/admin/automaticMng/exCallConfList/page.tsx @@ -8,6 +8,7 @@ import { Badge } from "@/components/ui/badge"; import { Plus, Search, Edit, Trash2, TestTube, Filter } from "lucide-react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { ExternalCallConfigAPI, ExternalCallConfig, @@ -57,11 +58,15 @@ export default function ExternalCallConfigsPage() { if (response.success) { setConfigs(response.data || []); } else { - toast.error(response.message || "외부 호출 설정 조회 실패"); + showErrorToast("외부 호출 설정 조회에 실패했습니다", response.message, { + guidance: "네트워크 연결을 확인하고 다시 시도해 주세요.", + }); } } catch (error) { console.error("외부 호출 설정 조회 오류:", error); - toast.error("외부 호출 설정 조회 중 오류가 발생했습니다."); + showErrorToast("외부 호출 설정 조회에 실패했습니다", error, { + guidance: "네트워크 연결을 확인하고 다시 시도해 주세요.", + }); } finally { setLoading(false); } @@ -113,11 +118,15 @@ export default function ExternalCallConfigsPage() { toast.success("외부 호출 설정이 삭제되었습니다."); fetchConfigs(); } else { - toast.error(response.message || "외부 호출 설정 삭제 실패"); + showErrorToast("외부 호출 설정 삭제에 실패했습니다", response.message, { + guidance: "잠시 후 다시 시도해 주세요.", + }); } } catch (error) { console.error("외부 호출 설정 삭제 오류:", error); - toast.error("외부 호출 설정 삭제 중 오류가 발생했습니다."); + showErrorToast("외부 호출 설정 삭제에 실패했습니다", error, { + guidance: "잠시 후 다시 시도해 주세요.", + }); } finally { setDeleteDialogOpen(false); setConfigToDelete(null); @@ -138,7 +147,9 @@ export default function ExternalCallConfigsPage() { } } catch (error) { console.error("외부 호출 설정 테스트 오류:", error); - toast.error("외부 호출 설정 테스트 중 오류가 발생했습니다."); + showErrorToast("외부 호출 테스트 실행에 실패했습니다", error, { + guidance: "URL과 설정을 확인해 주세요.", + }); } }; diff --git a/frontend/app/(main)/admin/batch-management-new/page.tsx b/frontend/app/(main)/admin/batch-management-new/page.tsx index 3093ed10..6b282ed4 100644 --- a/frontend/app/(main)/admin/batch-management-new/page.tsx +++ b/frontend/app/(main)/admin/batch-management-new/page.tsx @@ -10,6 +10,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Trash2, Plus, ArrowLeft, Save, RefreshCw, Globe, Database, Eye } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { BatchManagementAPI } from "@/lib/api/batchManagement"; // 타입 정의 @@ -469,7 +470,9 @@ export default function BatchManagementNewPage() { } } catch (error) { console.error("배치 저장 오류:", error); - toast.error("배치 저장 중 오류가 발생했습니다."); + showErrorToast("배치 설정 저장에 실패했습니다", error, { + guidance: "입력 데이터를 확인하고 다시 시도해 주세요.", + }); } return; } else if (batchType === "db-to-restapi") { @@ -558,7 +561,9 @@ export default function BatchManagementNewPage() { } } catch (error) { console.error("배치 저장 오류:", error); - toast.error("배치 저장 중 오류가 발생했습니다."); + showErrorToast("배치 설정 저장에 실패했습니다", error, { + guidance: "입력 데이터를 확인하고 다시 시도해 주세요.", + }); } return; } diff --git a/frontend/app/(main)/admin/batch-management/page.tsx b/frontend/app/(main)/admin/batch-management/page.tsx index 5bb82a84..b9ed4230 100644 --- a/frontend/app/(main)/admin/batch-management/page.tsx +++ b/frontend/app/(main)/admin/batch-management/page.tsx @@ -75,7 +75,7 @@ export default function BatchManagementPage() { setJobs(data); } catch (error) { console.error("배치 작업 목록 조회 오류:", error); - toast.error("배치 작업 목록을 불러오는데 실패했습니다."); + showErrorToast("배치 작업 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setIsLoading(false); } @@ -150,7 +150,7 @@ export default function BatchManagementPage() { loadJobs(); } catch (error) { console.error("배치 작업 삭제 오류:", error); - toast.error("배치 작업 삭제에 실패했습니다."); + showErrorToast("배치 작업 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; @@ -160,7 +160,7 @@ export default function BatchManagementPage() { toast.success(`"${job.job_name}" 배치 작업을 실행했습니다.`); } catch (error) { console.error("배치 작업 실행 오류:", error); - toast.error("배치 작업 실행에 실패했습니다."); + showErrorToast("배치 작업 실행에 실패했습니다", error, { guidance: "배치 설정을 확인해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/cascading-management/tabs/AutoFillTab.tsx b/frontend/app/(main)/admin/cascading-management/tabs/AutoFillTab.tsx index 79208186..b93e1797 100644 --- a/frontend/app/(main)/admin/cascading-management/tabs/AutoFillTab.tsx +++ b/frontend/app/(main)/admin/cascading-management/tabs/AutoFillTab.tsx @@ -45,6 +45,7 @@ import { GripVertical, } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { cn } from "@/lib/utils"; import { cascadingAutoFillApi, AutoFillGroup, AutoFillMapping } from "@/lib/api/cascadingAutoFill"; import { tableManagementApi } from "@/lib/api/tableManagement"; @@ -97,7 +98,7 @@ export default function AutoFillTab() { } } catch (error) { console.error("그룹 목록 로드 실패:", error); - toast.error("그룹 목록을 불러오는데 실패했습니다."); + showErrorToast("그룹 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setLoading(false); } @@ -269,7 +270,7 @@ export default function AutoFillTab() { toast.error(response.error || "저장에 실패했습니다."); } } catch (error) { - toast.error("저장 중 오류가 발생했습니다."); + showErrorToast("자동입력 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/cascading-management/tabs/CascadingRelationsTab.tsx b/frontend/app/(main)/admin/cascading-management/tabs/CascadingRelationsTab.tsx index 943d9d84..9cdfc1c1 100644 --- a/frontend/app/(main)/admin/cascading-management/tabs/CascadingRelationsTab.tsx +++ b/frontend/app/(main)/admin/cascading-management/tabs/CascadingRelationsTab.tsx @@ -33,6 +33,7 @@ import { Loader2, } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { cn } from "@/lib/utils"; import { cascadingRelationApi, CascadingRelation, CascadingRelationCreateInput } from "@/lib/api/cascadingRelation"; import { tableManagementApi } from "@/lib/api/tableManagement"; @@ -102,7 +103,7 @@ export default function CascadingRelationsTab() { setRelations(response.data); } } catch (error) { - toast.error("연쇄 관계 목록 조회에 실패했습니다."); + showErrorToast("연쇄 관계 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setLoading(false); } @@ -431,7 +432,7 @@ export default function CascadingRelationsTab() { toast.error(response.message || "저장에 실패했습니다."); } } catch (error) { - toast.error("저장 중 오류가 발생했습니다."); + showErrorToast("연쇄 관계 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } finally { setSaving(false); } @@ -452,7 +453,7 @@ export default function CascadingRelationsTab() { toast.error(response.message || "삭제에 실패했습니다."); } } catch (error) { - toast.error("삭제 중 오류가 발생했습니다."); + showErrorToast("연쇄 관계 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/cascading-management/tabs/ConditionTab.tsx b/frontend/app/(main)/admin/cascading-management/tabs/ConditionTab.tsx index d7caeb57..3bc452c9 100644 --- a/frontend/app/(main)/admin/cascading-management/tabs/ConditionTab.tsx +++ b/frontend/app/(main)/admin/cascading-management/tabs/ConditionTab.tsx @@ -43,6 +43,7 @@ import { TableRow, } from "@/components/ui/table"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { cascadingConditionApi, CascadingCondition, @@ -170,7 +171,7 @@ export default function ConditionTab() { toast.error(response.error || "삭제에 실패했습니다."); } } catch (error) { - toast.error("삭제 중 오류가 발생했습니다."); + showErrorToast("조건 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setIsDeleteDialogOpen(false); setDeletingConditionId(null); @@ -206,7 +207,7 @@ export default function ConditionTab() { toast.error(response.error || "저장에 실패했습니다."); } } catch (error) { - toast.error("저장 중 오류가 발생했습니다."); + showErrorToast("조건 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/cascading-management/tabs/HierarchyColumnTab.tsx b/frontend/app/(main)/admin/cascading-management/tabs/HierarchyColumnTab.tsx index d0d77230..2ca9fe9b 100644 --- a/frontend/app/(main)/admin/cascading-management/tabs/HierarchyColumnTab.tsx +++ b/frontend/app/(main)/admin/cascading-management/tabs/HierarchyColumnTab.tsx @@ -29,6 +29,7 @@ import { } from "@/components/ui/card"; import { Plus, Pencil, Trash2, Database, RefreshCw, Layers } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { hierarchyColumnApi, @@ -300,7 +301,7 @@ export default function HierarchyColumnTab() { } } catch (error) { console.error("저장 에러:", error); - toast.error("저장 중 오류가 발생했습니다."); + showErrorToast("계층구조 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } }; @@ -319,7 +320,7 @@ export default function HierarchyColumnTab() { } } catch (error) { console.error("삭제 에러:", error); - toast.error("삭제 중 오류가 발생했습니다."); + showErrorToast("계층구조 설정 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx index f6e98a9f..f9475759 100644 --- a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx +++ b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx @@ -11,6 +11,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { ArrowLeft, Save, RotateCcw, Eye } from "lucide-react"; import { useWebTypes, type WebTypeFormData } from "@/hooks/admin/useWebTypes"; import { AVAILABLE_COMPONENTS, getComponentInfo } from "@/lib/utils/availableComponents"; @@ -148,7 +149,7 @@ export default function EditWebTypePage() { toast.success("웹타입이 성공적으로 수정되었습니다."); router.push(`/admin/standards/${webType}`); } catch (error) { - toast.error(error instanceof Error ? error.message : "수정 중 오류가 발생했습니다."); + showErrorToast("웹타입 수정에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index 8c5e7e82..71a63371 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -19,6 +19,7 @@ import { AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { Plus, Search, Edit, Trash2, Eye, Filter, RotateCcw, Settings, SortAsc, SortDesc } from "lucide-react"; import { useWebTypes } from "@/hooks/admin/useWebTypes"; import Link from "next/link"; @@ -90,7 +91,7 @@ export default function WebTypesManagePage() { await deleteWebType(webType); toast.success(`웹타입 '${typeName}'이 삭제되었습니다.`); } catch (error) { - toast.error(error instanceof Error ? error.message : "삭제 중 오류가 발생했습니다."); + showErrorToast("웹타입 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/systemMng/collection-managementList/page.tsx b/frontend/app/(main)/admin/systemMng/collection-managementList/page.tsx index 75f00cdb..0b211a79 100644 --- a/frontend/app/(main)/admin/systemMng/collection-managementList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/collection-managementList/page.tsx @@ -37,6 +37,7 @@ import { RefreshCw } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { CollectionAPI, DataCollectionConfig } from "@/lib/api/collection"; import CollectionConfigModal from "@/components/admin/CollectionConfigModal"; @@ -69,7 +70,7 @@ export default function CollectionManagementPage() { setConfigs(data); } catch (error) { console.error("수집 설정 목록 조회 오류:", error); - toast.error("수집 설정 목록을 불러오는데 실패했습니다."); + showErrorToast("수집 설정 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setIsLoading(false); } @@ -131,7 +132,7 @@ export default function CollectionManagementPage() { toast.success(`"${config.config_name}" 수집 작업을 시작했습니다.`); } catch (error) { console.error("수집 작업 실행 오류:", error); - toast.error("수집 작업 실행에 실패했습니다."); + showErrorToast("수집 작업 실행에 실패했습니다", error, { guidance: "수집 설정을 확인해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/systemMng/dataflow/page.tsx b/frontend/app/(main)/admin/systemMng/dataflow/page.tsx index d55a6cf1..de0b8d95 100644 --- a/frontend/app/(main)/admin/systemMng/dataflow/page.tsx +++ b/frontend/app/(main)/admin/systemMng/dataflow/page.tsx @@ -6,6 +6,7 @@ import DataFlowList from "@/components/dataflow/DataFlowList"; import { FlowEditor } from "@/components/dataflow/node-editor/FlowEditor"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { Button } from "@/components/ui/button"; import { ScrollToTop } from "@/components/common/ScrollToTop"; import { ArrowLeft } from "lucide-react"; @@ -35,7 +36,7 @@ export default function DataFlowPage() { toast.success("플로우를 불러왔습니다."); } catch (error: any) { console.error("❌ 플로우 불러오기 실패:", error); - toast.error(error.message || "플로우를 불러오는데 실패했습니다."); + showErrorToast("플로우 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } }; diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index d5c41e6a..e2911ed8 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -24,6 +24,7 @@ import { import { cn } from "@/lib/utils"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { useMultiLang } from "@/hooks/useMultiLang"; import { useAuth } from "@/hooks/useAuth"; import { TABLE_MANAGEMENT_KEYS } from "@/constants/tableManagement"; @@ -331,11 +332,15 @@ export default function TableManagementPage() { setTables(response.data.data); toast.success("테이블 목록을 성공적으로 로드했습니다."); } else { - toast.error(response.data.message || "테이블 목록 로드에 실패했습니다."); + showErrorToast("테이블 목록을 불러오는 데 실패했습니다", response.data.message, { + guidance: "네트워크 연결을 확인해 주세요.", + }); } } catch (error) { // console.error("테이블 목록 로드 실패:", error); - toast.error("테이블 목록 로드 중 오류가 발생했습니다."); + showErrorToast("테이블 목록을 불러오는 데 실패했습니다", error, { + guidance: "네트워크 연결을 확인해 주세요.", + }); } finally { setLoading(false); } @@ -403,11 +408,15 @@ export default function TableManagementPage() { setTotalColumns(data.total || processedColumns.length); toast.success("컬럼 정보를 성공적으로 로드했습니다."); } else { - toast.error(response.data.message || "컬럼 정보 로드에 실패했습니다."); + showErrorToast("컬럼 정보를 불러오는 데 실패했습니다", response.data.message, { + guidance: "네트워크 연결을 확인해 주세요.", + }); } } catch (error) { // console.error("컬럼 타입 정보 로드 실패:", error); - toast.error("컬럼 정보 로드 중 오류가 발생했습니다."); + showErrorToast("컬럼 정보를 불러오는 데 실패했습니다", error, { + guidance: "네트워크 연결을 확인해 주세요.", + }); } finally { setColumnsLoading(false); } @@ -777,11 +786,15 @@ export default function TableManagementPage() { loadColumnTypes(selectedTable); }, 1000); } else { - toast.error(response.data.message || "컬럼 설정 저장에 실패했습니다."); + showErrorToast("컬럼 설정 저장에 실패했습니다", response.data.message, { + guidance: "입력 데이터를 확인하고 다시 시도해 주세요.", + }); } } catch (error) { // console.error("컬럼 설정 저장 실패:", error); - toast.error("컬럼 설정 저장 중 오류가 발생했습니다."); + showErrorToast("컬럼 설정 저장에 실패했습니다", error, { + guidance: "입력 데이터를 확인하고 다시 시도해 주세요.", + }); } }; @@ -980,12 +993,16 @@ export default function TableManagementPage() { loadColumnTypes(selectedTable, 1, pageSize); }, 1000); } else { - toast.error(response.data.message || "설정 저장에 실패했습니다."); + showErrorToast("설정 저장에 실패했습니다", response.data.message, { + guidance: "잠시 후 다시 시도해 주세요.", + }); } } } catch (error) { // console.error("설정 저장 실패:", error); - toast.error("설정 저장 중 오류가 발생했습니다."); + showErrorToast("설정 저장에 실패했습니다", error, { + guidance: "잠시 후 다시 시도해 주세요.", + }); } finally { setIsSaving(false); } @@ -1091,7 +1108,9 @@ export default function TableManagementPage() { toast.error(response.data.message || "PK 설정 실패"); } } catch (error: any) { - toast.error(error?.response?.data?.message || "PK 설정 중 오류가 발생했습니다."); + showErrorToast("PK 설정에 실패했습니다", error, { + guidance: "컬럼 정보를 확인하고 다시 시도해 주세요.", + }); } finally { setPkDialogOpen(false); } @@ -1115,7 +1134,9 @@ export default function TableManagementPage() { toast.error(response.data.message || "인덱스 설정 실패"); } } catch (error: any) { - toast.error(error?.response?.data?.message || error?.response?.data?.error || "인덱스 설정 중 오류가 발생했습니다."); + showErrorToast("인덱스 설정에 실패했습니다", error, { + guidance: "컬럼 정보를 확인하고 다시 시도해 주세요.", + }); } }, [selectedTable, loadConstraints], @@ -1154,10 +1175,14 @@ export default function TableManagementPage() { ), ); } else { - toast.error(response.data.message || "UNIQUE 설정 실패"); + showErrorToast("UNIQUE 제약 조건 설정에 실패했습니다", response.data.message, { + guidance: "해당 컬럼에 중복 데이터가 없는지 확인해 주세요.", + }); } } catch (error: any) { - toast.error(error?.response?.data?.message || "UNIQUE 설정 중 오류가 발생했습니다."); + showErrorToast("UNIQUE 제약 조건 설정에 실패했습니다", error, { + guidance: "해당 컬럼에 중복 데이터가 없는지 확인해 주세요.", + }); } }, [selectedTable], @@ -1188,12 +1213,14 @@ export default function TableManagementPage() { ), ); } else { - toast.error(response.data.message || "NOT NULL 설정 실패"); + showErrorToast("NOT NULL 제약 조건 설정에 실패했습니다", response.data.message, { + guidance: "해당 컬럼에 NULL 값이 없는지 확인해 주세요.", + }); } } catch (error: any) { - toast.error( - error?.response?.data?.message || "NOT NULL 설정 중 오류가 발생했습니다.", - ); + showErrorToast("NOT NULL 제약 조건 설정에 실패했습니다", error, { + guidance: "해당 컬럼에 NULL 값이 없는지 확인해 주세요.", + }); } }, [selectedTable], @@ -1225,10 +1252,14 @@ export default function TableManagementPage() { // 테이블 목록 새로고침 await loadTables(); } else { - toast.error(result.message || "테이블 삭제에 실패했습니다."); + showErrorToast("테이블 삭제에 실패했습니다", result.message, { + guidance: "테이블에 종속된 데이터가 있는지 확인해 주세요.", + }); } } catch (error: any) { - toast.error(error?.response?.data?.message || "테이블 삭제 중 오류가 발생했습니다."); + showErrorToast("테이블 삭제에 실패했습니다", error, { + guidance: "테이블에 종속된 데이터가 있는지 확인해 주세요.", + }); } finally { setIsDeleting(false); setDeleteDialogOpen(false); @@ -1308,7 +1339,9 @@ export default function TableManagementPage() { setSelectedTableIds(new Set()); await loadTables(); } catch (error: any) { - toast.error("테이블 삭제 중 오류가 발생했습니다."); + showErrorToast("테이블 삭제에 실패했습니다", error, { + guidance: "테이블에 종속된 데이터가 있는지 확인해 주세요.", + }); } finally { setIsDeleting(false); setDeleteDialogOpen(false); diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index d1e07abe..d65b7884 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -9,6 +9,7 @@ import { ScreenDefinition, LayoutData, ComponentData } from "@/types/screen"; import { LayerDefinition } from "@/types/screen-management"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { initializeComponents } from "@/lib/registry/components"; import { EditModal } from "@/components/screen/EditModal"; import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic"; @@ -219,7 +220,7 @@ function ScreenViewPage() { } catch (error) { console.error("화면 로드 실패:", error); setError("화면을 불러오는데 실패했습니다."); - toast.error("화면을 불러오는데 실패했습니다."); + showErrorToast("화면을 불러오는 데 실패했습니다", error, { guidance: "화면 설정을 확인하거나 잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); } diff --git a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx index 861795b5..224fb48d 100644 --- a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx +++ b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx @@ -8,6 +8,7 @@ import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition } from "@/types/screen"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; import { useAuth } from "@/hooks/useAuth"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; @@ -135,7 +136,7 @@ function PopScreenViewPage() { } catch (error) { console.error("[POP] 화면 로드 실패:", error); setError("화면을 불러오는데 실패했습니다."); - toast.error("화면을 불러오는데 실패했습니다."); + showErrorToast("POP 화면을 불러오는 데 실패했습니다", error, { guidance: "화면 설정을 확인하거나 잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); } diff --git a/frontend/components/GlobalFileViewer.tsx b/frontend/components/GlobalFileViewer.tsx index 248de1d5..34cf8e60 100644 --- a/frontend/components/GlobalFileViewer.tsx +++ b/frontend/components/GlobalFileViewer.tsx @@ -11,6 +11,7 @@ import { downloadFile } from "@/lib/api/file"; import { FileViewerModal } from "@/lib/registry/components/file-upload/FileViewerModal"; import { formatFileSize } from "@/lib/utils"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { File, FileText, @@ -134,7 +135,7 @@ export const GlobalFileViewer: React.FC = ({ toast.success(`파일 다운로드 시작: ${file.realFileName}`); } catch (error) { console.error("파일 다운로드 오류:", error); - toast.error("파일 다운로드에 실패했습니다."); + showErrorToast("파일 다운로드에 실패했습니다", error, { guidance: "파일이 존재하는지 확인하고 다시 시도해 주세요." }); } }; diff --git a/frontend/components/admin/AdvancedBatchModal.tsx b/frontend/components/admin/AdvancedBatchModal.tsx index 1276bcad..54b86cbf 100644 --- a/frontend/components/admin/AdvancedBatchModal.tsx +++ b/frontend/components/admin/AdvancedBatchModal.tsx @@ -20,6 +20,7 @@ import { import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { BatchAPI, BatchJob, BatchConfig } from "@/lib/api/batch"; import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection"; @@ -123,7 +124,7 @@ export default function AdvancedBatchModal({ setConnections(list); } catch (error) { console.error("연결 목록 조회 오류:", error); - toast.error("연결 목록을 불러오는데 실패했습니다."); + showErrorToast("연결 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } }; @@ -190,7 +191,7 @@ export default function AdvancedBatchModal({ onClose(); } catch (error) { console.error("배치 저장 오류:", error); - toast.error(error instanceof Error ? error.message : "저장에 실패했습니다."); + showErrorToast("배치 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } finally { setIsLoading(false); } diff --git a/frontend/components/admin/DDLLogViewer.tsx b/frontend/components/admin/DDLLogViewer.tsx index f707511b..0889f1bc 100644 --- a/frontend/components/admin/DDLLogViewer.tsx +++ b/frontend/components/admin/DDLLogViewer.tsx @@ -33,6 +33,7 @@ import { Trash2, } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { ddlApi } from "../../lib/api/ddl"; @@ -71,7 +72,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) { setStatistics(statsResult); } catch (error) { // console.error("DDL 로그 로드 실패:", error); - toast.error("DDL 로그를 불러오는데 실패했습니다."); + showErrorToast("DDL 로그를 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { if (showLoading) setLoading(false); setRefreshing(false); @@ -108,7 +109,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) { loadData(false); } catch (error) { // console.error("로그 정리 실패:", error); - toast.error("로그 정리에 실패했습니다."); + showErrorToast("로그 정리에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; diff --git a/frontend/components/admin/ExternalCallConfigModal.tsx b/frontend/components/admin/ExternalCallConfigModal.tsx index 30694034..e881b17a 100644 --- a/frontend/components/admin/ExternalCallConfigModal.tsx +++ b/frontend/components/admin/ExternalCallConfigModal.tsx @@ -15,6 +15,7 @@ import { } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { ExternalCallConfigAPI, ExternalCallConfig, @@ -259,7 +260,7 @@ export function ExternalCallConfigModal({ isOpen, onClose, onSave, editingConfig } } catch (error) { console.error("외부 호출 설정 저장 오류:", error); - toast.error("저장 중 오류가 발생했습니다."); + showErrorToast("외부 호출 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } finally { setLoading(false); } diff --git a/frontend/components/admin/LayoutFormModal.tsx b/frontend/components/admin/LayoutFormModal.tsx index a4bcdf4f..ad17198d 100644 --- a/frontend/components/admin/LayoutFormModal.tsx +++ b/frontend/components/admin/LayoutFormModal.tsx @@ -32,6 +32,7 @@ import { } from "lucide-react"; import { LayoutCategory } from "@/types/layout"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; interface LayoutFormModalProps { open: boolean; @@ -210,7 +211,7 @@ export const LayoutFormModal: React.FC = ({ open, onOpenCh success: false, message: result.message || "레이아웃 생성에 실패했습니다.", }); - toast.error("레이아웃 생성 실패"); + showErrorToast("레이아웃 생성에 실패했습니다", result.message, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } } catch (error) { console.error("레이아웃 생성 오류:", error); @@ -218,7 +219,7 @@ export const LayoutFormModal: React.FC = ({ open, onOpenCh success: false, message: "서버 오류가 발생했습니다.", }); - toast.error("서버 오류"); + showErrorToast("레이아웃 생성에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } finally { setIsGenerating(false); } diff --git a/frontend/components/admin/MenuCopyDialog.tsx b/frontend/components/admin/MenuCopyDialog.tsx index c33e726b..cc2a8e45 100644 --- a/frontend/components/admin/MenuCopyDialog.tsx +++ b/frontend/components/admin/MenuCopyDialog.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { Loader2 } from "lucide-react"; import { Dialog, @@ -94,7 +95,7 @@ export function MenuCopyDialog({ } } catch (error) { console.error("회사 목록 조회 실패:", error); - toast.error("회사 목록을 불러올 수 없습니다"); + showErrorToast("회사 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setLoadingCompanies(false); } @@ -160,7 +161,7 @@ export function MenuCopyDialog({ } } catch (error: any) { console.error("메뉴 복사 오류:", error); - toast.error(error.message || "메뉴 복사 중 오류가 발생했습니다"); + showErrorToast("메뉴 복사에 실패했습니다", error, { guidance: "복사 대상과 설정을 확인해 주세요." }); } finally { setCopying(false); } diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 854b1159..f75ded8f 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -18,6 +18,7 @@ import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveS import { screenApi } from "@/lib/api/screen"; import { ComponentData } from "@/types/screen"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { useAuth } from "@/hooks/useAuth"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; @@ -548,11 +549,15 @@ export const ScreenModal: React.FC = ({ className }) => { setOriginalData(normalizedData); // 🆕 원본 데이터 저장 (UPDATE 판단용) } } else { - toast.error("데이터를 불러올 수 없습니다."); + toast.error("수정할 데이터를 불러올 수 없습니다.", { + description: "해당 항목이 삭제되었거나 접근 권한이 없을 수 있습니다.", + }); } } catch (error) { console.error("수정 데이터 조회 오류:", error); - toast.error("데이터를 불러오는 중 오류가 발생했습니다."); + showErrorToast("수정 데이터 조회에 실패했습니다", error, { + guidance: "네트워크 연결을 확인하고 다시 시도해 주세요.", + }); } } } @@ -604,7 +609,9 @@ export const ScreenModal: React.FC = ({ className }) => { } } catch (error) { console.error("화면 데이터 로딩 오류:", error); - toast.error("화면을 불러오는 중 오류가 발생했습니다."); + showErrorToast("화면 데이터를 불러오는 데 실패했습니다", error, { + guidance: "화면 설정을 확인하거나 잠시 후 다시 시도해 주세요.", + }); handleClose(); } finally { setLoading(false); diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx index fa96bb99..af06af65 100644 --- a/frontend/components/dataflow/DataFlowList.tsx +++ b/frontend/components/dataflow/DataFlowList.tsx @@ -21,6 +21,7 @@ import { } from "@/components/ui/dialog"; import { MoreHorizontal, Trash2, Copy, Plus, Search, Network, Database, Calendar, User } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { useAuth } from "@/hooks/useAuth"; import { apiClient } from "@/lib/api/client"; @@ -61,7 +62,7 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { } } catch (error) { console.error("플로우 목록 조회 실패", error); - toast.error("플로우 목록을 불러오는데 실패했습니다."); + showErrorToast("플로우 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setLoading(false); } @@ -107,7 +108,7 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { } } catch (error) { console.error("플로우 복사 실패:", error); - toast.error("플로우 복사에 실패했습니다."); + showErrorToast("플로우 복사에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); } @@ -129,7 +130,7 @@ export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { } } catch (error) { console.error("플로우 삭제 실패:", error); - toast.error("플로우 삭제에 실패했습니다."); + showErrorToast("플로우 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); setShowDeleteModal(false); diff --git a/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx b/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx index 1af3beed..811e4b60 100644 --- a/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx +++ b/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx @@ -24,6 +24,7 @@ import { Timer, } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; // 타입 import import { @@ -144,7 +145,7 @@ const ExternalCallTestPanel: React.FC = ({ toast.success("API 테스트가 성공했습니다!"); setActiveTab("response"); } else { - toast.error("API 호출이 실패했습니다."); + showErrorToast("API 호출이 실패했습니다", null, { guidance: "URL과 요청 설정을 확인해 주세요." }); setActiveTab("response"); } } else { @@ -156,7 +157,7 @@ const ExternalCallTestPanel: React.FC = ({ }; setTestResult(errorResult); onTestResult(errorResult); - toast.error(response.error || "테스트 실행 중 오류가 발생했습니다."); + showErrorToast("API 테스트 실행에 실패했습니다", response.error, { guidance: "URL과 요청 설정을 확인해 주세요." }); } } catch (error) { const errorResult: ApiTestResult = { @@ -167,7 +168,7 @@ const ExternalCallTestPanel: React.FC = ({ }; setTestResult(errorResult); onTestResult(errorResult); - toast.error("테스트 실행 중 오류가 발생했습니다."); + showErrorToast("API 테스트 실행에 실패했습니다", error, { guidance: "네트워크 연결과 URL을 확인해 주세요." }); } finally { setIsLoading(false); } @@ -179,7 +180,7 @@ const ExternalCallTestPanel: React.FC = ({ await navigator.clipboard.writeText(text); toast.success("클립보드에 복사되었습니다."); } catch (error) { - toast.error("복사에 실패했습니다."); + showErrorToast("클립보드 복사에 실패했습니다", error, { guidance: "브라우저 권한을 확인해 주세요." }); } }, []); diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 8b521fe0..fbae903f 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -8,6 +8,7 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Plus, Save, Edit2, Trash2, FolderTree, Check, ChevronsUpDown } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorType } from "@/types/numbering-rule"; import { NumberingRuleCard } from "./NumberingRuleCard"; import { NumberingRulePreview } from "./NumberingRulePreview"; @@ -399,10 +400,10 @@ export const NumberingRuleDesigner: React.FC = ({ await onSave?.(response.data); toast.success("채번 규칙이 저장되었습니다"); } else { - toast.error(response.error || "저장 실패"); + showErrorToast("채번 규칙 저장에 실패했습니다", response.error, { guidance: "설정을 확인하고 다시 시도해 주세요." }); } } catch (error: any) { - toast.error(`저장 실패: ${error.message}`); + showErrorToast("채번 규칙 저장에 실패했습니다", error, { guidance: "설정을 확인하고 다시 시도해 주세요." }); } finally { setLoading(false); } @@ -446,10 +447,10 @@ export const NumberingRuleDesigner: React.FC = ({ toast.success("규칙이 삭제되었습니다"); } else { - toast.error(response.error || "삭제 실패"); + showErrorToast("채번 규칙 삭제에 실패했습니다", response.error, { guidance: "잠시 후 다시 시도해 주세요." }); } } catch (error: any) { - toast.error(`삭제 실패: ${error.message}`); + showErrorToast("채번 규칙 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); } diff --git a/frontend/components/pop/management/PopScreenSettingModal.tsx b/frontend/components/pop/management/PopScreenSettingModal.tsx index 7dd7a11e..c57d6d52 100644 --- a/frontend/components/pop/management/PopScreenSettingModal.tsx +++ b/frontend/components/pop/management/PopScreenSettingModal.tsx @@ -33,6 +33,7 @@ import { Save, } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { ScreenDefinition } from "@/types/screen"; import { screenApi } from "@/lib/api/screen"; import { PopScreenGroup, getPopScreenGroups } from "@/lib/api/popScreenGroup"; @@ -191,7 +192,7 @@ export function PopScreenSettingModal({ onOpenChange(false); } catch (error) { console.error("저장 실패:", error); - toast.error("저장에 실패했습니다."); + showErrorToast("POP 화면 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } finally { setSaving(false); } diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 7a9a3ff3..4d8a6fec 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -15,6 +15,7 @@ import { ko } from "date-fns/locale"; import { useAuth } from "@/hooks/useAuth"; import { uploadFilesAndCreateData } from "@/lib/api/file"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { useCascadingDropdown } from "@/hooks/useCascadingDropdown"; import { CascadingDropdownConfig, LayerDefinition } from "@/types/screen-management"; import { @@ -1265,7 +1266,7 @@ export const InteractiveScreenViewer: React.FC = ( } } catch (error) { // console.error("파일 업로드 오류:", error); - toast.error("파일 업로드에 실패했습니다."); + showErrorToast("파일 업로드에 실패했습니다", error, { guidance: "파일 크기와 형식을 확인하고 다시 시도해 주세요." }); // 파일 입력 초기화 e.target.value = ""; diff --git a/frontend/components/screen/MenuAssignmentModal.tsx b/frontend/components/screen/MenuAssignmentModal.tsx index fddf0bcc..55e57f74 100644 --- a/frontend/components/screen/MenuAssignmentModal.tsx +++ b/frontend/components/screen/MenuAssignmentModal.tsx @@ -14,6 +14,7 @@ import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { Search, Monitor, Settings, X, Plus } from "lucide-react"; import { menuScreenApi } from "@/lib/api/screen"; import { apiClient } from "@/lib/api/client"; @@ -93,7 +94,7 @@ export const MenuAssignmentModal: React.FC = ({ setMenus(allMenus); } catch (error) { // console.error("메뉴 목록 로드 실패:", error); - toast.error("메뉴 목록을 불러오는데 실패했습니다."); + showErrorToast("메뉴 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setLoading(false); } diff --git a/frontend/components/screen/NodeSettingModal.tsx b/frontend/components/screen/NodeSettingModal.tsx index 5ef1d612..287dcaff 100644 --- a/frontend/components/screen/NodeSettingModal.tsx +++ b/frontend/components/screen/NodeSettingModal.tsx @@ -41,6 +41,7 @@ import { CommandList, } from "@/components/ui/command"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { cn } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; import { @@ -403,7 +404,7 @@ export default function NodeSettingModal({ ]); toast.success("데이터가 새로고침되었습니다."); } catch (error) { - toast.error("새로고침 실패"); + showErrorToast("새로고침에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); } @@ -635,10 +636,10 @@ function TableRelationTab({ onReload(); onRefreshVisualization?.(); } else { - toast.error(response.message || "저장에 실패했습니다."); + showErrorToast("노드 설정 저장에 실패했습니다", response.message, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } } catch (error: any) { - toast.error(error.message || "저장 중 오류가 발생했습니다."); + showErrorToast("노드 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } }; @@ -653,10 +654,10 @@ function TableRelationTab({ onReload(); onRefreshVisualization?.(); } else { - toast.error(response.message || "삭제에 실패했습니다."); + showErrorToast("노드 설정 삭제에 실패했습니다", response.message, { guidance: "잠시 후 다시 시도해 주세요." }); } } catch (error: any) { - toast.error(error.message || "삭제 중 오류가 발생했습니다."); + showErrorToast("노드 설정 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; @@ -1178,10 +1179,10 @@ function JoinSettingTab({ onReload(); onRefreshVisualization?.(); } else { - toast.error(response.message || "저장에 실패했습니다."); + showErrorToast("노드 설정 저장에 실패했습니다", response.message, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } } catch (error: any) { - toast.error(error.message || "저장 중 오류가 발생했습니다."); + showErrorToast("노드 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } }; @@ -1196,10 +1197,10 @@ function JoinSettingTab({ onReload(); onRefreshVisualization?.(); } else { - toast.error(response.message || "삭제에 실패했습니다."); + showErrorToast("노드 설정 삭제에 실패했습니다", response.message, { guidance: "잠시 후 다시 시도해 주세요." }); } } catch (error: any) { - toast.error(error.message || "삭제 중 오류가 발생했습니다."); + showErrorToast("노드 설정 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; @@ -1586,10 +1587,10 @@ function DataFlowTab({ onReload(); onRefreshVisualization?.(); } else { - toast.error(response.message || "저장에 실패했습니다."); + showErrorToast("노드 설정 저장에 실패했습니다", response.message, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } } catch (error: any) { - toast.error(error.message || "저장 중 오류가 발생했습니다."); + showErrorToast("노드 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } }; @@ -1604,10 +1605,10 @@ function DataFlowTab({ onReload(); onRefreshVisualization?.(); } else { - toast.error(response.message || "삭제에 실패했습니다."); + showErrorToast("노드 설정 삭제에 실패했습니다", response.message, { guidance: "잠시 후 다시 시도해 주세요." }); } } catch (error: any) { - toast.error(error.message || "삭제 중 오류가 발생했습니다."); + showErrorToast("노드 설정 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; diff --git a/frontend/components/screen/SaveModal.tsx b/frontend/components/screen/SaveModal.tsx index 1c848d6a..259bf238 100644 --- a/frontend/components/screen/SaveModal.tsx +++ b/frontend/components/screen/SaveModal.tsx @@ -5,6 +5,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u import { Button } from "@/components/ui/button"; import { X, Save, Loader2 } from "lucide-react"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { InteractiveScreenViewer } from "./InteractiveScreenViewer"; import { screenApi } from "@/lib/api/screen"; @@ -76,7 +77,9 @@ export const SaveModal: React.FC = ({ } } catch (error) { console.error("화면 로드 실패:", error); - toast.error("화면을 불러오는데 실패했습니다."); + showErrorToast("화면 구성 정보를 불러오는 데 실패했습니다", error, { + guidance: "화면 설정을 확인하거나 잠시 후 다시 시도해 주세요.", + }); } finally { setLoading(false); } @@ -264,7 +267,9 @@ export const SaveModal: React.FC = ({ } catch (error: any) { // ❌ 저장 실패 - 모달은 닫히지 않음 console.error("저장 실패:", error); - toast.error(`저장 중 오류가 발생했습니다: ${error.message || "알 수 없는 오류"}`); + showErrorToast("데이터 저장에 실패했습니다", error, { + guidance: "입력 값을 확인하고 다시 시도해 주세요.", + }); } finally { setIsSaving(false); } diff --git a/frontend/components/screen/ScreenGroupModal.tsx b/frontend/components/screen/ScreenGroupModal.tsx index 3cf0759a..2726ce04 100644 --- a/frontend/components/screen/ScreenGroupModal.tsx +++ b/frontend/components/screen/ScreenGroupModal.tsx @@ -22,6 +22,7 @@ import { } from "@/components/ui/select"; import { ScreenGroup, createScreenGroup, updateScreenGroup } from "@/lib/api/screenGroup"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { apiClient } from "@/lib/api/client"; import { Command, @@ -225,11 +226,11 @@ export function ScreenGroupModal({ onSuccess(); onClose(); } else { - toast.error(response.message || "작업에 실패했습니다"); + showErrorToast("그룹 저장에 실패했습니다", response.message, { guidance: "잠시 후 다시 시도해 주세요." }); } } catch (error: any) { console.error("그룹 저장 실패:", error); - toast.error("그룹 저장에 실패했습니다"); + showErrorToast("그룹 저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); } diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index 1aa47f0d..e8b56b36 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -89,6 +89,7 @@ import { Check, ChevronsUpDown } from "lucide-react"; import { ScreenGroupModal } from "./ScreenGroupModal"; import CopyScreenModal from "./CopyScreenModal"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { screenApi } from "@/lib/api/screen"; interface ScreenGroupTreeViewProps { @@ -581,11 +582,11 @@ export function ScreenGroupTreeView({ await loadGroupsData(); window.dispatchEvent(new CustomEvent("screen-list-refresh")); } else { - toast.error(response.message || "그룹 삭제에 실패했습니다"); + showErrorToast("그룹 삭제에 실패했습니다", response.message, { guidance: "하위 항목이 있는 경우 먼저 삭제해 주세요." }); } } catch (error) { console.error("그룹 삭제 실패:", error); - toast.error("그룹 삭제에 실패했습니다"); + showErrorToast("그룹 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setIsDeleting(false); setDeleteProgress({ current: 0, total: 0, message: "" }); @@ -614,7 +615,7 @@ export function ScreenGroupTreeView({ window.dispatchEvent(new CustomEvent("screen-list-refresh")); } catch (error) { console.error("화면 삭제 실패:", error); - toast.error("화면 삭제에 실패했습니다"); + showErrorToast("화면 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setIsScreenDeleting(false); setIsScreenDeleteDialogOpen(false); @@ -765,7 +766,7 @@ export function ScreenGroupTreeView({ window.dispatchEvent(new CustomEvent("screen-list-refresh")); } catch (error) { console.error("화면 수정 실패:", error); - toast.error("화면 수정에 실패했습니다"); + showErrorToast("화면 정보 수정에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } finally { setIsEditScreenModalOpen(false); setEditingScreen(null); diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index 2271c96f..1f1853be 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -53,6 +53,7 @@ import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeU import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { RealtimePreview } from "./RealtimePreviewDynamic"; import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; +import { showErrorToast } from "@/lib/utils/toastUtils"; // InteractiveScreenViewer를 동적으로 import (SSR 비활성화) const InteractiveScreenViewer = dynamic( @@ -683,7 +684,7 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr setPreviewLayout(layoutData); } catch (error) { console.error("❌ 레이아웃 로드 실패:", error); - toast.error("화면 레이아웃을 불러오는데 실패했습니다."); + showErrorToast("화면 레이아웃을 불러오는 데 실패했습니다", error, { guidance: "화면 설정을 확인하거나 잠시 후 다시 시도해 주세요." }); } finally { setIsLoadingPreview(false); } diff --git a/frontend/components/screen/SimpleScreenDesigner.tsx b/frontend/components/screen/SimpleScreenDesigner.tsx index 6f76a112..d748bb8c 100644 --- a/frontend/components/screen/SimpleScreenDesigner.tsx +++ b/frontend/components/screen/SimpleScreenDesigner.tsx @@ -12,6 +12,7 @@ import { import { generateComponentId } from "@/lib/utils/generateId"; import { screenApi } from "@/lib/api/screen"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { RealtimePreview } from "./RealtimePreviewDynamic"; import DesignerToolbar from "./DesignerToolbar"; @@ -53,7 +54,7 @@ export default function SimpleScreenDesigner({ selectedScreen, onBackToList }: S toast.success("화면이 저장되었습니다."); } catch (error) { // console.error("저장 실패:", error); - toast.error("저장 중 오류가 발생했습니다."); + showErrorToast("화면 설정 저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setIsSaving(false); } diff --git a/frontend/components/screen/panels/FieldJoinPanel.tsx b/frontend/components/screen/panels/FieldJoinPanel.tsx index 86ebc226..42e71717 100644 --- a/frontend/components/screen/panels/FieldJoinPanel.tsx +++ b/frontend/components/screen/panels/FieldJoinPanel.tsx @@ -29,6 +29,7 @@ import { } from "@/components/ui/table"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { Plus, Pencil, Trash2, Link2, Database } from "lucide-react"; import { getFieldJoins, @@ -155,7 +156,7 @@ export default function FieldJoinPanel({ screenId, componentId, layoutId }: Fiel toast.error(response.message || "저장에 실패했습니다."); } } catch (error) { - toast.error("저장 중 오류가 발생했습니다."); + showErrorToast("필드 조인 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } }; @@ -172,7 +173,7 @@ export default function FieldJoinPanel({ screenId, componentId, layoutId }: Fiel toast.error(response.message || "삭제에 실패했습니다."); } } catch (error) { - toast.error("삭제 중 오류가 발생했습니다."); + showErrorToast("필드 조인 설정 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }; diff --git a/frontend/components/screen/panels/FileComponentConfigPanel.tsx b/frontend/components/screen/panels/FileComponentConfigPanel.tsx index f14c861f..21c21956 100644 --- a/frontend/components/screen/panels/FileComponentConfigPanel.tsx +++ b/frontend/components/screen/panels/FileComponentConfigPanel.tsx @@ -14,6 +14,7 @@ import { FileInfo, FileUploadResponse } from "@/lib/registry/components/file-upl import { uploadFiles, downloadFile, deleteFile } from "@/lib/api/file"; import { formatFileSize, cn } from "@/lib/utils"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; interface FileComponentConfigPanelProps { component: FileComponent; @@ -536,7 +537,7 @@ export const FileComponentConfigPanel: React.FC = // fieldName // }); toast.dismiss(); - toast.error(`파일 업로드에 실패했습니다: ${error?.message || '알 수 없는 오류'}`); + showErrorToast("파일 업로드에 실패했습니다", error, { guidance: "파일 크기와 형식을 확인하고 다시 시도해 주세요." }); } finally { // console.log("🏁 파일 업로드 완료, 로딩 상태 해제"); setUploading(false); @@ -554,7 +555,7 @@ export const FileComponentConfigPanel: React.FC = toast.success(`${file.realFileName || file.name} 다운로드가 완료되었습니다.`); } catch (error) { // console.error('파일 다운로드 오류:', error); - toast.error('파일 다운로드에 실패했습니다.'); + showErrorToast("파일 다운로드에 실패했습니다", error, { guidance: "파일이 존재하는지 확인하고 다시 시도해 주세요." }); } }, []); @@ -677,7 +678,7 @@ export const FileComponentConfigPanel: React.FC = toast.success('파일이 삭제되었습니다.'); } catch (error) { // console.error('파일 삭제 오류:', error); - toast.error('파일 삭제에 실패했습니다.'); + showErrorToast("파일 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }, [uploadedFiles, onUpdateProperty, component.id]); @@ -713,7 +714,7 @@ export const FileComponentConfigPanel: React.FC = toast.success(`${uploadedFiles.length}개 파일이 영구 저장되었습니다.`); } catch (error) { // console.error('파일 저장 오류:', error); - toast.error('파일 저장에 실패했습니다.'); + showErrorToast("파일 저장에 실패했습니다", error, { guidance: "파일 크기와 형식을 확인하고 다시 시도해 주세요." }); } }, [uploadedFiles, onUpdateProperty, component.id, setGlobalFileState]); diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index 5564a14d..ecb189c3 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -36,6 +36,7 @@ import { SingleTableWithSticky } from "@/lib/registry/components/table-list/Sing import type { ColumnConfig } from "@/lib/registry/components/table-list/types"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; import { Pagination, PaginationContent, @@ -265,7 +266,7 @@ export function FlowWidget({ setSearchValues({}); } catch (error) { console.error("필터 설정 저장 실패:", error); - toast.error("설정 저장에 실패했습니다"); + showErrorToast("필터 설정 저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }, [filterSettingKey, searchFilterColumns]); @@ -309,7 +310,7 @@ export function FlowWidget({ toast.success("그룹 설정이 저장되었습니다"); } catch (error) { console.error("그룹 설정 저장 실패:", error); - toast.error("설정 저장에 실패했습니다"); + showErrorToast("그룹 설정 저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }, [groupSettingKey, groupByColumns]); @@ -514,7 +515,7 @@ export function FlowWidget({ } } catch (err: any) { console.error("❌ 플로우 새로고침 실패:", err); - toast.error(err.message || "데이터를 새로고치는데 실패했습니다"); + showErrorToast("데이터 새로고침에 실패했습니다", err, { guidance: "네트워크 연결을 확인하고 다시 시도해 주세요." }); } finally { if (selectedStepId) { setStepDataLoading(false); @@ -747,7 +748,7 @@ export function FlowWidget({ } } catch (err: any) { console.error("Failed to load step data:", err); - toast.error(err.message || "데이터를 불러오는데 실패했습니다"); + showErrorToast("스텝 데이터를 불러오는 데 실패했습니다", err, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setStepDataLoading(false); } @@ -1023,7 +1024,7 @@ export function FlowWidget({ toast.success(`${exportData.length}개 행이 Excel로 내보내기 되었습니다.`); } catch (error) { console.error("Excel 내보내기 오류:", error); - toast.error("Excel 내보내기에 실패했습니다."); + showErrorToast("Excel 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." }); } }, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels, flowName, getRowKey]); @@ -1188,7 +1189,7 @@ export function FlowWidget({ toast.success(`${exportData.length}개 행이 PDF로 내보내기 되었습니다.`, { id: "pdf-export" }); } catch (error) { console.error("PDF 내보내기 오류:", error); - toast.error("PDF 내보내기에 실패했습니다.", { id: "pdf-export" }); + showErrorToast("PDF 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." }); } }, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels, flowName, getRowKey]); @@ -1216,7 +1217,7 @@ export function FlowWidget({ toast.success(`${copyData.length}개 행이 클립보드에 복사되었습니다.`); } catch (error) { console.error("복사 오류:", error); - toast.error("복사에 실패했습니다."); + showErrorToast("클립보드 복사에 실패했습니다", error, { guidance: "브라우저 권한을 확인해 주세요." }); } }, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels, getRowKey]); @@ -1318,7 +1319,7 @@ export function FlowWidget({ toast.success("데이터를 새로고침했습니다."); } catch (error) { console.error("새로고침 오류:", error); - toast.error("새로고침에 실패했습니다."); + showErrorToast("데이터 새로고침에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setStepDataLoading(false); } @@ -1399,7 +1400,7 @@ export function FlowWidget({ } } catch (error) { console.error("편집 저장 오류:", error); - toast.error("저장 중 오류가 발생했습니다."); + showErrorToast("데이터 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." }); } cancelEditing(); diff --git a/frontend/hooks/pop/usePopAction.ts b/frontend/hooks/pop/usePopAction.ts index 267beb4e..606a2953 100644 --- a/frontend/hooks/pop/usePopAction.ts +++ b/frontend/hooks/pop/usePopAction.ts @@ -24,6 +24,7 @@ import { usePopEvent } from "./usePopEvent"; import { executePopAction } from "./executePopAction"; import type { ActionResult } from "./executePopAction"; import { toast } from "sonner"; +import { showErrorToast } from "@/lib/utils/toastUtils"; // ======================================== // 타입 정의 @@ -107,7 +108,7 @@ export function usePopAction(screenId: string) { const msg = ACTION_SUCCESS_MESSAGES[action.type]; if (msg) toast.success(msg); } else { - toast.error(result.error || "작업에 실패했습니다."); + showErrorToast("작업에 실패했습니다", result.error, { guidance: "잠시 후 다시 시도해 주세요." }); } // 성공 시 후속 액션 실행 diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index f753a240..dd820cc3 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -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"; @@ -955,7 +956,7 @@ export const ButtonPrimaryComponent: React.FC = ({ } } catch (error: any) { console.error("❌ 데이터 전달 실패:", error); - toast.error(error.message || "데이터 전달 중 오류가 발생했습니다."); + showErrorToast("데이터 전달에 실패했습니다", error, { guidance: "대상 화면 설정과 데이터를 확인해 주세요." }); } }; diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index f655ebe3..b2c385a3 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -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 = ({ 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 = ({ toast.success(`${fileName} 삭제 완료`); } catch (error) { console.error("파일 삭제 오류:", error); - toast.error("파일 삭제에 실패했습니다."); + showErrorToast("파일 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }, [uploadedFiles, onUpdate, component.id, isRecordMode, onFormDataChange, recordTableName, recordId, columnName, getUniqueKey], diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index aee70dd2..d8c2a0b3 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ } } catch (error) { console.error("❌ PDF 내보내기 실패:", error); - toast.error("PDF 내보내기 중 오류가 발생했습니다."); + showErrorToast("PDF 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." }); } }, [ @@ -6644,7 +6645,7 @@ export const TableListComponent: React.FC = ({ handleRefresh(); } catch (error) { console.error("삭제 오류:", error); - toast.error("삭제 중 오류가 발생했습니다"); + showErrorToast("데이터 삭제에 실패했습니다", error, { guidance: "삭제 대상을 확인하고 다시 시도해 주세요." }); } } closeContextMenu(); diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index c00c1b1f..f6694aae 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -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"; @@ -1069,7 +1070,7 @@ export const ButtonPrimaryComponent: React.FC = ({ } } catch (error: any) { console.error("❌ 데이터 전달 실패:", error); - toast.error(error.message || "데이터 전달 중 오류가 발생했습니다."); + showErrorToast("데이터 전달에 실패했습니다", error, { guidance: "대상 화면 설정과 데이터를 확인해 주세요." }); } }; diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 717ea6ef..7b3ace6d 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -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 = ({ 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 = ({ toast.success(`${exportData.length}개 행이 Excel로 내보내기 되었습니다.`); } catch (error) { - toast.error("Excel 내보내기 중 오류가 발생했습니다."); + showErrorToast("Excel 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." }); } }, [ @@ -3216,7 +3217,7 @@ export const TableListComponent: React.FC = ({ 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 = ({ } } catch (error) { console.error("❌ PDF 내보내기 실패:", error); - toast.error("PDF 내보내기 중 오류가 발생했습니다."); + showErrorToast("PDF 파일 내보내기에 실패했습니다", error, { guidance: "데이터를 확인하고 다시 시도해 주세요." }); } }, [ @@ -4519,7 +4520,7 @@ export const TableListComponent: React.FC = ({ setSearchValues({}); } catch (error) { console.error("필터 설정 저장 실패:", error); - toast.error("설정 저장에 실패했습니다"); + showErrorToast("필터 설정 저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } }, [filterSettingKey, visibleFilterColumns]); diff --git a/frontend/lib/registry/pop-components/pop-string-list/PopStringListComponent.tsx b/frontend/lib/registry/pop-components/pop-string-list/PopStringListComponent.tsx index 567f6d1d..ea7f5d58 100644 --- a/frontend/lib/registry/pop-components/pop-string-list/PopStringListComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-string-list/PopStringListComponent.tsx @@ -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); } diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 2ed4db87..37d506dc 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -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; } } diff --git a/frontend/lib/utils/improvedButtonActionExecutor.ts b/frontend/lib/utils/improvedButtonActionExecutor.ts index 5bd20866..92f8000a 100644 --- a/frontend/lib/utils/improvedButtonActionExecutor.ts +++ b/frontend/lib/utils/improvedButtonActionExecutor.ts @@ -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: "잠시 후 다시 시도해 주세요. 문제가 계속되면 관리자에게 문의해 주세요.", + }); } } diff --git a/frontend/lib/utils/toastUtils.ts b/frontend/lib/utils/toastUtils.ts new file mode 100644 index 00000000..9e204583 --- /dev/null +++ b/frontend/lib/utils/toastUtils.ts @@ -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; + 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, + }); +} diff --git a/frontend/lib/v2-core/services/ScheduleGeneratorService.ts b/frontend/lib/v2-core/services/ScheduleGeneratorService.ts index 5d693005..30ee8c65 100644 --- a/frontend/lib/v2-core/services/ScheduleGeneratorService.ts +++ b/frontend/lib/v2-core/services/ScheduleGeneratorService.ts @@ -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); }