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:
@@ -5,6 +5,7 @@ import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { CalendarDays, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
MONTH_OPTIONS,
|
||||
DAY_OPTIONS,
|
||||
} from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
export function LeavePolicyManagement() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -89,9 +91,7 @@ export function LeavePolicyManagement() {
|
||||
description="휴가 정책을 관리합니다"
|
||||
icon={CalendarDays}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<ContentLoadingSpinner text="휴가 정책을 불러오는 중..." />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import { getPayments } from './actions';
|
||||
import type { PaymentHistory, SortOption } from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// ===== Props 타입 =====
|
||||
interface PaymentHistoryClientProps {
|
||||
@@ -61,6 +62,7 @@ export function PaymentHistoryClient({
|
||||
setTotalItems(result.pagination.total);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PaymentHistoryClient] Page change error:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
RotateCcw,
|
||||
Loader2,
|
||||
} 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';
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
denyAllPermissions,
|
||||
resetPermissions,
|
||||
} from './actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// 플랫 배열을 트리 구조로 변환
|
||||
interface FlatMenuItem {
|
||||
@@ -189,6 +191,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
toast.error(error instanceof Error ? error.message : '데이터 로드 실패');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -239,6 +242,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
toast.error(result.error || '역할 생성 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
toast.error(error instanceof Error ? error.message : '저장 중 오류 발생');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
@@ -264,6 +268,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
toast.error(result.error || '역할 수정 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
toast.error(error instanceof Error ? error.message : '수정 중 오류 발생');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
@@ -293,6 +298,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
toast.error(result.error || '권한 변경 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
toast.error(error instanceof Error ? error.message : '권한 변경 중 오류 발생');
|
||||
} finally {
|
||||
setIsTogglingPermission(false);
|
||||
@@ -316,6 +322,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
toast.error(result.error || '전체 허용 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
toast.error(error instanceof Error ? error.message : '전체 허용 중 오류 발생');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
@@ -339,6 +346,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
toast.error(result.error || '전체 거부 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
toast.error(error instanceof Error ? error.message : '전체 거부 중 오류 발생');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
@@ -362,6 +370,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
toast.error(result.error || '초기화 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
toast.error(error instanceof Error ? error.message : '초기화 중 오류 발생');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
@@ -384,6 +393,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
toast.error(result.error || '역할 삭제 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
toast.error(error instanceof Error ? error.message : '삭제 중 오류 발생');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
@@ -454,10 +464,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">로딩 중...</span>
|
||||
</div>
|
||||
<ContentLoadingSpinner text="권한 정보를 불러오는 중..." />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -572,6 +579,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
|
||||
toast.error(result.error || '숨김 설정 변경 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
setIsHidden(!checked);
|
||||
toast.error('숨김 설정 변경 중 오류 발생');
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
@@ -402,12 +403,7 @@ export function PermissionManagement() {
|
||||
|
||||
// ===== 로딩/에러 상태 =====
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
);
|
||||
return <ContentLoadingSpinner text="권한 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -127,6 +127,7 @@ export function PopupForm({ mode, initialData }: PopupFormProps) {
|
||||
setErrors({ submit: result.error || '저장에 실패했습니다.' });
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('Submit error:', error);
|
||||
setErrors({ submit: '서버 오류가 발생했습니다.' });
|
||||
} finally {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@/components/templates/IntegratedListTemplateV2';
|
||||
import { type Popup } from './types';
|
||||
import { deletePopup, deletePopups } from './actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
|
||||
@@ -117,27 +118,37 @@ export function PopupList({ initialData }: PopupListProps) {
|
||||
}, []);
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (deleteTargetId) {
|
||||
const result = await deletePopup(deleteTargetId);
|
||||
if (result.success) {
|
||||
setPopups((prev) => prev.filter((p) => p.id !== deleteTargetId));
|
||||
} else {
|
||||
console.error('[PopupList] Delete failed:', result.error);
|
||||
try {
|
||||
if (deleteTargetId) {
|
||||
const result = await deletePopup(deleteTargetId);
|
||||
if (result.success) {
|
||||
setPopups((prev) => prev.filter((p) => p.id !== deleteTargetId));
|
||||
} else {
|
||||
console.error('[PopupList] Delete failed:', result.error);
|
||||
}
|
||||
}
|
||||
setShowDeleteDialog(false);
|
||||
setDeleteTargetId(null);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PopupList] handleConfirmDelete error:', error);
|
||||
}
|
||||
setShowDeleteDialog(false);
|
||||
setDeleteTargetId(null);
|
||||
}, [deleteTargetId]);
|
||||
|
||||
const handleBulkDelete = useCallback(async () => {
|
||||
const ids = Array.from(selectedItems);
|
||||
const result = await deletePopups(ids);
|
||||
if (result.success) {
|
||||
setPopups((prev) => prev.filter((p) => !selectedItems.has(p.id)));
|
||||
} else {
|
||||
console.error('[PopupList] Bulk delete failed:', result.error);
|
||||
try {
|
||||
const ids = Array.from(selectedItems);
|
||||
const result = await deletePopups(ids);
|
||||
if (result.success) {
|
||||
setPopups((prev) => prev.filter((p) => !selectedItems.has(p.id)));
|
||||
} else {
|
||||
console.error('[PopupList] Bulk delete failed:', result.error);
|
||||
}
|
||||
setSelectedItems(new Set());
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PopupList] handleBulkDelete error:', error);
|
||||
}
|
||||
setSelectedItems(new Set());
|
||||
}, [selectedItems]);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Award, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
deleteRank,
|
||||
reorderRanks,
|
||||
} from './actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
export function RankManagement() {
|
||||
// 직급 데이터
|
||||
@@ -60,6 +62,7 @@ export function RankManagement() {
|
||||
toast.error(result.error || '직급 목록을 불러오는데 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('직급 목록 조회 실패:', error);
|
||||
toast.error('직급 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
@@ -87,6 +90,7 @@ export function RankManagement() {
|
||||
toast.error(result.error || '직급 추가에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('직급 추가 실패:', error);
|
||||
toast.error('직급 추가에 실패했습니다.');
|
||||
} finally {
|
||||
@@ -121,6 +125,7 @@ export function RankManagement() {
|
||||
toast.error(result.error || '직급 삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('직급 삭제 실패:', error);
|
||||
toast.error('직급 삭제에 실패했습니다.');
|
||||
} finally {
|
||||
@@ -145,6 +150,7 @@ export function RankManagement() {
|
||||
toast.error(result.error || '직급 수정에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('직급 수정 실패:', error);
|
||||
toast.error('직급 수정에 실패했습니다.');
|
||||
} finally {
|
||||
@@ -180,6 +186,7 @@ export function RankManagement() {
|
||||
loadRanks();
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('순서 변경 실패:', error);
|
||||
toast.error('순서 변경에 실패했습니다.');
|
||||
// 실패시 원래 순서로 복구
|
||||
@@ -254,10 +261,7 @@ export function RankManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">로딩 중...</span>
|
||||
</div>
|
||||
<ContentLoadingSpinner text="직급 목록을 불러오는 중..." />
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{ranks.map((rank, index) => (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Briefcase, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
deleteTitle,
|
||||
reorderTitles,
|
||||
} from './actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
export function TitleManagement() {
|
||||
// 직책 데이터
|
||||
@@ -60,6 +62,7 @@ export function TitleManagement() {
|
||||
toast.error(result.error || '직책 목록을 불러오는데 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('직책 목록 조회 실패:', error);
|
||||
toast.error('직책 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
@@ -87,6 +90,7 @@ export function TitleManagement() {
|
||||
toast.error(result.error || '직책 추가에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('직책 추가 실패:', error);
|
||||
toast.error('직책 추가에 실패했습니다.');
|
||||
} finally {
|
||||
@@ -121,6 +125,7 @@ export function TitleManagement() {
|
||||
toast.error(result.error || '직책 삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('직책 삭제 실패:', error);
|
||||
toast.error('직책 삭제에 실패했습니다.');
|
||||
} finally {
|
||||
@@ -145,6 +150,7 @@ export function TitleManagement() {
|
||||
toast.error(result.error || '직책 수정에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('직책 수정 실패:', error);
|
||||
toast.error('직책 수정에 실패했습니다.');
|
||||
} finally {
|
||||
@@ -180,6 +186,7 @@ export function TitleManagement() {
|
||||
loadTitles();
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('순서 변경 실패:', error);
|
||||
toast.error('순서 변경에 실패했습니다.');
|
||||
// 실패시 원래 순서로 복구
|
||||
@@ -254,10 +261,7 @@ export function TitleManagement() {
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">로딩 중...</span>
|
||||
</div>
|
||||
<ContentLoadingSpinner text="직책 목록을 불러오는 중..." />
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{titles.map((title, index) => (
|
||||
|
||||
Reference in New Issue
Block a user