feat: 422 ValidationException 에러 AlertDialog 팝업 추가
- ErrorAlertContext 생성 (전역 에러 알림 상태 관리) - useFieldManagement, useMasterFieldManagement, useTemplateManagement에 적용 - 중복 이름, 예약어 사용 시 디자인된 AlertDialog 팝업 표시 - toast 대신 모달 위에 표시되는 팝업으로 변경 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
interface ErrorAlertDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 알림 다이얼로그 컴포넌트
|
||||
* 422 ValidationException 등의 에러 메시지를 표시
|
||||
*/
|
||||
export function ErrorAlertDialog({
|
||||
open,
|
||||
onClose,
|
||||
title = '오류',
|
||||
message,
|
||||
}: ErrorAlertDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
{title}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-base">
|
||||
{message}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={onClose}>
|
||||
확인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
interface ErrorAlertState {
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ErrorAlertContextType {
|
||||
showErrorAlert: (message: string, title?: string) => void;
|
||||
}
|
||||
|
||||
const ErrorAlertContext = createContext<ErrorAlertContextType | null>(null);
|
||||
|
||||
/**
|
||||
* 에러 알림 Context 사용 훅
|
||||
*/
|
||||
export function useErrorAlert() {
|
||||
const context = useContext(ErrorAlertContext);
|
||||
if (!context) {
|
||||
throw new Error('useErrorAlert must be used within ErrorAlertProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
interface ErrorAlertProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 알림 Provider
|
||||
* ItemMasterDataManagement 컴포넌트에서 사용
|
||||
*/
|
||||
export function ErrorAlertProvider({ children }: ErrorAlertProviderProps) {
|
||||
const [errorAlert, setErrorAlert] = useState<ErrorAlertState>({
|
||||
open: false,
|
||||
title: '오류',
|
||||
message: '',
|
||||
});
|
||||
|
||||
const showErrorAlert = useCallback((message: string, title: string = '오류') => {
|
||||
setErrorAlert({
|
||||
open: true,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeErrorAlert = useCallback(() => {
|
||||
setErrorAlert(prev => ({
|
||||
...prev,
|
||||
open: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ErrorAlertContext.Provider value={{ showErrorAlert }}>
|
||||
{children}
|
||||
|
||||
{/* 에러 알림 다이얼로그 */}
|
||||
<AlertDialog open={errorAlert.open} onOpenChange={(isOpen) => !isOpen && closeErrorAlert()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
{errorAlert.title}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-base text-foreground">
|
||||
{errorAlert.message}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={closeErrorAlert}>
|
||||
확인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</ErrorAlertContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ErrorAlertProvider, useErrorAlert } from './ErrorAlertContext';
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export interface ErrorAlertState {
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UseErrorAlertReturn {
|
||||
errorAlert: ErrorAlertState;
|
||||
showErrorAlert: (message: string, title?: string) => void;
|
||||
closeErrorAlert: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 알림 다이얼로그 상태 관리 훅
|
||||
* AlertDialog로 에러 메시지를 표시할 때 사용
|
||||
*/
|
||||
export function useErrorAlert(): UseErrorAlertReturn {
|
||||
const [errorAlert, setErrorAlert] = useState<ErrorAlertState>({
|
||||
open: false,
|
||||
title: '오류',
|
||||
message: '',
|
||||
});
|
||||
|
||||
const showErrorAlert = useCallback((message: string, title: string = '오류') => {
|
||||
setErrorAlert({
|
||||
open: true,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeErrorAlert = useCallback(() => {
|
||||
setErrorAlert(prev => ({
|
||||
...prev,
|
||||
open: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
errorAlert,
|
||||
showErrorAlert,
|
||||
closeErrorAlert,
|
||||
};
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import { useErrorAlert } from '../contexts';
|
||||
import type { ItemPage, ItemField, ItemMasterField, FieldDisplayCondition } from '@/contexts/ItemMasterContext';
|
||||
import { type ConditionalFieldConfig } from '../components/ConditionalDisplayUI';
|
||||
import { fieldService } from '../services';
|
||||
import { ApiError } from '@/lib/api/error-handler';
|
||||
|
||||
export interface UseFieldManagementReturn {
|
||||
// 다이얼로그 상태
|
||||
@@ -79,6 +81,9 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
updateItemMasterField,
|
||||
} = useItemMaster();
|
||||
|
||||
// 에러 알림 (AlertDialog로 표시)
|
||||
const { showErrorAlert } = useErrorAlert();
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [isFieldDialogOpen, setIsFieldDialogOpen] = useState(false);
|
||||
const [selectedSectionForField, setSelectedSectionForField] = useState<number | null>(null);
|
||||
@@ -238,7 +243,23 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
resetFieldForm();
|
||||
} catch (error) {
|
||||
console.error('필드 처리 실패:', error);
|
||||
toast.error('항목 처리에 실패했습니다');
|
||||
|
||||
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등)
|
||||
if (error instanceof ApiError) {
|
||||
console.log('🔍 ApiError.errors:', error.errors); // 디버깅용
|
||||
|
||||
// errors 객체에서 첫 번째 에러 메시지 추출 → AlertDialog로 표시
|
||||
if (error.errors && Object.keys(error.errors).length > 0) {
|
||||
const firstKey = Object.keys(error.errors)[0];
|
||||
const firstError = error.errors[firstKey];
|
||||
const errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
||||
showErrorAlert(errorMessage, '항목 저장 실패');
|
||||
} else {
|
||||
showErrorAlert(error.message, '항목 저장 실패');
|
||||
}
|
||||
} else {
|
||||
showErrorAlert('항목 처리에 실패했습니다', '오류');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import { useErrorAlert } from '../contexts';
|
||||
import type { ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
import { masterFieldService } from '../services';
|
||||
import { ApiError } from '@/lib/api/error-handler';
|
||||
|
||||
/**
|
||||
* @deprecated 2025-11-27: item_fields로 통합됨.
|
||||
@@ -44,10 +46,10 @@ export interface UseMasterFieldManagementReturn {
|
||||
setNewMasterFieldColumnNames: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
|
||||
// 핸들러
|
||||
handleAddMasterField: () => void;
|
||||
handleAddMasterField: () => Promise<void>;
|
||||
handleEditMasterField: (field: ItemMasterField) => void;
|
||||
handleUpdateMasterField: () => void;
|
||||
handleDeleteMasterField: (id: number) => void;
|
||||
handleUpdateMasterField: () => Promise<void>;
|
||||
handleDeleteMasterField: (id: number) => Promise<void>;
|
||||
resetMasterFieldForm: () => void;
|
||||
}
|
||||
|
||||
@@ -59,6 +61,9 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
deleteItemMasterField,
|
||||
} = useItemMaster();
|
||||
|
||||
// 에러 알림 (AlertDialog로 표시)
|
||||
const { showErrorAlert } = useErrorAlert();
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [isMasterFieldDialogOpen, setIsMasterFieldDialogOpen] = useState(false);
|
||||
const [editingMasterFieldId, setEditingMasterFieldId] = useState<number | null>(null);
|
||||
@@ -77,7 +82,7 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
const [newMasterFieldColumnNames, setNewMasterFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
|
||||
|
||||
// 마스터 항목 추가
|
||||
const handleAddMasterField = () => {
|
||||
const handleAddMasterField = async () => {
|
||||
if (!newMasterFieldName.trim() || !newMasterFieldKey.trim()) {
|
||||
toast.error('항목명과 필드 키를 입력해주세요');
|
||||
return;
|
||||
@@ -106,9 +111,30 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
},
|
||||
};
|
||||
|
||||
addItemMasterField(newMasterFieldData as any);
|
||||
resetMasterFieldForm();
|
||||
toast.success('항목이 추가되었습니다');
|
||||
try {
|
||||
await addItemMasterField(newMasterFieldData as any);
|
||||
resetMasterFieldForm();
|
||||
toast.success('항목이 추가되었습니다');
|
||||
} catch (error) {
|
||||
console.error('항목 추가 실패:', error);
|
||||
|
||||
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어) → AlertDialog로 표시
|
||||
if (error instanceof ApiError) {
|
||||
console.log('🔍 ApiError.errors:', error.errors); // 디버깅용
|
||||
|
||||
// errors 객체에서 첫 번째 에러 메시지 추출
|
||||
if (error.errors && Object.keys(error.errors).length > 0) {
|
||||
const firstKey = Object.keys(error.errors)[0];
|
||||
const firstError = error.errors[firstKey];
|
||||
const errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
||||
showErrorAlert(errorMessage, '항목 추가 실패');
|
||||
} else {
|
||||
showErrorAlert(error.message, '항목 추가 실패');
|
||||
}
|
||||
} else {
|
||||
showErrorAlert('항목 추가에 실패했습니다', '오류');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 마스터 항목 수정 시작
|
||||
@@ -134,7 +160,7 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
};
|
||||
|
||||
// 마스터 항목 업데이트
|
||||
const handleUpdateMasterField = () => {
|
||||
const handleUpdateMasterField = async () => {
|
||||
if (!editingMasterFieldId || !newMasterFieldName.trim() || !newMasterFieldKey.trim()) {
|
||||
toast.error('항목명과 필드 키를 입력해주세요');
|
||||
return;
|
||||
@@ -159,16 +185,47 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
},
|
||||
};
|
||||
|
||||
updateItemMasterField(editingMasterFieldId, updateData);
|
||||
resetMasterFieldForm();
|
||||
toast.success('항목이 수정되었습니다');
|
||||
try {
|
||||
await updateItemMasterField(editingMasterFieldId, updateData);
|
||||
resetMasterFieldForm();
|
||||
toast.success('항목이 수정되었습니다');
|
||||
} catch (error) {
|
||||
console.error('항목 수정 실패:', error);
|
||||
|
||||
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등) → AlertDialog로 표시
|
||||
if (error instanceof ApiError) {
|
||||
console.log('🔍 ApiError.errors:', error.errors); // 디버깅용
|
||||
|
||||
// errors 객체에서 첫 번째 에러 메시지 추출
|
||||
if (error.errors && Object.keys(error.errors).length > 0) {
|
||||
const firstKey = Object.keys(error.errors)[0];
|
||||
const firstError = error.errors[firstKey];
|
||||
const errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
||||
showErrorAlert(errorMessage, '항목 수정 실패');
|
||||
} else {
|
||||
showErrorAlert(error.message, '항목 수정 실패');
|
||||
}
|
||||
} else {
|
||||
showErrorAlert('항목 수정에 실패했습니다', '오류');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 항목 삭제 (2025-11-27: 마스터 항목 → 항목으로 통합)
|
||||
const handleDeleteMasterField = (id: number) => {
|
||||
const handleDeleteMasterField = async (id: number) => {
|
||||
if (confirm('이 항목을 삭제하시겠습니까?\n(섹션에서 사용 중인 경우 연결도 함께 해제됩니다)')) {
|
||||
deleteItemMasterField(id);
|
||||
toast.success('항목이 삭제되었습니다');
|
||||
try {
|
||||
await deleteItemMasterField(id);
|
||||
toast.success('항목이 삭제되었습니다');
|
||||
} catch (error) {
|
||||
console.error('항목 삭제 실패:', error);
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error('항목 삭제에 실패했습니다');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import { useErrorAlert } from '../contexts';
|
||||
import type { ItemPage, SectionTemplate, TemplateField, BOMItem, ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
import { templateService } from '../services';
|
||||
import { ApiError } from '@/lib/api/error-handler';
|
||||
|
||||
export interface UseTemplateManagementReturn {
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
@@ -112,6 +114,9 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
deleteBOMItem,
|
||||
} = useItemMaster();
|
||||
|
||||
// 에러 알림 (AlertDialog로 표시)
|
||||
const { showErrorAlert } = useErrorAlert();
|
||||
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
const [isSectionTemplateDialogOpen, setIsSectionTemplateDialogOpen] = useState(false);
|
||||
const [editingSectionTemplateId, setEditingSectionTemplateId] = useState<number | null>(null);
|
||||
@@ -348,7 +353,23 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
resetTemplateFieldForm();
|
||||
} catch (error) {
|
||||
console.error('항목 처리 실패:', error);
|
||||
toast.error('항목 처리에 실패했습니다');
|
||||
|
||||
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등) → AlertDialog로 표시
|
||||
if (error instanceof ApiError) {
|
||||
console.log('🔍 ApiError.errors:', error.errors); // 디버깅용
|
||||
|
||||
// errors 객체에서 첫 번째 에러 메시지 추출
|
||||
if (error.errors && Object.keys(error.errors).length > 0) {
|
||||
const firstKey = Object.keys(error.errors)[0];
|
||||
const firstError = error.errors[firstKey];
|
||||
const errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
||||
showErrorAlert(errorMessage, '항목 저장 실패');
|
||||
} else {
|
||||
showErrorAlert(error.message, '항목 저장 실패');
|
||||
}
|
||||
} else {
|
||||
showErrorAlert('항목 처리에 실패했습니다', '오류');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user