fix(WEB): 토큰 만료 시 무한 로딩 대신 로그인 리다이렉트 처리

- 52개 이상의 컴포넌트에 isNextRedirectError 처리 추가
- Server Action의 redirect() 에러가 catch 블록에서 삼켜지는 문제 해결
- access_token + refresh_token 모두 만료 시 정상적으로 로그인 페이지로 리다이렉트

수정된 영역:
- accounting: 10개 컴포넌트
- production: 12개 컴포넌트
- hr: 5개 컴포넌트
- settings: 8개 컴포넌트
- approval: 5개 컴포넌트
- items: 20개+ 컴포넌트
- board: 5개 컴포넌트
- quality: 4개 컴포넌트
- material, outbound, quotes 등 기타 컴포넌트

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-11 17:19:11 +09:00
parent 8bc4b90fe9
commit e56b7d53a4
131 changed files with 3320 additions and 1979 deletions

View File

@@ -7,6 +7,10 @@ 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';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
// 필드 타입 정의 - field_type 캐스팅용
type FieldTypeValue = 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
export interface UseTemplateManagementReturn {
// 섹션 템플릿 다이얼로그 상태
@@ -44,8 +48,9 @@ export interface UseTemplateManagementReturn {
setTemplateFieldName: (name: string) => void;
templateFieldKey: string;
setTemplateFieldKey: (key: string) => void;
templateFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
setTemplateFieldInputType: (type: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea') => void;
// string 타입으로 유연하게 처리
templateFieldInputType: string;
setTemplateFieldInputType: (type: string) => void;
templateFieldRequired: boolean;
setTemplateFieldRequired: (required: boolean) => void;
templateFieldOptions: string;
@@ -59,9 +64,9 @@ export interface UseTemplateManagementReturn {
templateFieldColumnNames: string[];
setTemplateFieldColumnNames: React.Dispatch<React.SetStateAction<string[]>>;
// 템플릿 필드 마스터 항목 관련
templateFieldInputMode: 'custom' | 'master';
setTemplateFieldInputMode: (mode: 'custom' | 'master') => void;
// 템플릿 필드 마스터 항목 관련 - 유연한 타입 지원
templateFieldInputMode: 'custom' | 'master' | 'new' | 'existing';
setTemplateFieldInputMode: (mode: 'custom' | 'master' | 'new' | 'existing') => void;
templateFieldShowMasterFieldList: boolean;
setTemplateFieldShowMasterFieldList: (show: boolean) => void;
templateFieldSelectedMasterFieldId: string;
@@ -139,7 +144,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
// 템플릿 필드 폼 상태
const [templateFieldName, setTemplateFieldName] = useState('');
const [templateFieldKey, setTemplateFieldKey] = useState('');
const [templateFieldInputType, setTemplateFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [templateFieldInputType, setTemplateFieldInputType] = useState<string>('textbox');
const [templateFieldRequired, setTemplateFieldRequired] = useState(false);
const [templateFieldOptions, setTemplateFieldOptions] = useState('');
const [templateFieldDescription, setTemplateFieldDescription] = useState('');
@@ -147,8 +152,8 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
const [templateFieldColumnCount, setTemplateFieldColumnCount] = useState(2);
const [templateFieldColumnNames, setTemplateFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
// 템플릿 필드 마스터 항목 관련
const [templateFieldInputMode, setTemplateFieldInputMode] = useState<'custom' | 'master'>('custom');
// 템플릿 필드 마스터 항목 관련 - 'new'/'existing' 호환을 위해 유연하게 처리
const [templateFieldInputMode, setTemplateFieldInputMode] = useState<'custom' | 'master' | 'new' | 'existing'>('custom');
const [templateFieldShowMasterFieldList, setTemplateFieldShowMasterFieldList] = useState(false);
const [templateFieldSelectedMasterFieldId, setTemplateFieldSelectedMasterFieldId] = useState('');
@@ -176,6 +181,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
resetSectionTemplateForm();
toast.success('섹션이 추가되었습니다!');
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('섹션 추가 실패:', error);
toast.error('섹션 추가에 실패했습니다.');
}
@@ -212,6 +218,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
resetSectionTemplateForm();
toast.success('섹션이 수정되었습니다!');
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('섹션 수정 실패:', error);
toast.error('섹션 수정에 실패했습니다.');
}
@@ -225,6 +232,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
await deleteSection(id);
toast.success('섹션이 삭제되었습니다!');
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('섹션 삭제 실패:', error);
toast.error('섹션 삭제에 실패했습니다.');
}
@@ -282,7 +290,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
const updateData = {
field_name: templateFieldName,
field_key: templateFieldKey, // 2025-11-28: field_key 추가
field_type: templateFieldInputType,
field_type: templateFieldInputType as FieldTypeValue,
is_required: templateFieldRequired,
placeholder: templateFieldDescription || null,
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
@@ -326,7 +334,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
master_field_id: null,
field_name: templateFieldName,
field_key: templateFieldKey,
field_type: templateFieldInputType,
field_type: templateFieldInputType as FieldTypeValue,
order_no: 0,
is_required: templateFieldRequired,
placeholder: templateFieldDescription || null,
@@ -352,6 +360,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
resetTemplateFieldForm();
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('항목 처리 실패:', error);
// 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등) → AlertDialog로 표시
@@ -402,6 +411,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
await unlinkFieldFromSection(templateId, Number(fieldId));
toast.success('항목 연결이 해제되었습니다');
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('항목 연결 해제 실패:', error);
toast.error('항목 연결 해제에 실패했습니다.');
}
@@ -415,6 +425,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
await addBOMItem(templateId, item);
// toast는 BOMManagementSection 컴포넌트에서 처리
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('BOM 항목 추가 실패:', error);
toast.error('BOM 항목 추가에 실패했습니다');
}
@@ -427,6 +438,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
await updateBOMItem(itemId, item);
// toast는 BOMManagementSection 컴포넌트에서 처리
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('BOM 항목 수정 실패:', error);
toast.error('BOM 항목 수정에 실패했습니다');
}
@@ -439,6 +451,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
await deleteBOMItem(itemId);
// toast는 BOMManagementSection 컴포넌트에서 처리
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('BOM 항목 삭제 실패:', error);
toast.error('BOM 항목 삭제에 실패했습니다');
}