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:
@@ -10,7 +10,8 @@
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ClipboardCheck, Calendar, Loader2 } from 'lucide-react';
|
||||
import { ClipboardCheck, Calendar } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -27,6 +28,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { getReceivings } from './actions';
|
||||
import type { InspectionCheckItem, ReceivingItem } from './types';
|
||||
import { SuccessDialog } from './SuccessDialog';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// LOT 번호 생성 함수 (YYMMDD-NN 형식)
|
||||
function generateLotNo(): string {
|
||||
@@ -96,6 +98,7 @@ export function InspectionCreate({ id }: Props) {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
console.error('[InspectionCreate] loadTargets error:', err);
|
||||
} finally {
|
||||
setIsLoadingTargets(false);
|
||||
@@ -206,9 +209,7 @@ export function InspectionCreate({ id }: Props) {
|
||||
<Label className="text-sm font-medium">검사 대상 선택</Label>
|
||||
<div className="space-y-2 border rounded-lg p-2 bg-white min-h-[200px]">
|
||||
{isLoadingTargets ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
||||
</div>
|
||||
<ContentLoadingSpinner text="검사 대상을 불러오는 중..." />
|
||||
) : inspectionTargets.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground text-sm">
|
||||
검사 대기 중인 입고 건이 없습니다.
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Package, FileText, List, ClipboardCheck, Download, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { Package, FileText, List, ClipboardCheck, Download, AlertCircle } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -22,6 +23,7 @@ import type { ReceivingDetail as ReceivingDetailType, ReceivingProcessFormData }
|
||||
import { ReceivingProcessDialog } from './ReceivingProcessDialog';
|
||||
import { ReceivingReceiptDialog } from './ReceivingReceiptDialog';
|
||||
import { SuccessDialog } from './SuccessDialog';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -56,6 +58,7 @@ export function ReceivingDetail({ id }: Props) {
|
||||
setError(result.error || '입고 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
console.error('[ReceivingDetail] loadData error:', err);
|
||||
setError('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -100,6 +103,7 @@ export function ReceivingDetail({ id }: Props) {
|
||||
alert(result.error || '입고처리에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
console.error('[ReceivingDetail] handleReceivingComplete error:', err);
|
||||
alert('입고처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
@@ -115,9 +119,7 @@ export function ReceivingDetail({ id }: Props) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
<ContentLoadingSpinner text="입고 정보를 불러오는 중..." />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
ClipboardCheck,
|
||||
Calendar,
|
||||
Eye,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import { getReceivings, getReceivingStats } from './actions';
|
||||
import { RECEIVING_STATUS_LABELS, RECEIVING_STATUS_STYLES } from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import type { ReceivingItem, ReceivingStats } from './types';
|
||||
|
||||
// 페이지당 항목 수
|
||||
@@ -87,6 +88,7 @@ export function ReceivingList() {
|
||||
setStats(statsResult.data);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
console.error('[ReceivingList] loadData error:', err);
|
||||
setError('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -317,11 +319,7 @@ export function ReceivingList() {
|
||||
|
||||
// 로딩 상태
|
||||
if (isLoading && items.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
return <ContentLoadingSpinner text="입고 목록을 불러오는 중..." />;
|
||||
}
|
||||
|
||||
// 에러 상태
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Package, AlertCircle, Loader2, List } from 'lucide-react';
|
||||
import { Package, AlertCircle, List } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
LOT_STATUS_LABELS,
|
||||
} from './types';
|
||||
import type { StockDetail, LotDetail } from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
interface StockStatusDetailProps {
|
||||
id: string;
|
||||
@@ -55,6 +57,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
|
||||
setError(result.error || '재고 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
console.error('[StockStatusDetail] loadData error:', err);
|
||||
setError('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -90,9 +93,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
<ContentLoadingSpinner text="재고 정보를 불러오는 중..." />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
AlertCircle,
|
||||
Download,
|
||||
Eye,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import { getStocks, getStockStats, getStockStatsByType } from './actions';
|
||||
import { ITEM_TYPE_LABELS, ITEM_TYPE_STYLES, STOCK_STATUS_LABELS } from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import type { StockItem, StockStats, ItemType } from './types';
|
||||
|
||||
// 페이지당 항목 수
|
||||
@@ -84,6 +85,7 @@ export function StockStatusList() {
|
||||
setTypeStats(typeStatsResult.data);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
console.error('[StockStatusList] loadData error:', err);
|
||||
setError('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -332,11 +334,7 @@ export function StockStatusList() {
|
||||
|
||||
// 로딩 상태 표시 (모든 Hooks 선언 후)
|
||||
if (isLoading && items.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
return <ContentLoadingSpinner text="재고 목록을 불러오는 중..." />;
|
||||
}
|
||||
|
||||
// 에러 상태 표시
|
||||
|
||||
Reference in New Issue
Block a user