chore(WEB): actions.ts 에러 핸들링 및 CEO 대시보드 개선

- 전체 모듈 actions.ts redirect 에러 핸들링 추가
- CEODashboard DetailModal 추가
- MonthlyExpenseSection 개선
- fetch-wrapper redirect 에러 처리
- redirect-error 유틸 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-08 18:41:15 +09:00
parent 9885085259
commit 0d539628f3
61 changed files with 1226 additions and 359 deletions

View File

@@ -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;
}
```
```
### 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<SomeResult> {
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 함수에 동일 패턴 적용

View File

@@ -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<BadDebtRecord | null>
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<BadDebtSummaryApiData | null>
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,

View File

@@ -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,

View File

@@ -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' };
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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';

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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';

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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';

View File

@@ -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<InboxSummary | null> {
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,

View File

@@ -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<ApprovalPerson[]> {
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,

View File

@@ -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<DraftsSummary | null> {
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<DraftRecord | null> {
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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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<CEODashboardData>(mockData);
@@ -379,6 +381,10 @@ export function CEODashboard() {
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const [dashboardSettings, setDashboardSettings] = useState<DashboardSettings>(DEFAULT_DASHBOARD_SETTINGS);
// 상세 모달 상태
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [detailModalConfig, setDetailModalConfig] = useState<DetailModalConfig | null>(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<string, DetailModalConfig> = {
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 && (
<MonthlyExpenseSection
data={data.monthlyExpense}
onClick={handleMonthlyExpenseClick}
onCardClick={handleMonthlyExpenseCardClick}
/>
)}
@@ -599,6 +863,15 @@ export function CEODashboard() {
settings={dashboardSettings}
onSave={handleSettingsSave}
/>
{/* 상세 모달 */}
{detailModalConfig && (
<DetailModal
isOpen={isDetailModalOpen}
onClose={handleDetailModalClose}
config={detailModalConfig}
/>
)}
</PageLayout>
);
}

View File

@@ -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 (
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-500 mb-1">{data.label}</p>
<p className={cn(
"text-2xl font-bold",
data.isComparison && (data.isPositive ? "text-blue-600" : "text-red-600")
)}>
{data.isComparison && !data.isPositive && typeof data.value === 'string' && !data.value.startsWith('-') ? '-' : ''}
{displayValue}
</p>
</div>
);
};
/**
* 막대 차트 컴포넌트
*/
const BarChartSection = ({ config }: { config: BarChartConfig }) => {
return (
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm font-medium text-gray-700 mb-4">{config.title}</p>
<div className="h-[150px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={config.data} margin={{ top: 5, right: 5, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E5E7EB" />
<XAxis
dataKey={config.xAxisKey}
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#6B7280' }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 10, fill: '#6B7280' }}
tickFormatter={(value) => value >= 10000 ? `${value / 10000}` : value}
/>
<Tooltip
formatter={(value: number) => [formatCurrency(value) + '원', '']}
contentStyle={{ fontSize: 12 }}
/>
<Bar
dataKey={config.dataKey}
fill={config.color || '#60A5FA'}
radius={[4, 4, 0, 0]}
maxBarSize={40}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};
/**
* 도넛 차트 컴포넌트
*/
const PieChartSection = ({ config }: { config: PieChartConfig }) => {
return (
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm font-medium text-gray-700 mb-4">{config.title}</p>
{/* 도넛 차트 - 중앙 정렬 */}
<div className="flex justify-center mb-4">
<PieChart width={120} height={120}>
<Pie
data={config.data}
cx={60}
cy={60}
innerRadius={35}
outerRadius={55}
paddingAngle={2}
dataKey="value"
>
{config.data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</div>
{/* 범례 - 차트 아래 배치 */}
<div className="space-y-2">
{config.data.map((item, index) => (
<div key={index} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: item.color }}
/>
<span className="text-gray-600">{item.name}</span>
<span className="text-gray-400">{item.percentage}%</span>
</div>
<span className="font-medium text-gray-900">
{formatCurrency(item.value)}
</span>
</div>
))}
</div>
</div>
);
};
/**
* 테이블 컴포넌트
*/
const TableSection = ({ config }: { config: TableConfig }) => {
const [filters, setFilters] = useState<Record<string, string>>(() => {
const initial: Record<string, string> = {};
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 (
<div className="mt-6">
{/* 테이블 헤더 */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<h4 className="font-medium text-gray-800">{config.title}</h4>
<span className="text-sm text-gray-500"> {filteredData.length}</span>
</div>
{/* 필터 영역 */}
{config.filters && config.filters.length > 0 && (
<div className="flex items-center gap-2">
{config.filters.map((filter) => (
<Select
key={filter.key}
value={filters[filter.key]}
onValueChange={(value) => handleFilterChange(filter.key, value)}
>
<SelectTrigger className="h-8 w-auto min-w-[80px] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{filter.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
))}
</div>
)}
</div>
{/* 테이블 */}
<div className="border rounded-lg overflow-hidden">
<table className="w-full">
<thead>
<tr className="bg-gray-100">
{config.columns.map((column) => (
<th
key={column.key}
className={cn(
"px-4 py-3 text-xs font-medium text-gray-600",
getAlignClass(column.align),
column.width && `w-[${column.width}]`
)}
>
{column.label}
</th>
))}
</tr>
</thead>
<tbody>
{filteredData.map((row, rowIndex) => (
<tr
key={rowIndex}
className="border-t border-gray-100 hover:bg-gray-50"
>
{config.columns.map((column) => (
<td
key={column.key}
className={cn(
"px-4 py-3 text-sm",
getAlignClass(column.align)
)}
>
{column.key === 'no'
? rowIndex + 1
: formatCellValue(row[column.key], column.format)
}
</td>
))}
</tr>
))}
{/* 합계 행 */}
{config.showTotal && (
<tr className="border-t-2 border-gray-200 bg-gray-50 font-medium">
{config.columns.map((column, colIndex) => (
<td
key={column.key}
className={cn(
"px-4 py-3 text-sm",
getAlignClass(column.align)
)}
>
{column.key === config.totalColumnKey
? (typeof config.totalValue === 'number'
? formatCurrency(config.totalValue)
: config.totalValue)
: (colIndex === 0 ? config.totalLabel || '합계' : '')}
</td>
))}
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
/**
* 상세 모달 공통 컴포넌트
*/
export function DetailModal({ isOpen, onClose, config }: DetailModalProps) {
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="!w-[85vw] !max-w-[1600px] max-h-[90vh] overflow-y-auto p-0">
{/* 헤더 */}
<DialogHeader className="sticky top-0 bg-white z-10 px-6 py-4 border-b">
<div className="flex items-center justify-between">
<DialogTitle className="text-lg font-bold">{config.title}</DialogTitle>
<button
type="button"
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded-full transition-colors"
>
<X className="h-5 w-5 text-gray-500" />
</button>
</div>
</DialogHeader>
<div className="p-6 space-y-6">
{/* 요약 카드 영역 */}
{config.summaryCards.length > 0 && (
<div className="grid grid-cols-2 gap-4">
{config.summaryCards.map((card, index) => (
<SummaryCard key={index} data={card} />
))}
</div>
)}
{/* 차트 영역 */}
{(config.barChart || config.pieChart) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{config.barChart && <BarChartSection config={config.barChart} />}
{config.pieChart && <PieChartSection config={config.pieChart} />}
</div>
)}
{/* 테이블 영역 */}
{config.table && <TableSection key={config.title} config={config.table} />}
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -1 +1,2 @@
export { ScheduleDetailModal } from './ScheduleDetailModal';
export { DetailModal } from './DetailModal';

View File

@@ -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 (
<Card>
<CardContent className="p-6">
@@ -20,7 +20,7 @@ export function MonthlyExpenseSection({ data, onClick }: MonthlyExpenseSectionPr
<AmountCardItem
key={card.id}
card={card}
onClick={onClick}
onClick={onCardClick ? () => onCardClick(card.id) : undefined}
/>
))}
</div>

View File

@@ -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<string, unknown>[];
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: {

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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,

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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,

View File

@@ -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<Employee | null | { _
return transformApiToFrontend(result.data);
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[EmployeeActions] getEmployeeById error:', error);
return null;
}
@@ -179,7 +179,7 @@ export async function createEmployee(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[EmployeeActions] createEmployee error:', error);
return {
success: false,
@@ -230,7 +230,7 @@ export async function updateEmployee(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[EmployeeActions] updateEmployee error:', error);
return {
success: false,
@@ -268,7 +268,7 @@ export async function deleteEmployee(id: string): Promise<{ success: boolean; er
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[EmployeeActions] deleteEmployee error:', error);
return {
success: false,
@@ -309,7 +309,7 @@ export async function deleteEmployees(ids: string[]): Promise<{ success: boolean
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[EmployeeActions] deleteEmployees error:', error);
return {
success: false,
@@ -349,7 +349,7 @@ export async function getEmployeeStats(): Promise<EmployeeStats | null | { __aut
averageTenure: result.data.average_tenure ?? 0,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[EmployeeActions] getEmployeeStats error:', error);
return null;
}
@@ -401,7 +401,7 @@ export async function getPositions(type?: 'rank' | 'title'): Promise<PositionIte
return result.data;
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[EmployeeActions] getPositions error:', error);
return [];
}
@@ -450,7 +450,7 @@ export async function getDepartments(): Promise<DepartmentItem[]> {
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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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';

View File

@@ -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,

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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<PricingData | null> {
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<ItemInfo | null> {
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,

View File

@@ -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<DepartmentOption[]> {
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<ItemOptio
return [];
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[getItemList] Error:', error);
return [];
}

View File

@@ -7,7 +7,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 { WorkOrder, WorkerStatus, ProcessType, DashboardStats } from './types';
@@ -184,7 +184,7 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
stats,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[ProductionDashboardActions] getDashboardData error:', error);
return {
...emptyResult,

View File

@@ -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 {
WorkOrder,
@@ -124,7 +124,7 @@ export async function getWorkOrders(params?: {
},
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] getWorkOrders error:', error);
return { ...emptyResponse, error: '서버 오류가 발생했습니다.' };
}
@@ -168,7 +168,7 @@ export async function getWorkOrderStats(): Promise<{
data: transformStatsApiToFrontend(statsApi),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] getWorkOrderStats error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -210,7 +210,7 @@ export async function getWorkOrderById(id: string): Promise<{
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] getWorkOrderById error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -261,7 +261,7 @@ export async function createWorkOrder(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] createWorkOrder error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -304,7 +304,7 @@ export async function updateWorkOrder(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] updateWorkOrder error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -334,7 +334,7 @@ export async function deleteWorkOrder(id: string): Promise<{ success: boolean; e
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] deleteWorkOrder error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -375,7 +375,7 @@ export async function updateWorkOrderStatus(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] updateWorkOrderStatus error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -420,7 +420,7 @@ export async function assignWorkOrder(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] assignWorkOrder error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -461,7 +461,7 @@ export async function toggleBendingField(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] toggleBendingField error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -506,7 +506,7 @@ export async function addWorkOrderIssue(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] addWorkOrderIssue error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -544,7 +544,7 @@ export async function resolveWorkOrderIssue(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] resolveWorkOrderIssue error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -632,7 +632,7 @@ export async function getSalesOrdersForWorkOrder(params?: {
data: salesOrders,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] getSalesOrdersForWorkOrder error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
@@ -712,7 +712,7 @@ export async function getDepartmentsWithUsers(): Promise<{
data: departments,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] getDepartmentsWithUsers error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}

View File

@@ -1,7 +1,7 @@
'use server';
import { isRedirectError } from 'next/dist/client/components/redirect';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
/**
* 작업실적 관리 Server Actions
*
@@ -135,7 +135,7 @@ export async function getWorkResults(params?: {
},
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkResultActions] getWorkResults error:', error);
return {
success: false,
@@ -206,7 +206,7 @@ export async function getWorkResultStats(params?: {
data: transformStatsApiToFrontend(statsApi),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkResultActions] getWorkResultStats error:', error);
return {
success: false,
@@ -260,7 +260,7 @@ export async function getWorkResultById(id: string): Promise<{
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkResultActions] getWorkResultById error:', error);
return {
success: false,
@@ -340,7 +340,7 @@ export async function createWorkResult(data: {
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkResultActions] createWorkResult error:', error);
return {
success: false,
@@ -394,7 +394,7 @@ export async function updateWorkResult(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkResultActions] updateWorkResult error:', error);
return {
success: false,
@@ -436,7 +436,7 @@ export async function deleteWorkResult(id: string): Promise<{
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkResultActions] deleteWorkResult error:', error);
return {
success: false,
@@ -484,7 +484,7 @@ export async function toggleInspection(id: string): Promise<{
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkResultActions] toggleInspection error:', error);
return {
success: false,
@@ -532,7 +532,7 @@ export async function togglePackaging(id: string): Promise<{
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkResultActions] togglePackaging error:', error);
return {
success: false,

View File

@@ -8,7 +8,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 { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types';
@@ -133,7 +133,7 @@ export async function getMyWorkOrders(): Promise<{
data: workOrders,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getMyWorkOrders error:', error);
return {
success: false,
@@ -186,7 +186,7 @@ export async function completeWorkOrder(
lotNo,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] completeWorkOrder error:', error);
return {
success: false,
@@ -270,7 +270,7 @@ export async function getMaterialsForWorkOrder(
data: materials,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getMaterialsForWorkOrder error:', error);
return {
success: false,
@@ -313,7 +313,7 @@ export async function registerMaterialInput(
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] registerMaterialInput error:', error);
return {
success: false,
@@ -359,7 +359,7 @@ export async function reportIssue(
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] reportIssue error:', error);
return {
success: false,
@@ -471,7 +471,7 @@ export async function getProcessSteps(
data: steps,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getProcessSteps error:', error);
return {
success: false,
@@ -514,7 +514,7 @@ export async function requestInspection(
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] requestInspection error:', error);
return {
success: false,

View File

@@ -1,7 +1,7 @@
'use server';
import { isRedirectError } from 'next/dist/client/components/redirect';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
/**
* 검사 관리 Server Actions
* API 연동 완료 (2025-12-26)
@@ -265,7 +265,7 @@ export async function getInspections(params?: {
},
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] getInspections error:', error);
return {
success: false,
@@ -349,7 +349,7 @@ export async function getInspectionStats(params?: {
},
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] getInspectionStats error:', error);
return {
success: false,
@@ -411,7 +411,7 @@ export async function getInspectionById(id: string): Promise<{
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] getInspectionById error:', error);
return {
success: false,
@@ -498,7 +498,7 @@ export async function createInspection(data: {
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] createInspection error:', error);
return {
success: false,
@@ -590,7 +590,7 @@ export async function updateInspection(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] updateInspection error:', error);
return {
success: false,
@@ -640,7 +640,7 @@ export async function deleteInspection(id: string): Promise<{
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] deleteInspection error:', error);
return {
success: false,
@@ -708,7 +708,7 @@ export async function completeInspection(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] completeInspection error:', error);
return {
success: false,

View File

@@ -20,7 +20,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 {
Quote,
@@ -133,7 +133,7 @@ export async function getQuotes(params?: QuoteListParams): Promise<{
},
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] getQuotes error:', error);
return {
success: false,
@@ -210,7 +210,7 @@ export async function getQuoteById(id: string): Promise<{
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] getQuoteById error:', error);
return {
success: false,
@@ -267,7 +267,7 @@ export async function createQuote(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] createQuote error:', error);
return {
success: false,
@@ -325,7 +325,7 @@ export async function updateQuote(
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] updateQuote error:', error);
return {
success: false,
@@ -370,7 +370,7 @@ export async function deleteQuote(id: string): Promise<{ success: boolean; error
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] deleteQuote error:', error);
return {
success: false,
@@ -416,7 +416,7 @@ export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolea
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] bulkDeleteQuotes error:', error);
return {
success: false,
@@ -469,7 +469,7 @@ export async function finalizeQuote(id: string): Promise<{
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] finalizeQuote error:', error);
return {
success: false,
@@ -522,7 +522,7 @@ export async function cancelFinalizeQuote(id: string): Promise<{
data: transformApiToFrontend(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] cancelFinalizeQuote error:', error);
return {
success: false,
@@ -577,7 +577,7 @@ export async function convertQuoteToOrder(id: string): Promise<{
orderId: result.data?.order?.id ? String(result.data.order.id) : undefined,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] convertQuoteToOrder error:', error);
return {
success: false,
@@ -629,7 +629,7 @@ export async function getQuoteNumberPreview(): Promise<{
data: result.data?.quote_number || result.data,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] getQuoteNumberPreview error:', error);
return {
success: false,
@@ -681,7 +681,7 @@ export async function generateQuotePdf(id: string): Promise<{
data: blob,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] generateQuotePdf error:', error);
return {
success: false,
@@ -730,7 +730,7 @@ export async function sendQuoteEmail(
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] sendQuoteEmail error:', error);
return {
success: false,
@@ -779,7 +779,7 @@ export async function sendQuoteKakao(
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] sendQuoteKakao error:', error);
return {
success: false,
@@ -871,7 +871,7 @@ export async function getFinishedGoods(category?: string): Promise<{
})),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] getFinishedGoods error:', error);
return {
success: false,
@@ -964,7 +964,7 @@ export async function calculateBomBulk(items: BomCalculateItem[]): Promise<{
data: result.data || [],
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] calculateBomBulk error:', error);
return {
success: false,
@@ -1040,7 +1040,7 @@ export async function getQuotesSummary(params?: {
},
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] getQuotesSummary error:', error);
return {
success: false,
@@ -1085,7 +1085,7 @@ export async function getSiteNames(): Promise<{
data: siteNames,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[QuoteActions] getSiteNames error:', error);
return {
success: false,

View File

@@ -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 { ComprehensiveAnalysisData } from './types';
@@ -167,7 +167,7 @@ export async function getComprehensiveAnalysis(params?: {
data: transformedData,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[ComprehensiveAnalysisActions] getComprehensiveAnalysis error:', error);
return {
success: false,
@@ -208,7 +208,7 @@ export async function approveIssue(issueId: string): Promise<{
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[ComprehensiveAnalysisActions] approveIssue error:', error);
return {
success: false,
@@ -250,7 +250,7 @@ export async function rejectIssue(issueId: string, reason?: string): Promise<{
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[ComprehensiveAnalysisActions] rejectIssue error:', error);
return {
success: false,

View File

@@ -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 { AccountInfo, TermsAgreement, MarketingConsent } from './types';
@@ -112,7 +112,7 @@ export async function getAccountInfo(): Promise<{
},
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[AccountInfoActions] getAccountInfo error:', error);
return {
success: false,
@@ -162,7 +162,7 @@ export async function withdrawAccount(password: string): Promise<{
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[AccountInfoActions] withdrawAccount error:', error);
return {
success: false,
@@ -212,7 +212,7 @@ export async function suspendTenant(): Promise<{
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[AccountInfoActions] suspendTenant error:', error);
return {
success: false,
@@ -264,7 +264,7 @@ export async function updateAgreements(
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[AccountInfoActions] updateAgreements error:', error);
return {
success: false,
@@ -382,7 +382,7 @@ export async function uploadProfileImage(formData: FormData): Promise<{
},
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[AccountInfoActions] uploadProfileImage error:', error);
return {
success: false,

View File

@@ -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 { Account, AccountFormData, AccountStatus } from './types';
import { BANK_LABELS } from './types';
@@ -130,7 +130,7 @@ export async function getBankAccounts(params?: {
};
return { success: true, data: accounts, meta };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[getBankAccounts] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -173,7 +173,7 @@ export async function getBankAccount(id: number): Promise<{
const account = transformApiToFrontend(result.data);
return { success: true, data: account };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[getBankAccount] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -218,7 +218,7 @@ export async function createBankAccount(data: AccountFormData): Promise<{
const account = transformApiToFrontend(result.data);
return { success: true, data: account };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[createBankAccount] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -266,7 +266,7 @@ export async function updateBankAccount(
const account = transformApiToFrontend(result.data);
return { success: true, data: account };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[updateBankAccount] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -306,7 +306,7 @@ export async function deleteBankAccount(id: number): Promise<{
return { success: true };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[deleteBankAccount] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -348,7 +348,7 @@ export async function toggleBankAccountStatus(id: number): Promise<{
const account = transformApiToFrontend(result.data);
return { success: true, data: account };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[toggleBankAccountStatus] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -390,7 +390,7 @@ export async function setPrimaryBankAccount(id: number): Promise<{
const account = transformApiToFrontend(result.data);
return { success: true, data: account };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[setPrimaryBankAccount] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
@@ -421,7 +421,7 @@ export async function deleteBankAccounts(ids: number[]): Promise<{
return { success: true, deletedCount: successCount };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[deleteBankAccounts] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}

View File

@@ -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_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -112,7 +112,7 @@ export async function getAttendanceSetting(): Promise<ApiResponse<AttendanceSett
data: transformFromApi(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('getAttendanceSetting error:', error);
return {
success: false,
@@ -152,7 +152,7 @@ export async function updateAttendanceSetting(
data: transformFromApi(result.data),
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('updateAttendanceSetting error:', error);
return {
success: false,
@@ -211,7 +211,7 @@ export async function getDepartments(): Promise<ApiResponse<Department[]>> {
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,

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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<LeavePolicySettings>): Pro
data: transformedData,
};
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[LeavePolicyActions] updateLeavePolicy error:', error);
return {
success: false,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<ApiResponse<Role>> {
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<ApiResponse<void>> {
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<ApiResponse<RoleStats>> {
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<ApiResponse<Role[]>> {
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<ApiResponse<{
return { success: true, data: result.data };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('Failed to fetch permission menus:', error);
return { success: false, error: error instanceof Error ? error.message : '메뉴 트리 조회 실패' };
}
@@ -305,7 +305,7 @@ export async function fetchPermissionMatrix(roleId: number): Promise<ApiResponse
return { success: true, data: result.data };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('Failed to fetch permission matrix:', error);
return { success: false, error: error instanceof Error ? error.message : '권한 매트릭스 조회 실패' };
}
@@ -348,7 +348,7 @@ export async function togglePermission(
revalidatePath(`/settings/permissions/${roleId}`);
return { success: true, data: result.data };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('Failed to toggle permission:', error);
return { success: false, error: error instanceof Error ? error.message : '권한 토글 실패' };
}
@@ -380,7 +380,7 @@ export async function allowAllPermissions(roleId: number): Promise<ApiResponse<{
revalidatePath(`/settings/permissions/${roleId}`);
return { success: true, data: result.data };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('Failed to allow all permissions:', error);
return { success: false, error: error instanceof Error ? error.message : '전체 허용 실패' };
}
@@ -412,7 +412,7 @@ export async function denyAllPermissions(roleId: number): Promise<ApiResponse<{
revalidatePath(`/settings/permissions/${roleId}`);
return { success: true, data: result.data };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('Failed to deny all permissions:', error);
return { success: false, error: error instanceof Error ? error.message : '전체 거부 실패' };
}
@@ -444,7 +444,7 @@ export async function resetPermissions(roleId: number): Promise<ApiResponse<{ co
revalidatePath(`/settings/permissions/${roleId}`);
return { success: true, data: result.data };
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('Failed to reset permissions:', error);
return { success: false, error: error instanceof Error ? error.message : '권한 초기화 실패' };
}

View File

@@ -12,7 +12,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 { Popup, PopupFormData } from './types';
import { transformApiToFrontend, transformFrontendToApi, type PopupApiData } from './utils';
@@ -82,7 +82,7 @@ export async function getPopups(params?: {
return result.data.data.map(transformApiToFrontend);
} catch (error) {
if (isRedirectError(error)) throw error;
if (isNextRedirectError(error)) throw error;
console.error('[PopupActions] getPopups error:', error);
return [];
}
@@ -119,7 +119,7 @@ export async function getPopupById(id: string): Promise<Popup | null> {
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,

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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,

View File

@@ -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: '서버 오류가 발생했습니다.' };
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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')
);
}