diff --git a/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md b/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md index 82ded6b0..b540a9f5 100644 --- a/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md +++ b/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md @@ -310,6 +310,7 @@ Safari에서 `SameSite=Strict` + `Secure` 조합이 localhost에서 쿠키 저 |------|------|--------| | `src/lib/api/refresh-token.ts` | 공통 토큰 갱신 모듈 (캐싱 로직) | 2025-12-30 | | `src/lib/api/fetch-wrapper.ts` | Server Actions용 fetch wrapper | 2025-12-30 | +| `src/lib/utils/redirect-error.ts` | Next.js redirect 에러 감지 유틸리티 | 2026-01-08 | | `src/app/api/proxy/[...path]/route.ts` | 클라이언트 API 프록시 | 2025-12-30 | | `src/app/api/auth/login/route.ts` | 로그인 및 초기 토큰 설정 | - | | `src/app/api/auth/check/route.ts` | 인증 상태 확인 API | 2026-01-08 | @@ -412,4 +413,90 @@ if (refreshCache.result && !refreshCache.result.success) { refreshCache.promise = null; refreshCache.result = null; } -``` \ No newline at end of file +``` + +### 10.4 [2026-01-08] isRedirectError 자체 유틸리티 함수로 변경 + +**문제:** +Next.js 내부 경로(`next/dist/client/components/redirect`)가 버전 15에서 `redirect-error`로 변경됨. +내부 경로 의존 시 Next.js 업데이트마다 수정 필요. + +**해결:** +자체 유틸리티 함수 생성하여 Next.js 내부 경로 의존성 제거: + +```typescript +// src/lib/utils/redirect-error.ts +export function isNextRedirectError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'digest' in error && + typeof (error as { digest: string }).digest === 'string' && + (error as { digest: string }).digest.startsWith('NEXT_REDIRECT') + ); +} +``` + +**장점:** +- Next.js 버전 업데이트에 영향 안 받음 +- 내부 경로 의존성 제거 +- 한 곳에서 관리 가능 + +--- + +## 11. 신규 Server Actions 개발 가이드 + +### 11.1 필수 패턴 + +새로운 `actions.ts` 파일 생성 시 반드시 아래 패턴을 따라야 합니다: + +```typescript +'use server'; + +import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { serverFetch } from '@/lib/api/fetch-wrapper'; + +export async function someAction(params: SomeParams): Promise { + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/some-endpoint`; + + const { response, error } = await serverFetch(url, { + method: 'GET', // 또는 POST, PUT, DELETE + }); + + if (error || !response) { + return { success: false, error: error?.message || '요청 실패' }; + } + + const data = await response.json(); + return { success: true, data }; + + } catch (error) { + // ⚠️ 필수: redirect 에러는 다시 throw해야 함 + if (isNextRedirectError(error)) throw error; + + console.error('[SomeAction] error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} +``` + +### 11.2 왜 isNextRedirectError 처리가 필수인가? + +``` +serverFetch에서 401 응답 시: +1. refresh_token으로 토큰 갱신 시도 +2. 갱신 실패 시 redirect('/login') 호출 +3. redirect()는 NEXT_REDIRECT 에러를 throw +4. 이 에러가 catch에서 잡히면 → { success: false } 반환 → 무한 루프 +5. 이 에러를 다시 throw하면 → Next.js가 정상 리다이렉트 처리 +``` + +### 11.3 체크리스트 + +새 actions.ts 파일 생성 시: + +- [ ] `import { isNextRedirectError } from '@/lib/utils/redirect-error';` 추가 +- [ ] `import { serverFetch } from '@/lib/api/fetch-wrapper';` 사용 +- [ ] 모든 catch 블록에 `if (isNextRedirectError(error)) throw error;` 추가 +- [ ] 파일 내 모든 export 함수에 동일 패턴 적용 \ No newline at end of file diff --git a/src/components/accounting/BadDebtCollection/actions.ts b/src/components/accounting/BadDebtCollection/actions.ts index 74711487..715dbd32 100644 --- a/src/components/accounting/BadDebtCollection/actions.ts +++ b/src/components/accounting/BadDebtCollection/actions.ts @@ -14,7 +14,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { revalidatePath } from 'next/cache'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { BadDebtRecord, CollectionStatus } from './types'; @@ -288,7 +288,7 @@ export async function getBadDebts(params?: { return result.data.data.map(transformApiToFrontend); } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] getBadDebts error:', error); return []; } @@ -322,7 +322,7 @@ export async function getBadDebtById(id: string): Promise return transformApiToFrontend(result.data); } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] getBadDebtById error:', error); return null; } @@ -356,7 +356,7 @@ export async function getBadDebtSummary(): Promise return result.data; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] getBadDebtSummary error:', error); return null; } @@ -401,7 +401,7 @@ export async function createBadDebt( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] createBadDebt error:', error); return { success: false, @@ -450,7 +450,7 @@ export async function updateBadDebt( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] updateBadDebt error:', error); return { success: false, @@ -486,7 +486,7 @@ export async function deleteBadDebt(id: string): Promise<{ success: boolean; err return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] deleteBadDebt error:', error); return { success: false, @@ -525,7 +525,7 @@ export async function toggleBadDebt(id: string): Promise<{ success: boolean; dat data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] toggleBadDebt error:', error); return { success: false, @@ -574,7 +574,7 @@ export async function addBadDebtMemo( }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] addBadDebtMemo error:', error); return { success: false, @@ -611,7 +611,7 @@ export async function deleteBadDebtMemo( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BadDebtActions] deleteBadDebtMemo error:', error); return { success: false, diff --git a/src/components/accounting/BankTransactionInquiry/actions.ts b/src/components/accounting/BankTransactionInquiry/actions.ts index 04a02af1..d02dee21 100644 --- a/src/components/accounting/BankTransactionInquiry/actions.ts +++ b/src/components/accounting/BankTransactionInquiry/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { BankTransaction, TransactionKind } from './types'; @@ -157,7 +157,7 @@ export async function getBankTransactionList(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BankTransactionActions] getBankTransactionList error:', error); return { success: false, @@ -226,7 +226,7 @@ export async function getBankTransactionSummary(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BankTransactionActions] getBankTransactionSummary error:', error); return { success: false, @@ -274,7 +274,7 @@ export async function getBankAccountOptions(): Promise<{ data: result.data as BankAccountOption[], }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BankTransactionActions] getBankAccountOptions error:', error); return { success: false, diff --git a/src/components/accounting/BillManagement/actions.ts b/src/components/accounting/BillManagement/actions.ts index dbbcd0db..15cbf39c 100644 --- a/src/components/accounting/BillManagement/actions.ts +++ b/src/components/accounting/BillManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { BillRecord, BillApiData, BillStatus } from './types'; import { transformApiToFrontend, transformFrontendToApi } from './types'; @@ -105,7 +105,7 @@ export async function getBills(params: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getBills] Error:', error); return { success: false, @@ -148,7 +148,7 @@ export async function getBill(id: string): Promise<{ data: transformApiToFrontend(result.data as BillApiData), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getBill] Error:', error); return { success: false, error: 'Server error' }; } @@ -195,7 +195,7 @@ export async function createBill( data: transformApiToFrontend(result.data as BillApiData), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[createBill] Error:', error); return { success: false, error: 'Server error' }; } @@ -243,7 +243,7 @@ export async function updateBill( data: transformApiToFrontend(result.data as BillApiData), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[updateBill] Error:', error); return { success: false, error: 'Server error' }; } @@ -273,7 +273,7 @@ export async function deleteBill(id: string): Promise<{ success: boolean; error? return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteBill] Error:', error); return { success: false, error: 'Server error' }; } @@ -312,7 +312,7 @@ export async function updateBillStatus( data: transformApiToFrontend(result.data as BillApiData), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[updateBillStatus] Error:', error); return { success: false, error: 'Server error' }; } @@ -376,7 +376,7 @@ export async function getBillSummary(params: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getBillSummary] Error:', error); return { success: false, error: 'Server error' }; } @@ -419,7 +419,7 @@ export async function getClients(): Promise<{ })), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getClients] Error:', error); return { success: false, error: 'Server error' }; } diff --git a/src/components/accounting/CardTransactionInquiry/actions.ts b/src/components/accounting/CardTransactionInquiry/actions.ts index 587c32b0..5a3e5465 100644 --- a/src/components/accounting/CardTransactionInquiry/actions.ts +++ b/src/components/accounting/CardTransactionInquiry/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { CardTransaction } from './types'; @@ -160,7 +160,7 @@ export async function getCardTransactionList(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CardTransactionActions] getCardTransactionList error:', error); return { success: false, @@ -229,7 +229,7 @@ export async function getCardTransactionSummary(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CardTransactionActions] getCardTransactionSummary error:', error); return { success: false, @@ -281,7 +281,7 @@ export async function bulkUpdateAccountCode( updatedCount: result.data?.updated_count || 0, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CardTransactionActions] bulkUpdateAccountCode error:', error); return { success: false, diff --git a/src/components/accounting/DailyReport/actions.ts b/src/components/accounting/DailyReport/actions.ts index 492ec86b..88f8d9d2 100644 --- a/src/components/accounting/DailyReport/actions.ts +++ b/src/components/accounting/DailyReport/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { cookies } from 'next/headers'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { NoteReceivableItem, DailyAccountItem, MatchStatus } from './types'; @@ -118,7 +118,7 @@ export async function getNoteReceivables(params?: { data: items, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DailyReportActions] getNoteReceivables error:', error); return { success: false, @@ -176,7 +176,7 @@ export async function getDailyAccounts(params?: { data: items, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DailyReportActions] getDailyAccounts error:', error); return { success: false, @@ -258,7 +258,7 @@ export async function getDailyReportSummary(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DailyReportActions] getDailyReportSummary error:', error); return { success: false, @@ -315,7 +315,7 @@ export async function exportDailyReportExcel(params?: { filename, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DailyReportActions] exportDailyReportExcel error:', error); return { success: false, diff --git a/src/components/accounting/DepositManagement/actions.ts b/src/components/accounting/DepositManagement/actions.ts index 51278941..5683e1c9 100644 --- a/src/components/accounting/DepositManagement/actions.ts +++ b/src/components/accounting/DepositManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { DepositRecord, DepositType, DepositStatus } from './types'; diff --git a/src/components/accounting/ExpectedExpenseManagement/actions.ts b/src/components/accounting/ExpectedExpenseManagement/actions.ts index c900fd08..e72cd588 100644 --- a/src/components/accounting/ExpectedExpenseManagement/actions.ts +++ b/src/components/accounting/ExpectedExpenseManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { ExpectedExpenseRecord, TransactionType, PaymentStatus, ApprovalStatus } from './types'; @@ -190,7 +190,7 @@ export async function getExpectedExpenses(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getExpectedExpenses error:', error); return { success: false, @@ -239,7 +239,7 @@ export async function getExpectedExpenseById(id: string): Promise<{ data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getExpectedExpenseById error:', error); return { success: false, @@ -281,7 +281,7 @@ export async function createExpectedExpense( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] createExpectedExpense error:', error); return { success: false, @@ -324,7 +324,7 @@ export async function updateExpectedExpense( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] updateExpectedExpense error:', error); return { success: false, @@ -356,7 +356,7 @@ export async function deleteExpectedExpense(id: string): Promise<{ success: bool return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] deleteExpectedExpense error:', error); return { success: false, @@ -400,7 +400,7 @@ export async function deleteExpectedExpenses(ids: string[]): Promise<{ deletedCount: result.data?.deleted_count, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] deleteExpectedExpenses error:', error); return { success: false, @@ -448,7 +448,7 @@ export async function updateExpectedPaymentDate( updatedCount: result.data?.updated_count, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] updateExpectedPaymentDate error:', error); return { success: false, @@ -507,7 +507,7 @@ export async function getExpectedExpenseSummary(params?: { data: result.data, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getExpectedExpenseSummary error:', error); return { success: false, @@ -552,7 +552,7 @@ export async function getClients(): Promise<{ })), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getClients error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } @@ -596,7 +596,7 @@ export async function getBankAccounts(): Promise<{ })), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getBankAccounts error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/accounting/PurchaseManagement/actions.ts b/src/components/accounting/PurchaseManagement/actions.ts index f78c903b..18eb8c57 100644 --- a/src/components/accounting/PurchaseManagement/actions.ts +++ b/src/components/accounting/PurchaseManagement/actions.ts @@ -13,7 +13,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { PurchaseRecord, PurchaseType } from './types'; diff --git a/src/components/accounting/ReceivablesStatus/actions.ts b/src/components/accounting/ReceivablesStatus/actions.ts index 3c5507b9..039f485b 100644 --- a/src/components/accounting/ReceivablesStatus/actions.ts +++ b/src/components/accounting/ReceivablesStatus/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { cookies } from 'next/headers'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { VendorReceivables, CategoryType, MonthlyAmount, ReceivablesListResponse, MemoUpdateRequest } from './types'; @@ -128,7 +128,7 @@ export async function getReceivablesList(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivablesActions] getReceivablesList error:', error); return { success: false, @@ -209,7 +209,7 @@ export async function getReceivablesSummary(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivablesActions] getReceivablesSummary error:', error); return { success: false, @@ -265,7 +265,7 @@ export async function updateOverdueStatus( updatedCount: result.data?.updated_count || updates.length, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivablesActions] updateOverdueStatus error:', error); return { success: false, @@ -321,7 +321,7 @@ export async function updateMemos( updatedCount: result.data?.updated_count || memos.length, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivablesActions] updateMemos error:', error); return { success: false, @@ -388,7 +388,7 @@ export async function exportReceivablesExcel(params?: { filename, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivablesActions] exportReceivablesExcel error:', error); return { success: false, diff --git a/src/components/accounting/SalesManagement/actions.ts b/src/components/accounting/SalesManagement/actions.ts index 553361cb..f6e3fdb8 100644 --- a/src/components/accounting/SalesManagement/actions.ts +++ b/src/components/accounting/SalesManagement/actions.ts @@ -14,7 +14,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { SalesRecord, diff --git a/src/components/accounting/VendorLedger/actions.ts b/src/components/accounting/VendorLedger/actions.ts index 3f67f516..f6661865 100644 --- a/src/components/accounting/VendorLedger/actions.ts +++ b/src/components/accounting/VendorLedger/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { cookies } from 'next/headers'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { VendorLedgerItem, VendorLedgerDetail, VendorLedgerSummary, TransactionEntry } from './types'; @@ -226,7 +226,7 @@ export async function getVendorLedgerList(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[VendorLedgerActions] getVendorLedgerList error:', error); return { success: false, @@ -290,7 +290,7 @@ export async function getVendorLedgerSummary(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[VendorLedgerActions] getVendorLedgerSummary error:', error); return { success: false, @@ -354,7 +354,7 @@ export async function getVendorLedgerDetail(clientId: string, params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[VendorLedgerActions] getVendorLedgerDetail error:', error); return { success: false, @@ -415,7 +415,7 @@ export async function exportVendorLedgerExcel(params?: { filename, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[VendorLedgerActions] exportVendorLedgerExcel error:', error); return { success: false, @@ -474,7 +474,7 @@ export async function exportVendorLedgerDetailPdf(clientId: string, params?: { filename, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[VendorLedgerActions] exportVendorLedgerDetailPdf error:', error); return { success: false, diff --git a/src/components/accounting/VendorManagement/actions.ts b/src/components/accounting/VendorManagement/actions.ts index 78db567b..04a2da26 100644 --- a/src/components/accounting/VendorManagement/actions.ts +++ b/src/components/accounting/VendorManagement/actions.ts @@ -13,7 +13,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { Vendor, diff --git a/src/components/accounting/WithdrawalManagement/actions.ts b/src/components/accounting/WithdrawalManagement/actions.ts index 8ff6268b..a5c25b37 100644 --- a/src/components/accounting/WithdrawalManagement/actions.ts +++ b/src/components/accounting/WithdrawalManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { WithdrawalRecord, WithdrawalType } from './types'; diff --git a/src/components/approval/ApprovalBox/actions.ts b/src/components/approval/ApprovalBox/actions.ts index f01e0226..fa8c6779 100644 --- a/src/components/approval/ApprovalBox/actions.ts +++ b/src/components/approval/ApprovalBox/actions.ts @@ -11,7 +11,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { ApprovalRecord, ApprovalType, ApprovalStatus } from './types'; @@ -224,7 +224,7 @@ export async function getInbox(params?: { lastPage: result.data.last_page, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ApprovalBoxActions] getInbox error:', error); return { data: [], total: 0, lastPage: 1 }; } @@ -253,7 +253,7 @@ export async function getInboxSummary(): Promise { return result.data; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ApprovalBoxActions] getInboxSummary error:', error); return null; } @@ -291,7 +291,7 @@ export async function approveDocument(id: string, comment?: string): Promise<{ s return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ApprovalBoxActions] approveDocument error:', error); return { success: false, @@ -339,7 +339,7 @@ export async function rejectDocument(id: string, comment: string): Promise<{ suc return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ApprovalBoxActions] rejectDocument error:', error); return { success: false, diff --git a/src/components/approval/DocumentCreate/actions.ts b/src/components/approval/DocumentCreate/actions.ts index f8becb7e..fc2e18dc 100644 --- a/src/components/approval/DocumentCreate/actions.ts +++ b/src/components/approval/DocumentCreate/actions.ts @@ -11,7 +11,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { cookies } from 'next/headers'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { @@ -185,7 +185,7 @@ export async function uploadFiles(files: File[]): Promise<{ return { success: true, data: uploadedFiles }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] uploadFiles error:', error); return { success: false, error: '파일 업로드 중 오류가 발생했습니다.' }; } @@ -237,7 +237,7 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{ finalDifference: result.data.final_difference, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] getExpenseEstimateItems error:', error); return null; } @@ -278,7 +278,7 @@ export async function getEmployees(search?: string): Promise { return result.data.data.map(transformEmployee); } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] getEmployees error:', error); return []; } @@ -359,7 +359,7 @@ export async function createApproval(formData: DocumentFormData): Promise<{ }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] createApproval error:', error); return { success: false, @@ -402,7 +402,7 @@ export async function submitApproval(id: number): Promise<{ return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] submitApproval error:', error); return { success: false, @@ -440,7 +440,7 @@ export async function createAndSubmitApproval(formData: DocumentFormData): Promi data: createResult.data, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] createAndSubmitApproval error:', error); return { success: false, @@ -488,7 +488,7 @@ export async function getApprovalById(id: number): Promise<{ return { success: true, data: formDataResult }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] getApprovalById error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -567,7 +567,7 @@ export async function updateApproval(id: number, formData: DocumentFormData): Pr }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] updateApproval error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -602,7 +602,7 @@ export async function updateAndSubmitApproval(id: number, formData: DocumentForm data: updateResult.data, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] updateAndSubmitApproval error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -641,7 +641,7 @@ export async function deleteApproval(id: number): Promise<{ return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DocumentCreateActions] deleteApproval error:', error); return { success: false, diff --git a/src/components/approval/DraftBox/actions.ts b/src/components/approval/DraftBox/actions.ts index 5ba1dfcf..88bd718f 100644 --- a/src/components/approval/DraftBox/actions.ts +++ b/src/components/approval/DraftBox/actions.ts @@ -13,7 +13,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { DraftRecord, DocumentStatus, Approver } from './types'; @@ -227,7 +227,7 @@ export async function getDrafts(params?: { lastPage: result.data.last_page, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DraftBoxActions] getDrafts error:', error); return { data: [], total: 0, lastPage: 1 }; } @@ -256,7 +256,7 @@ export async function getDraftsSummary(): Promise { return result.data; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DraftBoxActions] getDraftsSummary error:', error); return null; } @@ -285,7 +285,7 @@ export async function getDraftById(id: string): Promise { return transformApiToFrontend(result.data); } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DraftBoxActions] getDraftById error:', error); return null; } @@ -320,7 +320,7 @@ export async function deleteDraft(id: string): Promise<{ success: boolean; error return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DraftBoxActions] deleteDraft error:', error); return { success: false, @@ -385,7 +385,7 @@ export async function submitDraft(id: string): Promise<{ success: boolean; error return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DraftBoxActions] submitDraft error:', error); return { success: false, @@ -450,7 +450,7 @@ export async function cancelDraft(id: string): Promise<{ success: boolean; error return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DraftBoxActions] cancelDraft error:', error); return { success: false, diff --git a/src/components/approval/ReferenceBox/actions.ts b/src/components/approval/ReferenceBox/actions.ts index 773430ea..d18f32c5 100644 --- a/src/components/approval/ReferenceBox/actions.ts +++ b/src/components/approval/ReferenceBox/actions.ts @@ -10,7 +10,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { ReferenceRecord, ApprovalType, DocumentStatus } from './types'; @@ -187,7 +187,7 @@ export async function getReferences(params?: { lastPage: result.data.last_page, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReferenceBoxActions] getReferences error:', error); return { data: [], total: 0, lastPage: 1 }; } @@ -209,7 +209,7 @@ export async function getReferenceSummary(): Promise<{ all: number; read: number unread: unreadResult.total, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReferenceBoxActions] getReferenceSummary error:', error); return null; } @@ -246,7 +246,7 @@ export async function markAsRead(id: string): Promise<{ success: boolean; error? return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReferenceBoxActions] markAsRead error:', error); return { success: false, @@ -286,7 +286,7 @@ export async function markAsUnread(id: string): Promise<{ success: boolean; erro return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReferenceBoxActions] markAsUnread error:', error); return { success: false, diff --git a/src/components/attendance/actions.ts b/src/components/attendance/actions.ts index 879f133d..5bd2ea66 100644 --- a/src/components/attendance/actions.ts +++ b/src/components/attendance/actions.ts @@ -10,7 +10,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch, getServerApiHeaders } from '@/lib/api/fetch-wrapper'; // ============================================ @@ -143,7 +143,7 @@ export async function checkIn( error: result.message || '출근 기록에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[checkIn] Error:', error); return { success: false, @@ -197,7 +197,7 @@ export async function checkOut( error: result.message || '퇴근 기록에 실패했습니다.', }; } catch (err) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[checkOut] Error:', err); return { success: false, @@ -253,7 +253,7 @@ export async function getTodayAttendance(): Promise<{ error: result.message || '근태 조회에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getTodayAttendance] Error:', error); return { success: false, diff --git a/src/components/board/BoardManagement/actions.ts b/src/components/board/BoardManagement/actions.ts index ab2a55c0..a6e0e63f 100644 --- a/src/components/board/BoardManagement/actions.ts +++ b/src/components/board/BoardManagement/actions.ts @@ -13,7 +13,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { Board, BoardApiData, BoardFormData } from './types'; @@ -135,7 +135,7 @@ export async function getBoards(filters?: { const boards = result.data.map(transformApiToFrontend); return { success: true, data: boards }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] getBoards error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -184,7 +184,7 @@ export async function getTenantBoards(filters?: { const boards = result.data.map(transformApiToFrontend); return { success: true, data: boards }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] getTenantBoards error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -224,7 +224,7 @@ export async function getBoardByCode(code: string): Promise<{ success: boolean; return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] getBoardByCode error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -264,7 +264,7 @@ export async function getBoardById(id: string): Promise<{ success: boolean; data return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] getBoardById error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -313,7 +313,7 @@ export async function createBoard( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] createBoard error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -363,7 +363,7 @@ export async function updateBoard( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] updateBoard error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -405,7 +405,7 @@ export async function deleteBoard(id: string): Promise<{ success: boolean; error return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] deleteBoard error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -433,7 +433,7 @@ export async function deleteBoardsBulk(ids: string[]): Promise<{ success: boolea return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] deleteBoardsBulk error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/board/DynamicBoard/actions.ts b/src/components/board/DynamicBoard/actions.ts index 7aa905b2..c6999f48 100644 --- a/src/components/board/DynamicBoard/actions.ts +++ b/src/components/board/DynamicBoard/actions.ts @@ -6,7 +6,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { PostApiData, @@ -62,7 +62,7 @@ export async function getDynamicBoardPosts( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] getDynamicBoardPosts error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -99,7 +99,7 @@ export async function getDynamicBoardPost( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] getDynamicBoardPost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -142,7 +142,7 @@ export async function createDynamicBoardPost( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] createDynamicBoardPost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -186,7 +186,7 @@ export async function updateDynamicBoardPost( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] updateDynamicBoardPost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -222,7 +222,7 @@ export async function deleteDynamicBoardPost( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] deleteDynamicBoardPost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -261,7 +261,7 @@ export async function getDynamicBoardComments( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] getDynamicBoardComments error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -299,7 +299,7 @@ export async function createDynamicBoardComment( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] createDynamicBoardComment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -338,7 +338,7 @@ export async function updateDynamicBoardComment( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] updateDynamicBoardComment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -375,7 +375,7 @@ export async function deleteDynamicBoardComment( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[DynamicBoardActions] deleteDynamicBoardComment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/board/actions.ts b/src/components/board/actions.ts index 336122bd..d670c9a4 100644 --- a/src/components/board/actions.ts +++ b/src/components/board/actions.ts @@ -13,7 +13,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { PostApiData, @@ -103,7 +103,7 @@ export async function getPosts( return { success: true, data: result.data, posts }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] getPosts error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -156,7 +156,7 @@ export async function getMyPosts( return { success: true, data: result.data, posts }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] getMyPosts error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -193,7 +193,7 @@ export async function getPost( return { success: true, data: transformApiToPost(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] getPost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -236,7 +236,7 @@ export async function createPost( return { success: true, data: transformApiToPost(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] createPost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -280,7 +280,7 @@ export async function updatePost( return { success: true, data: transformApiToPost(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] updatePost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -316,7 +316,7 @@ export async function deletePost( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[BoardActions] deletePost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/business/CEODashboard/CEODashboard.tsx b/src/components/business/CEODashboard/CEODashboard.tsx index 0cd39c5e..ab4d871c 100644 --- a/src/components/business/CEODashboard/CEODashboard.tsx +++ b/src/components/business/CEODashboard/CEODashboard.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useCallback, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; import { Loader2, LayoutDashboard, Settings } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { PageLayout } from '@/components/organisms/PageLayout'; @@ -17,9 +18,9 @@ import { VatSection, CalendarSection, } from './sections'; -import type { CEODashboardData, CalendarScheduleItem, DashboardSettings } from './types'; +import type { CEODashboardData, CalendarScheduleItem, DashboardSettings, DetailModalConfig } from './types'; import { DEFAULT_DASHBOARD_SETTINGS } from './types'; -import { ScheduleDetailModal } from './modals'; +import { ScheduleDetailModal, DetailModal } from './modals'; import { DashboardSettingsDialog } from './dialogs/DashboardSettingsDialog'; // 목데이터 @@ -368,6 +369,7 @@ const mockData: CEODashboardData = { }; export function CEODashboard() { + const router = useRouter(); const [isLoading] = useState(false); const [data] = useState(mockData); @@ -379,6 +381,10 @@ export function CEODashboard() { const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [dashboardSettings, setDashboardSettings] = useState(DEFAULT_DASHBOARD_SETTINGS); + // 상세 모달 상태 + const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); + const [detailModalConfig, setDetailModalConfig] = useState(null); + // 클라이언트에서만 localStorage에서 설정 불러오기 (hydration 에러 방지) useEffect(() => { const saved = localStorage.getItem('ceo-dashboard-settings'); @@ -410,15 +416,273 @@ export function CEODashboard() { setIsSettingsModalOpen(false); }, []); - // 일일 일보 클릭 + // 일일 일보 클릭 → 일일 일보 페이지로 이동 const handleDailyReportClick = useCallback(() => { - // TODO: 일일 일보 상세 팝업 열기 - console.log('일일 일보 클릭'); + router.push('/ko/accounting/daily-report'); + }, [router]); + + // 상세 모달 닫기 + const handleDetailModalClose = useCallback(() => { + setIsDetailModalOpen(false); + setDetailModalConfig(null); }, []); - // 당월 예상 지출 클릭 + // 당월 예상 지출 카드 클릭 (개별 카드 클릭 시 상세 모달) + const handleMonthlyExpenseCardClick = useCallback((cardId: string) => { + // 카드 ID에 따라 다른 상세 데이터 설정 + const cardConfigs: Record = { + me1: { + title: '당월 매입 상세', + summaryCards: [ + { label: '당월 매입', value: 3123000, unit: '원' }, + { label: '전월 대비', value: '-12.5%', isComparison: true, isPositive: false }, + ], + barChart: { + title: '월별 매입 추이', + data: [ + { name: '1월', value: 45000000 }, + { name: '2월', value: 52000000 }, + { name: '3월', value: 48000000 }, + { name: '4월', value: 61000000 }, + { name: '5월', value: 55000000 }, + { name: '6월', value: 58000000 }, + { name: '7월', value: 50000000 }, + ], + dataKey: 'value', + xAxisKey: 'name', + color: '#60A5FA', + }, + pieChart: { + title: '자재 유형별 구매 비율', + data: [ + { name: '원자재', value: 55000000, percentage: 55, color: '#60A5FA' }, + { name: '부자재', value: 35000000, percentage: 35, color: '#34D399' }, + { name: '포장재', value: 10000000, percentage: 10, color: '#FBBF24' }, + ], + }, + table: { + title: '일별 매입 내역', + columns: [ + { key: 'no', label: 'No.', align: 'center' }, + { key: 'date', label: '매입일', align: 'center', format: 'date' }, + { key: 'vendor', label: '거래처', align: 'left' }, + { key: 'amount', label: '매입금액', align: 'right', format: 'currency' }, + { key: 'type', label: '매입유형', align: 'center' }, + ], + data: [ + { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '원재료매입' }, + { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '부재료매입' }, + { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '미설정' }, + { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '원재료매입' }, + { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '부재료매입' }, + { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '미설정' }, + { date: '2025-12-12', vendor: '회사명', amount: 11000000, type: '원재료매입' }, + ], + filters: [ + { + key: 'type', + options: [ + { value: 'all', label: '전체' }, + { value: '원재료매입', label: '원재료매입' }, + { value: '부재료매입', label: '부재료매입' }, + { value: '미설정', label: '미설정' }, + ], + defaultValue: 'all', + }, + { + key: 'sortOrder', + options: [ + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '오래된순' }, + ], + defaultValue: 'latest', + }, + ], + showTotal: true, + totalLabel: '합계', + totalValue: 111000000, + totalColumnKey: 'amount', + }, + }, + me2: { + title: '당월 카드 상세', + summaryCards: [ + { label: '당월 카드', value: 30123000, unit: '원' }, + { label: '전월 대비', value: '+10.5%', isComparison: true, isPositive: true }, + ], + barChart: { + title: '월별 카드 사용 추이', + data: [ + { name: '1월', value: 25000000 }, + { name: '2월', value: 28000000 }, + { name: '3월', value: 22000000 }, + { name: '4월', value: 30000000 }, + { name: '5월', value: 27000000 }, + { name: '6월', value: 29000000 }, + { name: '7월', value: 30000000 }, + ], + dataKey: 'value', + xAxisKey: 'name', + color: '#34D399', + }, + pieChart: { + title: '카드 사용 유형별 비율', + data: [ + { name: '접대비', value: 12000000, percentage: 40, color: '#60A5FA' }, + { name: '복리후생비', value: 9000000, percentage: 30, color: '#34D399' }, + { name: '소모품비', value: 9123000, percentage: 30, color: '#FBBF24' }, + ], + }, + table: { + title: '일별 카드 사용 내역', + columns: [ + { key: 'no', label: 'No.', align: 'center' }, + { key: 'date', label: '사용일', align: 'center', format: 'date' }, + { key: 'store', label: '가맹점', align: 'left' }, + { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, + { key: 'category', label: '분류', align: 'center' }, + ], + data: [ + { date: '2025-12-12', store: '가맹점명', amount: 5000000, category: '접대비' }, + { date: '2025-12-11', store: '가맹점명', amount: 3000000, category: '복리후생비' }, + { date: '2025-12-10', store: '가맹점명', amount: 4000000, category: '소모품비' }, + ], + filters: [ + { + key: 'category', + options: [ + { value: 'all', label: '전체' }, + { value: '접대비', label: '접대비' }, + { value: '복리후생비', label: '복리후생비' }, + { value: '소모품비', label: '소모품비' }, + ], + defaultValue: 'all', + }, + { + key: 'sortOrder', + options: [ + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '오래된순' }, + ], + defaultValue: 'latest', + }, + ], + showTotal: true, + totalLabel: '합계', + totalValue: 30123000, + totalColumnKey: 'amount', + }, + }, + me3: { + title: '당월 발행어음 상세', + summaryCards: [ + { label: '당월 발행어음', value: 30123000, unit: '원' }, + { label: '전월 대비', value: '+10.5%', isComparison: true, isPositive: true }, + ], + barChart: { + title: '월별 발행어음 추이', + data: [ + { name: '1월', value: 20000000 }, + { name: '2월', value: 25000000 }, + { name: '3월', value: 22000000 }, + { name: '4월', value: 28000000 }, + { name: '5월', value: 26000000 }, + { name: '6월', value: 30000000 }, + { name: '7월', value: 30000000 }, + ], + dataKey: 'value', + xAxisKey: 'name', + color: '#F472B6', + }, + table: { + title: '발행어음 내역', + columns: [ + { key: 'no', label: 'No.', align: 'center' }, + { key: 'date', label: '발행일', align: 'center', format: 'date' }, + { key: 'vendor', label: '수취인', align: 'left' }, + { key: 'amount', label: '금액', align: 'right', format: 'currency' }, + { key: 'dueDate', label: '만기일', align: 'center', format: 'date' }, + ], + data: [ + { date: '2025-12-12', vendor: '회사명', amount: 10000000, dueDate: '2026-03-12' }, + { date: '2025-12-10', vendor: '회사명', amount: 10123000, dueDate: '2026-03-10' }, + { date: '2025-12-08', vendor: '회사명', amount: 10000000, dueDate: '2026-03-08' }, + ], + filters: [ + { + key: 'sortOrder', + options: [ + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '오래된순' }, + ], + defaultValue: 'latest', + }, + ], + showTotal: true, + totalLabel: '합계', + totalValue: 30123000, + totalColumnKey: 'amount', + }, + }, + me4: { + title: '총 예상 지출 상세', + summaryCards: [ + { label: '총 예상 지출', value: 350000000, unit: '원' }, + { label: '전월 대비', value: '+10.5%', isComparison: true, isPositive: true }, + ], + barChart: { + title: '월별 총 지출 추이', + data: [ + { name: '1월', value: 280000000 }, + { name: '2월', value: 300000000 }, + { name: '3월', value: 290000000 }, + { name: '4월', value: 320000000 }, + { name: '5월', value: 310000000 }, + { name: '6월', value: 340000000 }, + { name: '7월', value: 350000000 }, + ], + dataKey: 'value', + xAxisKey: 'name', + color: '#A78BFA', + }, + pieChart: { + title: '지출 항목별 비율', + data: [ + { name: '매입', value: 305000000, percentage: 87, color: '#60A5FA' }, + { name: '카드', value: 30000000, percentage: 9, color: '#34D399' }, + { name: '발행어음', value: 15000000, percentage: 4, color: '#FBBF24' }, + ], + }, + table: { + title: '지출 항목별 내역', + columns: [ + { key: 'no', label: 'No.', align: 'center' }, + { key: 'category', label: '항목', align: 'left' }, + { key: 'amount', label: '금액', align: 'right', format: 'currency' }, + { key: 'ratio', label: '비율', align: 'center' }, + ], + data: [ + { category: '매입', amount: 305000000, ratio: '87%' }, + { category: '카드', amount: 30000000, ratio: '9%' }, + { category: '발행어음', amount: 15000000, ratio: '4%' }, + ], + showTotal: true, + totalLabel: '합계', + totalValue: 350000000, + totalColumnKey: 'amount', + }, + }, + }; + + const config = cardConfigs[cardId]; + if (config) { + setDetailModalConfig(config); + setIsDetailModalOpen(true); + } + }, []); + + // 당월 예상 지출 클릭 (deprecated - 개별 카드 클릭으로 대체) const handleMonthlyExpenseClick = useCallback(() => { - // TODO: 당월 예상 지출 상세 팝업 열기 console.log('당월 예상 지출 클릭'); }, []); @@ -536,7 +800,7 @@ export function CEODashboard() { {dashboardSettings.monthlyExpense && ( )} @@ -599,6 +863,15 @@ export function CEODashboard() { settings={dashboardSettings} onSave={handleSettingsSave} /> + + {/* 상세 모달 */} + {detailModalConfig && ( + + )} ); } \ No newline at end of file diff --git a/src/components/business/CEODashboard/modals/DetailModal.tsx b/src/components/business/CEODashboard/modals/DetailModal.tsx new file mode 100644 index 00000000..217e60f3 --- /dev/null +++ b/src/components/business/CEODashboard/modals/DetailModal.tsx @@ -0,0 +1,384 @@ +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { X } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + PieChart, + Pie, + Cell, +} from 'recharts'; +import { cn } from '@/lib/utils'; +import type { + DetailModalConfig, + SummaryCardData, + BarChartConfig, + PieChartConfig, + TableConfig, + TableFilterConfig, +} from '../types'; + +interface DetailModalProps { + isOpen: boolean; + onClose: () => void; + config: DetailModalConfig; +} + +/** + * 금액 포맷 함수 + */ +const formatCurrency = (value: number): string => { + return new Intl.NumberFormat('ko-KR').format(value); +}; + +/** + * 요약 카드 컴포넌트 + */ +const SummaryCard = ({ data }: { data: SummaryCardData }) => { + const displayValue = typeof data.value === 'number' + ? formatCurrency(data.value) + (data.unit || '원') + : data.value; + + return ( +
+

