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:
byeongcheolryu
2026-01-06 20:13:06 +09:00
parent e5098b0880
commit 3b52847d89
12 changed files with 821 additions and 417 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export { ErrorAlertProvider, useErrorAlert } from './ErrorAlertContext';

View File

@@ -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,
};
}

View File

@@ -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('항목 처리에 실패했습니다', '오류');
}
}
};

View File

@@ -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('항목 삭제에 실패했습니다');
}
}
}
};

View File

@@ -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('항목 처리에 실패했습니다', '오류');
}
}
};