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

@@ -11,8 +11,8 @@ import {
Plus,
FileText,
Edit,
Loader2,
} from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
@@ -57,6 +57,7 @@ import {
SORT_OPTIONS,
FILTER_OPTIONS,
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
export function AttendanceManagement() {
const router = useRouter();
@@ -108,6 +109,7 @@ export function AttendanceManagement() {
setAttendanceRecords(attendancesResult.data);
setTotal(attendancesResult.total);
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[AttendanceManagement] fetchData error:', error);
} finally {
setIsLoading(false);
@@ -340,6 +342,7 @@ export function AttendanceManagement() {
}
setAttendanceDialogOpen(false);
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('Save attendance error:', error);
} finally {
setIsSaving(false);
@@ -552,11 +555,7 @@ export function AttendanceManagement() {
// 로딩 상태
if (isLoading) {
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="근태 정보를 불러오는 중..." />;
}
return (

View File

@@ -28,6 +28,7 @@ import {
deleteDepartmentsMany,
type DepartmentRecord,
} from './actions';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
/**
* API 응답을 로컬 Department 타입으로 변환
@@ -77,6 +78,7 @@ export function DepartmentManagement() {
console.error('[DepartmentManagement] fetchDepartments error:', result.error);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DepartmentManagement] fetchDepartments error:', error);
} finally {
setIsLoading(false);
@@ -221,6 +223,7 @@ export function DepartmentManagement() {
}
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DepartmentManagement] confirmDelete error:', error);
} finally {
setDeleteDialogOpen(false);
@@ -260,6 +263,7 @@ export function DepartmentManagement() {
}
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DepartmentManagement] handleDialogSubmit error:', error);
} finally {
setDialogOpen(false);

View File

@@ -46,6 +46,7 @@ import {
DEFAULT_FIELD_SETTINGS,
USER_ROLE_LABELS,
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
// 필터 옵션 타입
type FilterOption = 'all' | 'hasUserId' | 'noUserId' | 'active' | 'leave' | 'resigned';
@@ -116,6 +117,7 @@ export function EmployeeManagement() {
setEmployees(result.data);
setTotal(result.total);
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[EmployeeManagement] fetchEmployees error:', error);
} finally {
setIsLoading(false);
@@ -312,6 +314,7 @@ export function EmployeeManagement() {
console.error('Bulk delete failed:', result.error);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('Bulk delete error:', error);
} finally {
setIsDeleting(false);
@@ -341,6 +344,7 @@ export function EmployeeManagement() {
console.error('Delete failed:', result.error);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('Delete error:', error);
} finally {
setIsDeleting(false);

View File

@@ -53,6 +53,7 @@ import {
SORT_OPTIONS,
formatCurrency,
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
export function SalaryManagement() {
// ===== 상태 관리 =====
@@ -98,6 +99,7 @@ export function SalaryManagement() {
toast.error(result.error || '급여 목록을 불러오는데 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('loadSalaries error:', error);
toast.error('급여 목록을 불러오는데 실패했습니다.');
} finally {
@@ -147,6 +149,7 @@ export function SalaryManagement() {
toast.error(result.error || '상태 변경에 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('handleMarkCompleted error:', error);
toast.error('상태 변경에 실패했습니다.');
} finally {
@@ -173,6 +176,7 @@ export function SalaryManagement() {
toast.error(result.error || '상태 변경에 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('handleMarkScheduled error:', error);
toast.error('상태 변경에 실패했습니다.');
} finally {
@@ -193,6 +197,7 @@ export function SalaryManagement() {
toast.error(result.error || '급여 상세 정보를 불러오는데 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('handleViewDetail error:', error);
toast.error('급여 상세 정보를 불러오는데 실패했습니다.');
} finally {
@@ -215,6 +220,7 @@ export function SalaryManagement() {
toast.error(result.error || '저장에 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('handleSaveDetail error:', error);
toast.error('저장에 실패했습니다.');
} finally {

View File

@@ -74,6 +74,7 @@ import {
REQUEST_STATUS_LABELS,
REQUEST_STATUS_COLORS,
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
// ===== Mock 데이터 생성 (request 탭용 - 신청 현황은 leaves API 사용 예정) =====
@@ -170,6 +171,7 @@ export function VacationManagement() {
setUsageData(converted);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[VacationManagement] fetchUsageData error:', error);
} finally {
setIsLoading(false);
@@ -204,6 +206,7 @@ export function VacationManagement() {
setGrantData(converted);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[VacationManagement] fetchGrantData error:', error);
} finally {
setIsLoading(false);
@@ -246,6 +249,7 @@ export function VacationManagement() {
setRequestData(converted);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[VacationManagement] fetchLeaveRequests error:', error);
} finally {
setIsLoading(false);
@@ -344,6 +348,7 @@ export function VacationManagement() {
console.error('[VacationManagement] 승인 실패:', result.error);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[VacationManagement] handleApproveConfirm error:', error);
} finally {
setSelectedItems(new Set());
@@ -369,6 +374,7 @@ export function VacationManagement() {
console.error('[VacationManagement] 반려 실패:', result.error);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[VacationManagement] handleRejectConfirm error:', error);
} finally {
setSelectedItems(new Set());
@@ -750,6 +756,7 @@ export function VacationManagement() {
console.error('[VacationManagement] 휴가 부여 실패:', result.error);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[VacationManagement] 휴가 부여 에러:', error);
alert('휴가 부여 중 오류가 발생했습니다.');
} finally {
@@ -779,6 +786,7 @@ export function VacationManagement() {
console.error('[VacationManagement] 휴가 신청 실패:', result.error);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[VacationManagement] 휴가 신청 에러:', error);
alert('휴가 신청 중 오류가 발생했습니다.');
} finally {