{data.label}

+

+ {data.isComparison && !data.isPositive && typeof data.value === 'string' && !data.value.startsWith('-') ? '-' : ''} + {displayValue} +

+
+ ); +}; + +/** + * 막대 차트 컴포넌트 + */ +const BarChartSection = ({ config }: { config: BarChartConfig }) => { + return ( +
+

{config.title}

+
+ + + + + value >= 10000 ? `${value / 10000}만` : value} + /> + [formatCurrency(value) + '원', '']} + contentStyle={{ fontSize: 12 }} + /> + + + +
+
+ ); +}; + +/** + * 도넛 차트 컴포넌트 + */ +const PieChartSection = ({ config }: { config: PieChartConfig }) => { + return ( +
+

{config.title}

+ {/* 도넛 차트 - 중앙 정렬 */} +
+ + + {config.data.map((entry, index) => ( + + ))} + + +
+ {/* 범례 - 차트 아래 배치 */} +
+ {config.data.map((item, index) => ( +
+
+
+ {item.name} + {item.percentage}% +
+ + {formatCurrency(item.value)}원 + +
+ ))} +
+
+ ); +}; + +/** + * 테이블 컴포넌트 + */ +const TableSection = ({ config }: { config: TableConfig }) => { + const [filters, setFilters] = useState>(() => { + const initial: Record = {}; + config.filters?.forEach((filter) => { + initial[filter.key] = filter.defaultValue; + }); + return initial; + }); + + const handleFilterChange = useCallback((key: string, value: string) => { + setFilters((prev) => ({ ...prev, [key]: value })); + }, []); + + // 필터링된 데이터 + const filteredData = useMemo(() => { + // 데이터가 없는 경우 빈 배열 반환 + if (!config.data || !Array.isArray(config.data)) { + return []; + } + let result = [...config.data]; + + // 각 필터 적용 (sortOrder는 정렬용이므로 제외) + config.filters?.forEach((filter) => { + if (filter.key === 'sortOrder') return; // 정렬 필터는 값 필터링에서 제외 + const filterValue = filters[filter.key]; + if (filterValue && filterValue !== 'all') { + result = result.filter((row) => row[filter.key] === filterValue); + } + }); + + // 정렬 필터 적용 (sortOrder가 있는 경우) + if (filters['sortOrder']) { + const sortOrder = filters['sortOrder']; + result.sort((a, b) => { + const dateA = new Date(a['date'] as string).getTime(); + const dateB = new Date(b['date'] as string).getTime(); + return sortOrder === 'latest' ? dateB - dateA : dateA - dateB; + }); + } + + return result; + }, [config.data, config.filters, filters]); + + // 셀 값 포맷팅 + const formatCellValue = (value: unknown, format?: string): string => { + if (value === null || value === undefined) return '-'; + + switch (format) { + case 'currency': + return typeof value === 'number' ? formatCurrency(value) : String(value); + case 'number': + return typeof value === 'number' ? formatCurrency(value) : String(value); + case 'date': + return String(value); + default: + return String(value); + } + }; + + // 셀 정렬 클래스 + const getAlignClass = (align?: string): string => { + switch (align) { + case 'center': + return 'text-center'; + case 'right': + return 'text-right'; + default: + return 'text-left'; + } + }; + + return ( +
+ {/* 테이블 헤더 */} +
+
+

{config.title}

+ 총 {filteredData.length}건 +
+ + {/* 필터 영역 */} + {config.filters && config.filters.length > 0 && ( +
+ {config.filters.map((filter) => ( + + ))} +
+ )} +
+ + {/* 테이블 */} +
+ + + + {config.columns.map((column) => ( + + ))} + + + + {filteredData.map((row, rowIndex) => ( + + {config.columns.map((column) => ( + + ))} + + ))} + + {/* 합계 행 */} + {config.showTotal && ( + + {config.columns.map((column, colIndex) => ( + + ))} + + )} + +
+ {column.label} +
+ {column.key === 'no' + ? rowIndex + 1 + : formatCellValue(row[column.key], column.format) + } +
+ {column.key === config.totalColumnKey + ? (typeof config.totalValue === 'number' + ? formatCurrency(config.totalValue) + : config.totalValue) + : (colIndex === 0 ? config.totalLabel || '합계' : '')} +
+
+
+ ); +}; + +/** + * 상세 모달 공통 컴포넌트 + */ +export function DetailModal({ isOpen, onClose, config }: DetailModalProps) { + return ( + !open && onClose()}> + + {/* 헤더 */} + +
+ {config.title} + +
+
+ +
+ {/* 요약 카드 영역 */} + {config.summaryCards.length > 0 && ( +
+ {config.summaryCards.map((card, index) => ( + + ))} +
+ )} + + {/* 차트 영역 */} + {(config.barChart || config.pieChart) && ( +
+ {config.barChart && } + {config.pieChart && } +
+ )} + + {/* 테이블 영역 */} + {config.table && } +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/business/CEODashboard/modals/index.ts b/src/components/business/CEODashboard/modals/index.ts index 466bb702..4427fc4d 100644 --- a/src/components/business/CEODashboard/modals/index.ts +++ b/src/components/business/CEODashboard/modals/index.ts @@ -1 +1,2 @@ export { ScheduleDetailModal } from './ScheduleDetailModal'; +export { DetailModal } from './DetailModal'; diff --git a/src/components/business/CEODashboard/sections/MonthlyExpenseSection.tsx b/src/components/business/CEODashboard/sections/MonthlyExpenseSection.tsx index ad89c81c..b0237209 100644 --- a/src/components/business/CEODashboard/sections/MonthlyExpenseSection.tsx +++ b/src/components/business/CEODashboard/sections/MonthlyExpenseSection.tsx @@ -6,10 +6,10 @@ import type { MonthlyExpenseData } from '../types'; interface MonthlyExpenseSectionProps { data: MonthlyExpenseData; - onClick?: () => void; + onCardClick?: (cardId: string) => void; } -export function MonthlyExpenseSection({ data, onClick }: MonthlyExpenseSectionProps) { +export function MonthlyExpenseSection({ data, onCardClick }: MonthlyExpenseSectionProps) { return ( @@ -20,7 +20,7 @@ export function MonthlyExpenseSection({ data, onClick }: MonthlyExpenseSectionPr onCardClick(card.id) : undefined} /> ))}
diff --git a/src/components/business/CEODashboard/types.ts b/src/components/business/CEODashboard/types.ts index 474d0591..085a8cfb 100644 --- a/src/components/business/CEODashboard/types.ts +++ b/src/components/business/CEODashboard/types.ts @@ -214,6 +214,90 @@ export interface DashboardSettings { calendar: boolean; } +// ===== 상세 모달 공통 타입 ===== + +// 요약 카드 타입 +export interface SummaryCardData { + label: string; + value: string | number; + isComparison?: boolean; // 전월 대비 등 비교 값인 경우 + isPositive?: boolean; // 비교 값일 때 증가/감소 여부 + unit?: string; +} + +// 막대 차트 데이터 타입 +export interface BarChartDataItem { + name: string; + value: number; + [key: string]: string | number; +} + +// 막대 차트 설정 타입 +export interface BarChartConfig { + title: string; + data: BarChartDataItem[]; + dataKey: string; + xAxisKey: string; + color?: string; +} + +// 도넛 차트 데이터 타입 +export interface PieChartDataItem { + name: string; + value: number; + percentage: number; + color: string; +} + +// 도넛 차트 설정 타입 +export interface PieChartConfig { + title: string; + data: PieChartDataItem[]; +} + +// 테이블 컬럼 타입 +export interface TableColumnConfig { + key: string; + label: string; + align?: 'left' | 'center' | 'right'; + format?: 'number' | 'currency' | 'date' | 'text'; + width?: string; +} + +// 테이블 필터 옵션 타입 +export interface TableFilterOption { + value: string; + label: string; +} + +// 테이블 필터 설정 타입 +export interface TableFilterConfig { + key: string; + options: TableFilterOption[]; + defaultValue: string; +} + +// 테이블 설정 타입 +export interface TableConfig { + title: string; + columns: TableColumnConfig[]; + data: Record[]; + filters?: TableFilterConfig[]; + showTotal?: boolean; + totalLabel?: string; + totalValue?: string | number; + totalColumnKey?: string; // 합계가 들어갈 컬럼 키 +} + +// 상세 모달 전체 설정 타입 +export interface DetailModalConfig { + title: string; + summaryCards: SummaryCardData[]; + barChart?: BarChartConfig; + pieChart?: PieChartConfig; + table?: TableConfig; +} + // 기본 설정값 export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = { todayIssue: { diff --git a/src/components/customer-center/shared/actions.ts b/src/components/customer-center/shared/actions.ts index b5f6864e..8bd95207 100644 --- a/src/components/customer-center/shared/actions.ts +++ b/src/components/customer-center/shared/actions.ts @@ -6,7 +6,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { PostApiData, @@ -63,7 +63,7 @@ export async function getPosts( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] getPosts error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -100,7 +100,7 @@ export async function getPost( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] getPost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -142,7 +142,7 @@ export async function createPost( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] createPost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -185,7 +185,7 @@ export async function updatePost( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] updatePost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -221,7 +221,7 @@ export async function deletePost( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] deletePost error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -260,7 +260,7 @@ export async function getComments( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] getComments error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -298,7 +298,7 @@ export async function createComment( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] createComment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -337,7 +337,7 @@ export async function updateComment( return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] updateComment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -374,7 +374,7 @@ export async function deleteComment( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[CustomerCenterActions] deleteComment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/hr/AttendanceManagement/actions.ts b/src/components/hr/AttendanceManagement/actions.ts index 57c942a1..6ccb9b4b 100644 --- a/src/components/hr/AttendanceManagement/actions.ts +++ b/src/components/hr/AttendanceManagement/actions.ts @@ -14,7 +14,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { AttendanceRecord, diff --git a/src/components/hr/CardManagement/actions.ts b/src/components/hr/CardManagement/actions.ts index 9ebbe3e2..e970ce6a 100644 --- a/src/components/hr/CardManagement/actions.ts +++ b/src/components/hr/CardManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { Card, CardFormData, CardStatus } from './types'; @@ -274,7 +274,7 @@ export async function deleteCards(ids: string[]): Promise<{ success: boolean; er return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteCards] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/hr/DepartmentManagement/actions.ts b/src/components/hr/DepartmentManagement/actions.ts index c4803bc6..5e48ec7c 100644 --- a/src/components/hr/DepartmentManagement/actions.ts +++ b/src/components/hr/DepartmentManagement/actions.ts @@ -13,7 +13,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; // ============================================ @@ -316,7 +316,7 @@ export async function deleteDepartmentsMany( results, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteDepartmentsMany] Error:', error); return { success: false, diff --git a/src/components/hr/EmployeeManagement/actions.ts b/src/components/hr/EmployeeManagement/actions.ts index 39f4d374..a1539849 100644 --- a/src/components/hr/EmployeeManagement/actions.ts +++ b/src/components/hr/EmployeeManagement/actions.ts @@ -16,7 +16,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { cookies } from 'next/headers'; import { serverFetch, getServerApiHeaders } from '@/lib/api/fetch-wrapper'; import type { Employee, EmployeeFormData, EmployeeStats } from './types'; @@ -100,7 +100,7 @@ export async function getEmployees(params?: { lastPage: result.data.last_page, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[EmployeeActions] getEmployees error:', error); return { data: [], total: 0, lastPage: 1 }; } @@ -132,7 +132,7 @@ export async function getEmployeeById(id: string): Promise { const departments = Array.isArray(result.data) ? result.data : result.data.data || []; return departments; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[EmployeeActions] getDepartments error:', error); return []; } @@ -513,7 +513,7 @@ export async function uploadProfileImage(inputFormData: FormData): Promise<{ }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[EmployeeActions] uploadProfileImage error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/hr/SalaryManagement/actions.ts b/src/components/hr/SalaryManagement/actions.ts index 5a787259..9c6cdab3 100644 --- a/src/components/hr/SalaryManagement/actions.ts +++ b/src/components/hr/SalaryManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { SalaryRecord, SalaryDetail, PaymentStatus } from './types'; diff --git a/src/components/hr/VacationManagement/actions.ts b/src/components/hr/VacationManagement/actions.ts index 60aedcac..00d9f45e 100644 --- a/src/components/hr/VacationManagement/actions.ts +++ b/src/components/hr/VacationManagement/actions.ts @@ -22,7 +22,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; // ============================================ @@ -293,7 +293,7 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{ error: result.message || '휴가 목록 조회에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getLeaves] Error:', error); return { success: false, @@ -332,7 +332,7 @@ export async function getLeaveById(id: number): Promise<{ error: result.message || '휴가 조회에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getLeaveById] Error:', error); return { success: false, @@ -379,7 +379,7 @@ export async function createLeave( error: result.message || '휴가 신청에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[createLeave] Error:', error); return { success: false, @@ -420,7 +420,7 @@ export async function approveLeave( error: result.message || '휴가 승인에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[approveLeave] Error:', error); return { success: false, @@ -461,7 +461,7 @@ export async function rejectLeave( error: result.message || '휴가 반려에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[rejectLeave] Error:', error); return { success: false, @@ -502,7 +502,7 @@ export async function cancelLeave( error: result.message || '휴가 취소에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[cancelLeave] Error:', error); return { success: false, @@ -545,7 +545,7 @@ export async function getMyLeaveBalance(year?: number): Promise<{ error: result.message || '잔여 휴가 조회에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getMyLeaveBalance] Error:', error); return { success: false, @@ -591,7 +591,7 @@ export async function getUserLeaveBalance( error: result.message || '잔여 휴가 조회에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getUserLeaveBalance] Error:', error); return { success: false, @@ -635,7 +635,7 @@ export async function setLeaveBalance( error: result.message || '잔여 휴가 설정에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[setLeaveBalance] Error:', error); return { success: false, @@ -669,7 +669,7 @@ export async function deleteLeave(id: number): Promise<{ success: boolean; error error: result.message || '휴가 삭제에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteLeave] Error:', error); return { success: false, @@ -701,7 +701,7 @@ export async function approveLeavesMany(ids: number[]): Promise<{ error: allSuccess ? undefined : '일부 휴가 승인에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[approveLeavesMany] Error:', error); return { success: false, @@ -736,7 +736,7 @@ export async function rejectLeavesMany( error: allSuccess ? undefined : '일부 휴가 반려에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[rejectLeavesMany] Error:', error); return { success: false, @@ -795,7 +795,7 @@ export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise error: result.message || '휴가 사용현황 조회에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getLeaveBalances] Error:', error); return { success: false, @@ -942,7 +942,7 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{ error: result.message || '휴가 부여 이력 조회에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getLeaveGrants] Error:', error); return { success: false, @@ -988,7 +988,7 @@ export async function createLeaveGrant( error: result.message || '휴가 부여에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[createLeaveGrant] Error:', error); return { success: false, @@ -1022,7 +1022,7 @@ export async function deleteLeaveGrant(id: number): Promise<{ success: boolean; error: result.message || '휴가 부여 삭제에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteLeaveGrant] Error:', error); return { success: false, @@ -1116,7 +1116,7 @@ export async function getActiveEmployees(): Promise<{ error: result.message || '직원 목록 조회에 실패했습니다.', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getActiveEmployees] Error:', error); return { success: false, diff --git a/src/components/material/ReceivingManagement/actions.ts b/src/components/material/ReceivingManagement/actions.ts index f23062f2..4ec2e097 100644 --- a/src/components/material/ReceivingManagement/actions.ts +++ b/src/components/material/ReceivingManagement/actions.ts @@ -14,7 +14,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { ReceivingItem, @@ -242,7 +242,7 @@ export async function getReceivings(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivingActions] getReceivings error:', error); return { success: false, @@ -282,7 +282,7 @@ export async function getReceivingStats(): Promise<{ return { success: true, data: transformApiToStats(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivingActions] getReceivingStats error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -317,7 +317,7 @@ export async function getReceivingById(id: string): Promise<{ return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivingActions] getReceivingById error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -351,7 +351,7 @@ export async function createReceiving( return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivingActions] createReceiving error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -386,7 +386,7 @@ export async function updateReceiving( return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivingActions] updateReceiving error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -418,7 +418,7 @@ export async function deleteReceiving( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivingActions] deleteReceiving error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -453,7 +453,7 @@ export async function processReceiving( return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ReceivingActions] processReceiving error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/material/StockStatus/actions.ts b/src/components/material/StockStatus/actions.ts index 038f8039..3beae9c8 100644 --- a/src/components/material/StockStatus/actions.ts +++ b/src/components/material/StockStatus/actions.ts @@ -11,7 +11,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { StockItem, @@ -259,7 +259,7 @@ export async function getStocks(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[StockActions] getStocks error:', error); return { success: false, @@ -299,7 +299,7 @@ export async function getStockStats(): Promise<{ return { success: true, data: transformApiToStats(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[StockActions] getStockStats error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -334,7 +334,7 @@ export async function getStockStatsByType(): Promise<{ return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[StockActions] getStockStatsByType error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -369,7 +369,7 @@ export async function getStockById(id: string): Promise<{ return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[StockActions] getStockById error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/outbound/ShipmentManagement/actions.ts b/src/components/outbound/ShipmentManagement/actions.ts index e6dc2b45..c5d813ee 100644 --- a/src/components/outbound/ShipmentManagement/actions.ts +++ b/src/components/outbound/ShipmentManagement/actions.ts @@ -18,7 +18,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { ShipmentItem, @@ -375,7 +375,7 @@ export async function getShipments(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] getShipments error:', error); return { success: false, @@ -419,7 +419,7 @@ export async function getShipmentStats(): Promise<{ return { success: true, data: transformApiToStats(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] getShipmentStats error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -458,7 +458,7 @@ export async function getShipmentStatsByStatus(): Promise<{ return { success: true, data: transformApiToStatsByStatus(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] getShipmentStatsByStatus error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -497,7 +497,7 @@ export async function getShipmentById(id: string): Promise<{ return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] getShipmentById error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -536,7 +536,7 @@ export async function createShipment( return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] createShipment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -576,7 +576,7 @@ export async function updateShipment( return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] updateShipment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -631,7 +631,7 @@ export async function updateShipmentStatus( return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] updateShipmentStatus error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -666,7 +666,7 @@ export async function deleteShipment( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] deleteShipment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -705,7 +705,7 @@ export async function getLotOptions(): Promise<{ return { success: true, data: result.data || [] }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] getLotOptions error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } @@ -744,7 +744,7 @@ export async function getLogisticsOptions(): Promise<{ return { success: true, data: result.data || [] }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] getLogisticsOptions error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } @@ -783,7 +783,7 @@ export async function getVehicleTonnageOptions(): Promise<{ return { success: true, data: result.data || [] }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[ShipmentActions] getVehicleTonnageOptions error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/pricing/actions.ts b/src/components/pricing/actions.ts index 91edf697..ce120ca4 100644 --- a/src/components/pricing/actions.ts +++ b/src/components/pricing/actions.ts @@ -14,7 +14,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { PricingData, ItemInfo } from './types'; @@ -197,7 +197,7 @@ export async function getPricingById(id: string): Promise { return transformApiToFrontend(result.data); } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PricingActions] getPricingById error:', error); return null; } @@ -248,7 +248,7 @@ export async function getItemInfo(itemId: string): Promise { unit: item.unit || 'EA', }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PricingActions] getItemInfo error:', error); return null; } @@ -304,7 +304,7 @@ export async function createPricing( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PricingActions] createPricing error:', error); return { success: false, @@ -367,7 +367,7 @@ export async function updatePricing( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PricingActions] updatePricing error:', error); return { success: false, @@ -415,7 +415,7 @@ export async function deletePricing(id: string): Promise<{ success: boolean; err return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PricingActions] deletePricing error:', error); return { success: false, @@ -466,7 +466,7 @@ export async function finalizePricing(id: string): Promise<{ success: boolean; d data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PricingActions] finalizePricing error:', error); return { success: false, @@ -547,7 +547,7 @@ export async function getPricingRevisions(priceId: string): Promise<{ data: revisions, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PricingActions] getPricingRevisions error:', error); return { success: false, diff --git a/src/components/process-management/actions.ts b/src/components/process-management/actions.ts index 6e463c2b..c3698c27 100644 --- a/src/components/process-management/actions.ts +++ b/src/components/process-management/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { Process, ProcessFormData, ClassificationRule } from '@/types/process'; @@ -169,7 +169,7 @@ export async function getProcessList(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getProcessList] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -201,7 +201,7 @@ export async function getProcessById(id: string): Promise<{ success: boolean; da return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getProcessById] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -235,7 +235,7 @@ export async function createProcess(data: ProcessFormData): Promise<{ success: b return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[createProcess] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -269,7 +269,7 @@ export async function updateProcess(id: string, data: ProcessFormData): Promise< return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[updateProcess] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -300,7 +300,7 @@ export async function deleteProcess(id: string): Promise<{ success: boolean; err return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteProcess] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -332,7 +332,7 @@ export async function deleteProcesses(ids: string[]): Promise<{ success: boolean return { success: true, deletedCount: result.data.deleted_count }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteProcesses] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -363,7 +363,7 @@ export async function toggleProcessActive(id: string): Promise<{ success: boolea return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[toggleProcessActive] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -410,7 +410,7 @@ export async function getProcessOptions(): Promise<{ })), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getProcessOptions] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -455,7 +455,7 @@ export async function getProcessStats(): Promise<{ }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getProcessStats] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -503,7 +503,7 @@ export async function getDepartmentOptions(): Promise { return []; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getDepartmentOptions] Error:', error); return []; } @@ -564,7 +564,7 @@ export async function getItemList(params?: GetItemListParams): Promise> { return { success: true, data: departments }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('getDepartments error:', error); return { success: false, diff --git a/src/components/settings/CompanyInfoManagement/actions.ts b/src/components/settings/CompanyInfoManagement/actions.ts index 3e9eaf2d..1ff4d0e5 100644 --- a/src/components/settings/CompanyInfoManagement/actions.ts +++ b/src/components/settings/CompanyInfoManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { CompanyFormData } from './types'; @@ -76,7 +76,7 @@ export async function getCompanyInfo(): Promise<{ return { success: true, data: formData }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getCompanyInfo] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -126,7 +126,7 @@ export async function updateCompanyInfo( const updatedData = transformApiToFrontend(result.data); return { success: true, data: updatedData }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[updateCompanyInfo] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -275,7 +275,7 @@ export async function uploadCompanyLogo(formData: FormData): Promise<{ }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[uploadCompanyLogo] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/settings/LeavePolicyManagement/actions.ts b/src/components/settings/LeavePolicyManagement/actions.ts index 070b4be8..0a13760e 100644 --- a/src/components/settings/LeavePolicyManagement/actions.ts +++ b/src/components/settings/LeavePolicyManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { LeavePolicySettings } from './types'; @@ -101,7 +101,7 @@ export async function getLeavePolicy(): Promise<{ data: transformedData, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[LeavePolicyActions] getLeavePolicy error:', error); return { success: false, @@ -158,7 +158,7 @@ export async function updateLeavePolicy(data: Partial): Pro data: transformedData, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[LeavePolicyActions] updateLeavePolicy error:', error); return { success: false, diff --git a/src/components/settings/NotificationSettings/actions.ts b/src/components/settings/NotificationSettings/actions.ts index e65e0624..33d4ba79 100644 --- a/src/components/settings/NotificationSettings/actions.ts +++ b/src/components/settings/NotificationSettings/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { NotificationSettings } from './types'; import { DEFAULT_NOTIFICATION_SETTINGS } from './types'; @@ -54,7 +54,7 @@ export async function getNotificationSettings(): Promise<{ data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[NotificationActions] getNotificationSettings error:', error); return { success: true, @@ -104,7 +104,7 @@ export async function saveNotificationSettings( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[NotificationActions] saveNotificationSettings error:', error); return { success: false, diff --git a/src/components/settings/PaymentHistoryManagement/actions.ts b/src/components/settings/PaymentHistoryManagement/actions.ts index 3cd5ebdb..1a0d6ead 100644 --- a/src/components/settings/PaymentHistoryManagement/actions.ts +++ b/src/components/settings/PaymentHistoryManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { PaymentApiData, PaymentHistory } from './types'; import { transformApiToFrontend } from './utils'; @@ -87,7 +87,7 @@ export async function getPayments(params?: { }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PaymentActions] getPayments error:', error); return { success: false, @@ -236,7 +236,7 @@ export async function getPaymentStatement(id: string): Promise<{ }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PaymentActions] getPaymentStatement error:', error); return { success: false, diff --git a/src/components/settings/PermissionManagement/actions.ts b/src/components/settings/PermissionManagement/actions.ts index 6d39231d..8bbe9a7f 100644 --- a/src/components/settings/PermissionManagement/actions.ts +++ b/src/components/settings/PermissionManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { revalidatePath } from 'next/cache'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { Role, RoleStats, PermissionMatrix, MenuTreeItem, ApiResponse, PaginatedResponse } from './types'; @@ -45,7 +45,7 @@ export async function fetchRoles(params?: { return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('Failed to fetch roles:', error); return { success: false, error: error instanceof Error ? error.message : '역할 목록 조회 실패' }; } @@ -74,7 +74,7 @@ export async function fetchRole(id: number): Promise> { return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('Failed to fetch role:', error); return { success: false, error: error instanceof Error ? error.message : '역할 조회 실패' }; } @@ -111,7 +111,7 @@ export async function createRole(data: { revalidatePath('/settings/permissions'); return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('Failed to create role:', error); return { success: false, error: error instanceof Error ? error.message : '역할 생성 실패' }; } @@ -152,7 +152,7 @@ export async function updateRole( revalidatePath(`/settings/permissions/${id}`); return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('Failed to update role:', error); return { success: false, error: error instanceof Error ? error.message : '역할 수정 실패' }; } @@ -184,7 +184,7 @@ export async function deleteRole(id: number): Promise> { revalidatePath('/settings/permissions'); return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('Failed to delete role:', error); return { success: false, error: error instanceof Error ? error.message : '역할 삭제 실패' }; } @@ -213,7 +213,7 @@ export async function fetchRoleStats(): Promise> { return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('Failed to fetch role stats:', error); return { success: false, error: error instanceof Error ? error.message : '역할 통계 조회 실패' }; } @@ -242,7 +242,7 @@ export async function fetchActiveRoles(): Promise> { return { success: true, data: result.data }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('Failed to fetch active roles:', error); return { success: false, error: error instanceof Error ? error.message : '활성 역할 목록 조회 실패' }; } @@ -276,7 +276,7 @@ export async function fetchPermissionMenus(): Promise { return transformApiToFrontend(result.data); } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PopupActions] getPopupById error:', error); return null; } @@ -174,7 +174,7 @@ export async function createPopup( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PopupActions] createPopup error:', error); return { success: false, @@ -233,7 +233,7 @@ export async function updatePopup( data: transformApiToFrontend(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PopupActions] updatePopup error:', error); return { success: false, @@ -281,7 +281,7 @@ export async function deletePopup(id: string): Promise<{ success: boolean; error return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PopupActions] deletePopup error:', error); return { success: false, @@ -307,7 +307,7 @@ export async function deletePopups(ids: string[]): Promise<{ success: boolean; e return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[PopupActions] deletePopups error:', error); return { success: false, diff --git a/src/components/settings/RankManagement/actions.ts b/src/components/settings/RankManagement/actions.ts index c4f70276..088febc1 100644 --- a/src/components/settings/RankManagement/actions.ts +++ b/src/components/settings/RankManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { Rank } from './types'; @@ -84,7 +84,7 @@ export async function getRanks(params?: { const ranks = result.data.map(transformApiToFrontend); return { success: true, data: ranks }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getRanks] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -136,7 +136,7 @@ export async function createRank(data: { const rank = transformApiToFrontend(result.data); return { success: true, data: rank }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[createRank] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -186,7 +186,7 @@ export async function updateRank( const rank = transformApiToFrontend(result.data); return { success: true, data: rank }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[updateRank] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -226,7 +226,7 @@ export async function deleteRank(id: number): Promise<{ return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteRank] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -269,7 +269,7 @@ export async function reorderRanks( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[reorderRanks] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/settings/SubscriptionManagement/actions.ts b/src/components/settings/SubscriptionManagement/actions.ts index 3beb98dc..f5067430 100644 --- a/src/components/settings/SubscriptionManagement/actions.ts +++ b/src/components/settings/SubscriptionManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { SubscriptionApiData, UsageApiData, SubscriptionInfo } from './types'; import { transformApiToFrontend } from './utils'; @@ -54,7 +54,7 @@ export async function getCurrentSubscription(): Promise<{ data: result.data, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[SubscriptionActions] getCurrentSubscription error:', error); return { success: false, @@ -112,7 +112,7 @@ export async function getUsage(): Promise<{ data: result.data, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[SubscriptionActions] getUsage error:', error); return { success: false, @@ -166,7 +166,7 @@ export async function cancelSubscription( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[SubscriptionActions] cancelSubscription error:', error); return { success: false, @@ -225,7 +225,7 @@ export async function requestDataExport( }, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[SubscriptionActions] requestDataExport error:', error); return { success: false, @@ -264,7 +264,7 @@ export async function getSubscriptionData(): Promise<{ data, }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[SubscriptionActions] getSubscriptionData error:', error); return { success: false, diff --git a/src/components/settings/TitleManagement/actions.ts b/src/components/settings/TitleManagement/actions.ts index 4f2e3bda..3e11a28c 100644 --- a/src/components/settings/TitleManagement/actions.ts +++ b/src/components/settings/TitleManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { Title } from './types'; @@ -84,7 +84,7 @@ export async function getTitles(params?: { const titles = result.data.map(transformApiToFrontend); return { success: true, data: titles }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[getTitles] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -136,7 +136,7 @@ export async function createTitle(data: { const title = transformApiToFrontend(result.data); return { success: true, data: title }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[createTitle] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -186,7 +186,7 @@ export async function updateTitle( const title = transformApiToFrontend(result.data); return { success: true, data: title }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[updateTitle] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -226,7 +226,7 @@ export async function deleteTitle(id: number): Promise<{ return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[deleteTitle] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } @@ -269,7 +269,7 @@ export async function reorderTitles( return { success: true }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('[reorderTitles] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/settings/WorkScheduleManagement/actions.ts b/src/components/settings/WorkScheduleManagement/actions.ts index 8d74731b..34d9a145 100644 --- a/src/components/settings/WorkScheduleManagement/actions.ts +++ b/src/components/settings/WorkScheduleManagement/actions.ts @@ -1,7 +1,7 @@ 'use server'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://sam.kr:8080'; @@ -120,7 +120,7 @@ export async function getWorkSetting(): Promise<{ data: transformFromApi(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('getWorkSetting error:', error); return { success: false, @@ -169,7 +169,7 @@ export async function updateWorkSetting( data: transformFromApi(result.data), }; } catch (error) { - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error('updateWorkSetting error:', error); return { success: false, diff --git a/src/lib/api/fetch-wrapper.ts b/src/lib/api/fetch-wrapper.ts index 32b7c97e..34f1170b 100644 --- a/src/lib/api/fetch-wrapper.ts +++ b/src/lib/api/fetch-wrapper.ts @@ -9,7 +9,7 @@ import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; -import { isRedirectError } from 'next/dist/client/components/redirect'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { createErrorResponse, type ApiErrorResponse } from './errors'; import { refreshAccessToken } from './refresh-token'; @@ -177,7 +177,7 @@ export async function serverFetch( return { response, error: null }; } catch (error) { // Next.js 15: redirect()는 특수한 에러를 throw하므로 다시 throw해서 Next.js가 처리하도록 함 - if (isRedirectError(error)) throw error; + if (isNextRedirectError(error)) throw error; console.error(`[serverFetch] Network error: ${url}`, error); return { response: null, diff --git a/src/lib/utils/redirect-error.ts b/src/lib/utils/redirect-error.ts new file mode 100644 index 00000000..e13ce732 --- /dev/null +++ b/src/lib/utils/redirect-error.ts @@ -0,0 +1,38 @@ +/** + * Next.js Redirect Error 유틸리티 + * + * Next.js의 redirect() 함수가 throw하는 NEXT_REDIRECT 에러를 감지합니다. + * + * 왜 자체 구현인가? + * - Next.js 내부 경로(next/dist/...)는 버전 업데이트 시 변경될 수 있음 + * - 공개 API로 제공되지 않는 내부 함수에 의존하지 않기 위함 + * - 한 곳에서 관리하여 유지보수 용이 + * + * @see https://nextjs.org/docs/app/api-reference/functions/redirect + */ + +/** + * Next.js redirect() 에러인지 확인 + * + * redirect() 호출 시 Next.js는 특수한 에러를 throw합니다. + * 이 에러는 catch 블록에서 잡히면 안 되고, 다시 throw해야 합니다. + * + * @example + * ```typescript + * try { + * // ... some code that might call redirect() + * } catch (error) { + * if (isNextRedirectError(error)) throw error; + * // handle other errors + * } + * ``` + */ +export function isNextRedirectError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'digest' in error && + typeof (error as { digest: string }).digest === 'string' && + (error as { digest: string }).digest.startsWith('NEXT_REDIRECT') + ); +} \ No newline at end of file