Files
sam-react-prod/src/lib/api/error-handler.ts
권혁성 98b65a6ca4 feat(WEB): 작업지시 수정 페이지 및 생산 관리 기능 개선
신규 기능:
- 작업지시 수정 페이지 추가 (/production/work-orders/[id]/edit)
- WorkOrderEdit 컴포넌트 신규 생성
- bulk-actions.ts 일괄 작업 유틸리티 추가
- toast-utils.ts 알림 유틸리티 추가

기능 개선:
- ProductionDashboard 대시보드 액션 및 표시 개선
- WorkOrderCreate 생성 화면 개선
- WorkResultList 작업 결과 목록 타입 및 표시 개선
- EstimateDetailForm 견적 폼 개선
- QuoteRegistration 견적 등록 개선
- client-management-sales-admin 거래처 관리 개선
- error-handler.ts 에러 처리 개선
2026-01-16 15:39:02 +09:00

137 lines
4.0 KiB
TypeScript

// API 에러 핸들링 헬퍼 유틸리티
// API 요청 실패 시 에러 처리 및 사용자 친화적 메시지 생성
/**
* API 에러 클래스
* - 표준 Error를 확장하여 HTTP 상태 코드와 validation errors 포함
*/
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public errors?: Record<string, string[]>
) {
super(message);
this.name = 'ApiError';
}
}
/**
* 품목코드 중복 에러 클래스
* - 백엔드에서 400 에러와 함께 duplicate_id, duplicate_code 반환 시 사용
* - 2025-12-11: 백엔드 DuplicateCodeException 대응
*/
export class DuplicateCodeError extends ApiError {
constructor(
public message: string,
public duplicateId: number,
public duplicateCode: string
) {
super(400, message);
this.name = 'DuplicateCodeError';
}
}
/**
* API 응답 에러를 처리하고 ApiError를 throw
* @param response - fetch Response 객체
* @throws {ApiError} HTTP 상태 코드, 메시지, validation errors 포함
*/
export const handleApiError = async (response: Response): Promise<never> => {
const data = await response.json().catch(() => ({}));
// 401 Unauthorized - 토큰 만료 또는 인증 실패
// ✅ 자동으로 로그인 페이지로 리다이렉트
if (response.status === 401) {
console.warn('⚠️ 401 Unauthorized - 로그인 페이지로 이동합니다.');
// 클라이언트 사이드에서만 리다이렉트
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
throw new ApiError(
401,
data.message || '인증이 필요합니다. 로그인 상태를 확인해주세요.',
data.errors
);
}
// 403 Forbidden - 권한 없음
if (response.status === 403) {
throw new ApiError(
403,
data.message || '접근 권한이 없습니다.',
data.errors
);
}
// 400 Bad Request - 품목코드 중복 에러 체크
// 백엔드 DuplicateCodeException이 duplicate_id, duplicate_code 반환
if (response.status === 400 && data.duplicate_id) {
console.warn('⚠️ 품목코드 중복 감지:', {
duplicateId: data.duplicate_id,
duplicateCode: data.duplicate_code,
message: data.message
});
throw new DuplicateCodeError(
data.message || '해당 품목코드가 이미 존재합니다.',
data.duplicate_id,
data.duplicate_code
);
}
// 422 Unprocessable Entity - Validation 에러
if (response.status === 422) {
// 상세 validation 에러 로그 출력
console.error('🔴 [API 422 Validation Error]', {
message: data.message,
errors: data.errors,
fullResponse: data
});
throw new ApiError(
422,
data.message || '입력값을 확인해주세요.',
data.errors
);
}
// 기타 에러
throw new ApiError(
response.status,
data.message || '서버 오류가 발생했습니다',
data.errors
);
};
/**
* 디버그 모드 설정
* - true: 에러 코드 표시 (개발/테스트)
* - false: 메시지만 표시 (프로덕션)
*
* TODO: 프로덕션 배포 시 false로 변경하거나 환경변수 사용
*/
const SHOW_ERROR_CODE = true;
/**
* 에러 객체에서 사용자 친화적인 메시지 추출
* @param error - 발생한 에러 객체 (ApiError, Error, unknown)
* @param includeCode - 에러 코드 포함 여부 (기본값: SHOW_ERROR_CODE 설정 따름)
* @returns 사용자에게 표시할 에러 메시지
*/
export const getErrorMessage = (error: unknown, includeCode?: boolean): string => {
const showCode = includeCode ?? SHOW_ERROR_CODE;
if (error instanceof DuplicateCodeError) {
return showCode
? `[${error.status}] ${error.message} (코드: ${error.duplicateCode})`
: error.message;
}
if (error instanceof ApiError) {
return showCode ? `[${error.status}] ${error.message}` : error.message;
}
if (error instanceof Error) {
return error.message;
}
return '알 수 없는 오류가 발생했습니다';
};