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:
@@ -69,6 +69,7 @@ import {
|
||||
APPROVAL_STATUS_LABELS,
|
||||
APPROVAL_STATUS_COLORS,
|
||||
} from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// ===== 통계 타입 =====
|
||||
interface InboxSummary {
|
||||
@@ -142,6 +143,7 @@ export function ApprovalBox() {
|
||||
setTotalCount(result.total);
|
||||
setTotalPages(result.lastPage);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load inbox:', error);
|
||||
toast.error('결재함 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
@@ -155,6 +157,7 @@ export function ApprovalBox() {
|
||||
const result = await getInboxSummary();
|
||||
setSummary(result);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load summary:', error);
|
||||
}
|
||||
}, []);
|
||||
@@ -240,6 +243,7 @@ export function ApprovalBox() {
|
||||
toast.error(result.error || '승인 처리에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Approve error:', error);
|
||||
toast.error('승인 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -277,6 +281,7 @@ export function ApprovalBox() {
|
||||
toast.error(result.error || '반려 처리에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Reject error:', error);
|
||||
toast.error('반려 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Table,
|
||||
@@ -63,10 +63,7 @@ export function ExpenseEstimateForm({ data, onChange, isLoading }: ExpenseEstima
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">지출 예상 내역서 목록</h3>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">항목을 불러오는 중...</span>
|
||||
</div>
|
||||
<ContentLoadingSpinner text="항목을 불러오는 중..." />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { useState, useCallback, useEffect, useTransition, useRef } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
import { FileText, Trash2, Send, Save, ArrowLeft, Eye, Loader2 } from 'lucide-react';
|
||||
import { FileText, Trash2, Send, Save, ArrowLeft, Eye } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
getExpenseEstimateItems,
|
||||
@@ -38,6 +40,7 @@ import type {
|
||||
ExpenseReportData,
|
||||
ExpenseEstimateData,
|
||||
} from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// 초기 데이터 - SSR에서는 빈 문자열, 클라이언트에서 날짜 설정
|
||||
const getInitialBasicInfo = (): BasicInfo => ({
|
||||
@@ -137,6 +140,7 @@ export function DocumentCreate() {
|
||||
router.back();
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load document:', error);
|
||||
toast.error('문서를 불러오는데 실패했습니다.');
|
||||
router.back();
|
||||
@@ -186,6 +190,7 @@ export function DocumentCreate() {
|
||||
router.back();
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load document for copy:', error);
|
||||
toast.error('원본 문서를 불러오는데 실패했습니다.');
|
||||
router.back();
|
||||
@@ -211,6 +216,7 @@ export function DocumentCreate() {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load expense estimate items:', error);
|
||||
toast.error('비용견적서 항목을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
@@ -259,6 +265,7 @@ export function DocumentCreate() {
|
||||
toast.error(result.error || '문서 삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Delete error:', error);
|
||||
toast.error('문서 삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -304,6 +311,7 @@ export function DocumentCreate() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Submit error:', error);
|
||||
toast.error('문서 상신 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -341,6 +349,7 @@ export function DocumentCreate() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Save draft error:', error);
|
||||
toast.error('저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -463,9 +472,7 @@ export function DocumentCreate() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
<ContentLoadingSpinner text="문서를 불러오는 중..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import type {
|
||||
SortOption,
|
||||
FilterOption,
|
||||
} from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import {
|
||||
SORT_OPTIONS,
|
||||
FILTER_OPTIONS,
|
||||
@@ -119,6 +120,7 @@ export function DraftBox() {
|
||||
setTotalCount(result.total);
|
||||
setTotalPages(result.lastPage);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load drafts:', error);
|
||||
toast.error('기안함 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
@@ -132,6 +134,7 @@ export function DraftBox() {
|
||||
const result = await getDraftsSummary();
|
||||
setSummary(result);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load summary:', error);
|
||||
}
|
||||
}, []);
|
||||
@@ -186,6 +189,7 @@ export function DraftBox() {
|
||||
toast.error(result.error || '상신에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Submit error:', error);
|
||||
toast.error('상신 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -208,6 +212,7 @@ export function DraftBox() {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Delete error:', error);
|
||||
toast.error('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -227,6 +232,7 @@ export function DraftBox() {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Delete error:', error);
|
||||
toast.error('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -306,6 +312,7 @@ export function DraftBox() {
|
||||
toast.error(result.error || '상신에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Submit error:', error);
|
||||
toast.error('상신 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
READ_STATUS_LABELS,
|
||||
READ_STATUS_COLORS,
|
||||
} from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// ===== 통계 타입 =====
|
||||
interface ReferenceSummary {
|
||||
@@ -132,6 +133,7 @@ export function ReferenceBox() {
|
||||
setTotalCount(result.total);
|
||||
setTotalPages(result.lastPage);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load references:', error);
|
||||
toast.error('참조함 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
@@ -145,6 +147,7 @@ export function ReferenceBox() {
|
||||
const result = await getReferenceSummary();
|
||||
setSummary(result);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Failed to load summary:', error);
|
||||
}
|
||||
}, []);
|
||||
@@ -220,6 +223,7 @@ export function ReferenceBox() {
|
||||
toast.error(result.error || '열람 처리에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Mark read error:', error);
|
||||
toast.error('열람 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -250,6 +254,7 @@ export function ReferenceBox() {
|
||||
toast.error(result.error || '미열람 처리에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Mark unread error:', error);
|
||||
toast.error('미열람 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user