refactor(WEB): 전체 actions.ts에 공통 API 유틸 적용
- buildApiUrl / executePaginatedAction 패턴으로 전환 (40+ actions 파일) - 직접 URLSearchParams 조립 → buildApiUrl 유틸 사용 - 수동 페이지네이션 메타 변환 → executePaginatedAction 자동 처리 - HandoverReportDocumentModal, OrderDocumentModal 개선 - 급여관리 SalaryManagement 코드 개선 - CLAUDE.md Server Action 공통 유틸 규칙 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
53
CLAUDE.md
53
CLAUDE.md
@@ -326,36 +326,45 @@ const form = useForm<FormData>({
|
||||
|
||||
---
|
||||
|
||||
## Server Action 공통 유틸리티 (신규 코드 적용)
|
||||
**Priority**: 🟡
|
||||
## Server Action 공통 유틸리티
|
||||
**Priority**: 🔴
|
||||
|
||||
### 신규 actions.ts 작성 시 필수 패턴:
|
||||
- `buildApiUrl()` 사용 (직접 URLSearchParams 조립 금지)
|
||||
### 규칙:
|
||||
- `buildApiUrl()` 사용 필수 (직접 `new URLSearchParams` 또는 `${API_URL}` 조립 금지)
|
||||
- 페이지네이션 조회 → `executePaginatedAction()` 사용
|
||||
- 단건/목록 조회 → `executeServerAction()` 유지
|
||||
- `toPaginationMeta()` 직접 사용도 허용
|
||||
|
||||
```typescript
|
||||
// ✅ 신규 코드 패턴
|
||||
import { buildApiUrl, executePaginatedAction } from '@/lib/api';
|
||||
### 현황:
|
||||
- **전체 43개 actions.ts 마이그레이션 완료** (2026-02-12)
|
||||
- `new URLSearchParams` 사용 0건 (actions.ts 기준)
|
||||
- 모든 URL 빌딩은 `buildApiUrl(path, params)` 사용
|
||||
|
||||
export async function getItems(params: SearchParams) {
|
||||
return executePaginatedAction({
|
||||
url: buildApiUrl('/api/v1/items', {
|
||||
search: params.search,
|
||||
status: params.status !== 'all' ? params.status : undefined,
|
||||
page: params.page,
|
||||
per_page: params.perPage,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
```typescript
|
||||
// ✅ 필수 패턴
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
|
||||
// 쿼리 파라미터 있는 경우
|
||||
url: buildApiUrl('/api/v1/items', {
|
||||
search: params.search,
|
||||
status: params.status !== 'all' ? params.status : undefined,
|
||||
page: params.page,
|
||||
}),
|
||||
|
||||
// 동적 경로 + 파라미터
|
||||
url: buildApiUrl(`/api/v1/items/${id}`, { with_details: true }),
|
||||
|
||||
// 파라미터 없는 단순 경로
|
||||
url: buildApiUrl('/api/v1/items'),
|
||||
```
|
||||
|
||||
### 기존 코드: 마이그레이션 없음
|
||||
- 잘 동작하는 기존 actions.ts는 수정하지 않음
|
||||
- 해당 파일을 수정할 일이 생길 때만 선택적으로 적용
|
||||
```typescript
|
||||
// ❌ 금지 패턴
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
const params = new URLSearchParams();
|
||||
params.set('search', value);
|
||||
url: `${API_URL}/api/v1/items?${params.toString()}`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
| 항목 | 상태 | 날짜 |
|
||||
|------|------|------|
|
||||
| Phase 1: 공통 훅 추출 (executeServerAction 등) | ✅ 완료 | 이전 세션 |
|
||||
| 중복 코드 공통화 (buildApiUrl + executePaginatedAction) | ✅ 완료 | 2026-02-11 |
|
||||
| 중복 코드 공통화 (buildApiUrl 전체 43개 actions.ts 마이그레이션) | ✅ 완료 | 2026-02-12 |
|
||||
| executePaginatedAction 전체 마이그레이션 (14개 actions.ts, ~220줄 감소) | ✅ 완료 | 2026-02-12 |
|
||||
| Phase 3: 공용 유틸 추출 (PaginatedApiResponse 등) | ✅ 완료 | 이전 세션 |
|
||||
| Phase 4: SearchableSelectionModal 공통화 | ✅ 완료 | 이전 세션 |
|
||||
| Phase 5: any 21건 + memo 3개 정리 | ✅ 완료 | 이전 세션 |
|
||||
|
||||
@@ -162,9 +162,9 @@ export const remove = service.remove;
|
||||
|
||||
**미전환 사유**: 84개 중 전환 가능 15~20개, 작업 2~4시간 대비 기능 변화 없음. 시간 대비 효율 낮음
|
||||
|
||||
### Server Action 공통 유틸리티 — 신규 코드 적용 규칙 (2026-02-11)
|
||||
### Server Action 공통 유틸리티 — 전체 마이그레이션 완료 (2026-02-12)
|
||||
|
||||
**결정**: 기존 actions.ts 마이그레이션 없음. **신규 actions.ts에만 `buildApiUrl` + `executePaginatedAction` 적용**
|
||||
**결정**: `buildApiUrl()` 전체 43개 actions.ts에 적용 완료
|
||||
|
||||
**배경**:
|
||||
- 89개 actions.ts 중 43개에서 동일한 URLSearchParams 조건부 `.set()` 패턴 반복 (326+ 건)
|
||||
@@ -175,12 +175,30 @@ export const remove = service.remove;
|
||||
1. `src/lib/api/query-params.ts` — `buildQueryParams()`, `buildApiUrl()`: URLSearchParams 보일러플레이트 제거
|
||||
2. `src/lib/api/execute-paginated-action.ts` — `executePaginatedAction()`: 페이지네이션 조회 패턴 통합 (내부에서 `toPaginationMeta` 사용)
|
||||
|
||||
**마이그레이션 결과** (2026-02-12):
|
||||
- `new URLSearchParams` 사용: 326건 → **0건** (actions.ts 기준)
|
||||
- `const API_URL = process.env.NEXT_PUBLIC_API_URL` 선언: 43개 → **0개** (마이그레이션 대상 파일)
|
||||
- `buildApiUrl()` import: 43개 actions.ts 전체 적용
|
||||
- 3가지 API_URL 패턴 통합: 표준(`process.env`), `/api` 접미사(HR), `API_BASE` 전체경로(품질) → 모두 `buildApiUrl('/api/v1/...')` 통일
|
||||
|
||||
**`executePaginatedAction` 마이그레이션** (2026-02-12):
|
||||
- 14개 actions.ts에서 페이지네이션 목록 조회 함수를 `executePaginatedAction`으로 전환
|
||||
- Wave A (accounting 9개): BillManagement, DepositManagement, SalesManagement, PurchaseManagement, WithdrawalManagement, VendorLedger, CardTransactionInquiry, BankTransactionInquiry, ExpectedExpenseManagement
|
||||
- Wave B (5개): PaymentHistoryManagement, StockStatus, ReceivingManagement, ShipmentManagement, quotes
|
||||
- 제외 5개: AccountManagement(`meta` 필드명), orders(`data.items` 중첩), VacationManagement, EmployeeManagement, construction/order-management (별도 구조)
|
||||
- 순 감소: ~220줄 (14파일 × ~20줄 제거, ~28줄 추가)
|
||||
- 제거된 보일러플레이트: `DEFAULT_PAGINATION`, `FrontendPagination`/`PaginationMeta` 로컬 인터페이스, `PaginatedApiResponse` import, 수동 transform+pagination 조립
|
||||
|
||||
**buildApiUrl 마이그레이션 전략**:
|
||||
- Wave A: 1건짜리 단순 파일 20개
|
||||
- Wave B: 2건짜리 파일 12개 (quotes, WorkOrders, orders 등 대형 파일 포함)
|
||||
- Wave C: 3건 이상 파일 12개 (VendorLedger 5건, ReceivingManagement 5건, ProcessManagement 19건 URL 등)
|
||||
|
||||
**효과**:
|
||||
- 페이지네이션 조회 코드: ~20줄 → ~5줄
|
||||
- `DEFAULT_PAGINATION` 중앙화 (`execute-paginated-action.ts` 내부)
|
||||
- `toPaginationMeta` 자동 활용 (직접 import 불필요)
|
||||
|
||||
**미적용 사유**: 기존 89개 actions.ts는 정상 동작 중. 전면 전환 비용 >> 이득
|
||||
- URL 빌딩 패턴 완전 일관화 (undefined/null/'' 자동 필터링, boolean/number 자동 변환)
|
||||
|
||||
### Zod 스키마 검증 — 신규 폼 적용 규칙 (2026-02-11)
|
||||
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { BadDebtRecord, BadDebtItem, CollectionStatus } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
|
||||
interface BadDebtItemApiData {
|
||||
@@ -157,18 +156,13 @@ export async function getBadDebts(params?: {
|
||||
status?: string;
|
||||
client_id?: string;
|
||||
}): Promise<BadDebtRecord[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', mapFrontendStatusToApi(params.status as CollectionStatus));
|
||||
}
|
||||
if (params?.client_id && params.client_id !== 'all') {
|
||||
searchParams.set('client_id', params.client_id);
|
||||
}
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/bad-debts', {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
status: params?.status && params.status !== 'all' ? mapFrontendStatusToApi(params.status as CollectionStatus) : undefined,
|
||||
client_id: params?.client_id && params.client_id !== 'all' ? params.client_id : undefined,
|
||||
}),
|
||||
transform: (data: PaginatedApiResponse<BadDebtApiData>) => data.data.map(transformApiToFrontend),
|
||||
errorMessage: '악성채권 목록 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -178,7 +172,7 @@ export async function getBadDebts(params?: {
|
||||
// ===== 악성채권 상세 조회 =====
|
||||
export async function getBadDebtById(id: string): Promise<BadDebtRecord | null> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bad-debts/${id}`),
|
||||
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '악성채권 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -188,7 +182,7 @@ export async function getBadDebtById(id: string): Promise<BadDebtRecord | null>
|
||||
// ===== 악성채권 통계 조회 =====
|
||||
export async function getBadDebtSummary(): Promise<BadDebtSummaryApiData | null> {
|
||||
const result = await executeServerAction<BadDebtSummaryApiData>({
|
||||
url: `${API_URL}/api/v1/bad-debts/summary`,
|
||||
url: buildApiUrl('/api/v1/bad-debts/summary'),
|
||||
errorMessage: '악성채권 통계 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || null;
|
||||
@@ -199,7 +193,7 @@ export async function createBadDebt(
|
||||
data: Partial<BadDebtRecord>
|
||||
): Promise<ActionResult<BadDebtRecord>> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts`,
|
||||
url: buildApiUrl('/api/v1/bad-debts'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
||||
@@ -215,7 +209,7 @@ export async function updateBadDebt(
|
||||
data: Partial<BadDebtRecord>
|
||||
): Promise<ActionResult<BadDebtRecord>> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bad-debts/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
||||
@@ -228,7 +222,7 @@ export async function updateBadDebt(
|
||||
// ===== 악성채권 삭제 =====
|
||||
export async function deleteBadDebt(id: string): Promise<ActionResult> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bad-debts/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '악성채권 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -239,7 +233,7 @@ export async function deleteBadDebt(id: string): Promise<ActionResult> {
|
||||
// ===== 악성채권 활성화 토글 =====
|
||||
export async function toggleBadDebt(id: string): Promise<ActionResult<BadDebtRecord>> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts/${id}/toggle`,
|
||||
url: buildApiUrl(`/api/v1/bad-debts/${id}/toggle`),
|
||||
method: 'PATCH',
|
||||
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '상태 변경에 실패했습니다.',
|
||||
@@ -254,7 +248,7 @@ export async function addBadDebtMemo(
|
||||
content: string
|
||||
): Promise<ActionResult<{ id: string; content: string; createdAt: string; createdBy: string }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts/${badDebtId}/memos`,
|
||||
url: buildApiUrl(`/api/v1/bad-debts/${badDebtId}/memos`),
|
||||
method: 'POST',
|
||||
body: { content },
|
||||
transform: (memo: { id: number; content: string; created_at: string; created_by_user?: { name: string } | null }) => ({
|
||||
@@ -273,8 +267,8 @@ export async function deleteBadDebtMemo(
|
||||
memoId: string
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts/${badDebtId}/memos/${memoId}`,
|
||||
url: buildApiUrl(`/api/v1/bad-debts/${badDebtId}/memos/${memoId}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '메모 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { BankTransaction, TransactionKind } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface BankTransactionApiItem {
|
||||
id: number;
|
||||
@@ -35,11 +34,6 @@ interface BankTransactionApiSummary {
|
||||
withdrawal_unset_count: number;
|
||||
}
|
||||
|
||||
type BankTransactionPaginatedResponse = PaginatedApiResponse<BankTransactionApiItem>;
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== API → Frontend 변환 =====
|
||||
function transformItem(item: BankTransactionApiItem): BankTransaction {
|
||||
return {
|
||||
@@ -67,41 +61,33 @@ export async function getBankTransactionList(params?: {
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
bankAccountId?: number; transactionType?: string; search?: string;
|
||||
sortBy?: string; sortDir?: 'asc' | 'desc';
|
||||
}): Promise<{ success: boolean; data: BankTransaction[]; pagination: FrontendPagination; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.bankAccountId) searchParams.set('bank_account_id', String(params.bankAccountId));
|
||||
if (params?.transactionType) searchParams.set('transaction_type', params.transactionType);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-transactions${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: BankTransactionPaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformItem),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
||||
}) {
|
||||
return executePaginatedAction<BankTransactionApiItem, BankTransaction>({
|
||||
url: buildApiUrl('/api/v1/bank-transactions', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
bank_account_id: params?.bankAccountId,
|
||||
transaction_type: params?.transactionType,
|
||||
search: params?.search,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
}),
|
||||
transform: transformItem,
|
||||
errorMessage: '은행 거래 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 입출금 요약 통계 =====
|
||||
export async function getBankTransactionSummary(params?: {
|
||||
startDate?: string; endDate?: string;
|
||||
}): Promise<ActionResult<{ totalDeposit: number; totalWithdrawal: number; depositUnsetCount: number; withdrawalUnsetCount: number }>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-transactions/summary${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/bank-transactions/summary', {
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
}),
|
||||
transform: (data: BankTransactionApiSummary) => ({
|
||||
totalDeposit: data.total_deposit,
|
||||
totalWithdrawal: data.total_withdrawal,
|
||||
@@ -117,9 +103,9 @@ export async function getBankAccountOptions(): Promise<{
|
||||
success: boolean; data: { id: number; label: string }[]; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-transactions/accounts`,
|
||||
url: buildApiUrl('/api/v1/bank-transactions/accounts'),
|
||||
transform: (data: { id: number; label: string }[]) => data,
|
||||
errorMessage: '계좌 목록 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data || [], error: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { BillRecord, BillApiData, BillStatus } from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
type BillPaginatedResponse = PaginatedApiResponse<BillApiData>;
|
||||
|
||||
interface BillSummaryApiData {
|
||||
total_amount: number;
|
||||
total_count: number;
|
||||
@@ -19,47 +16,38 @@ interface BillSummaryApiData {
|
||||
maturity_alert_amount: number;
|
||||
}
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== 어음 목록 조회 =====
|
||||
export async function getBills(params: {
|
||||
search?: string; billType?: string; status?: string; clientId?: string;
|
||||
isElectronic?: boolean; issueStartDate?: string; issueEndDate?: string;
|
||||
maturityStartDate?: string; maturityEndDate?: string;
|
||||
sortBy?: string; sortDir?: string; perPage?: number; page?: number;
|
||||
}): Promise<{ success: boolean; data: BillRecord[]; pagination: FrontendPagination; error?: string }> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.search) queryParams.append('search', params.search);
|
||||
if (params.billType && params.billType !== 'all') queryParams.append('bill_type', params.billType);
|
||||
if (params.status && params.status !== 'all') queryParams.append('status', params.status);
|
||||
if (params.clientId) queryParams.append('client_id', params.clientId);
|
||||
if (params.isElectronic !== undefined) queryParams.append('is_electronic', String(params.isElectronic));
|
||||
if (params.issueStartDate) queryParams.append('issue_start_date', params.issueStartDate);
|
||||
if (params.issueEndDate) queryParams.append('issue_end_date', params.issueEndDate);
|
||||
if (params.maturityStartDate) queryParams.append('maturity_start_date', params.maturityStartDate);
|
||||
if (params.maturityEndDate) queryParams.append('maturity_end_date', params.maturityEndDate);
|
||||
if (params.sortBy) queryParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) queryParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) queryParams.append('per_page', String(params.perPage));
|
||||
if (params.page) queryParams.append('page', String(params.page));
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bills${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: BillPaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformApiToFrontend),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
||||
}) {
|
||||
return executePaginatedAction<BillApiData, BillRecord>({
|
||||
url: buildApiUrl('/api/v1/bills', {
|
||||
search: params.search,
|
||||
bill_type: params.billType && params.billType !== 'all' ? params.billType : undefined,
|
||||
status: params.status && params.status !== 'all' ? params.status : undefined,
|
||||
client_id: params.clientId,
|
||||
is_electronic: params.isElectronic,
|
||||
issue_start_date: params.issueStartDate,
|
||||
issue_end_date: params.issueEndDate,
|
||||
maturity_start_date: params.maturityStartDate,
|
||||
maturity_end_date: params.maturityEndDate,
|
||||
sort_by: params.sortBy,
|
||||
sort_dir: params.sortDir,
|
||||
per_page: params.perPage,
|
||||
page: params.page,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '어음 목록 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 어음 상세 조회 =====
|
||||
export async function getBill(id: string): Promise<ActionResult<BillRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bills/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bills/${id}`),
|
||||
transform: (data: BillApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '어음 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -68,7 +56,7 @@ export async function getBill(id: string): Promise<ActionResult<BillRecord>> {
|
||||
// ===== 어음 등록 =====
|
||||
export async function createBill(data: Partial<BillRecord>): Promise<ActionResult<BillRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bills`,
|
||||
url: buildApiUrl('/api/v1/bills'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: BillApiData) => transformApiToFrontend(d),
|
||||
@@ -79,7 +67,7 @@ export async function createBill(data: Partial<BillRecord>): Promise<ActionResul
|
||||
// ===== 어음 수정 =====
|
||||
export async function updateBill(id: string, data: Partial<BillRecord>): Promise<ActionResult<BillRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bills/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bills/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: BillApiData) => transformApiToFrontend(d),
|
||||
@@ -90,7 +78,7 @@ export async function updateBill(id: string, data: Partial<BillRecord>): Promise
|
||||
// ===== 어음 삭제 =====
|
||||
export async function deleteBill(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bills/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bills/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '어음 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -99,7 +87,7 @@ export async function deleteBill(id: string): Promise<ActionResult> {
|
||||
// ===== 어음 상태 변경 =====
|
||||
export async function updateBillStatus(id: string, status: BillStatus): Promise<ActionResult<BillRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bills/${id}/status`,
|
||||
url: buildApiUrl(`/api/v1/bills/${id}/status`),
|
||||
method: 'PATCH',
|
||||
body: { status },
|
||||
transform: (data: BillApiData) => transformApiToFrontend(data),
|
||||
@@ -117,16 +105,14 @@ export async function getBillSummary(params: {
|
||||
byStatus: Record<string, { total: number; count: number }>;
|
||||
maturityAlertAmount: number;
|
||||
}>> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.billType && params.billType !== 'all') queryParams.append('bill_type', params.billType);
|
||||
if (params.issueStartDate) queryParams.append('issue_start_date', params.issueStartDate);
|
||||
if (params.issueEndDate) queryParams.append('issue_end_date', params.issueEndDate);
|
||||
if (params.maturityStartDate) queryParams.append('maturity_start_date', params.maturityStartDate);
|
||||
if (params.maturityEndDate) queryParams.append('maturity_end_date', params.maturityEndDate);
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bills/summary${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/bills/summary', {
|
||||
bill_type: params.billType && params.billType !== 'all' ? params.billType : undefined,
|
||||
issue_start_date: params.issueStartDate,
|
||||
issue_end_date: params.issueEndDate,
|
||||
maturity_start_date: params.maturityStartDate,
|
||||
maturity_end_date: params.maturityEndDate,
|
||||
}),
|
||||
transform: (data: BillSummaryApiData) => ({
|
||||
totalAmount: data.total_amount,
|
||||
totalCount: data.total_count,
|
||||
@@ -141,7 +127,7 @@ export async function getBillSummary(params: {
|
||||
// ===== 거래처 목록 조회 =====
|
||||
export async function getClients(): Promise<ActionResult<{ id: number; name: string }[]>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients?per_page=100`,
|
||||
url: buildApiUrl('/api/v1/clients', { per_page: 100 }),
|
||||
transform: (data: { data?: { id: number; name: string }[] } | { id: number; name: string }[]) => {
|
||||
type ClientApi = { id: number; name: string };
|
||||
const clients: ClientApi[] = Array.isArray(data) ? data : (data as { data?: ClientApi[] })?.data || [];
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { CardTransaction } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface CardTransactionApiItem {
|
||||
id: number;
|
||||
@@ -36,11 +35,6 @@ interface CardTransactionApiSummary {
|
||||
total_amount: number;
|
||||
}
|
||||
|
||||
type CardPaginatedResponse = PaginatedApiResponse<CardTransactionApiItem>;
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== API → Frontend 변환 =====
|
||||
function transformItem(item: CardTransactionApiItem): CardTransaction {
|
||||
const card = item.card;
|
||||
@@ -67,40 +61,32 @@ function transformItem(item: CardTransactionApiItem): CardTransaction {
|
||||
export async function getCardTransactionList(params?: {
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
cardId?: number; search?: string; sortBy?: string; sortDir?: 'asc' | 'desc';
|
||||
}): Promise<{ success: boolean; data: CardTransaction[]; pagination: FrontendPagination; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.cardId) searchParams.set('card_id', String(params.cardId));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/card-transactions${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: CardPaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformItem),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
||||
}) {
|
||||
return executePaginatedAction<CardTransactionApiItem, CardTransaction>({
|
||||
url: buildApiUrl('/api/v1/card-transactions', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
card_id: params?.cardId,
|
||||
search: params?.search,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
}),
|
||||
transform: transformItem,
|
||||
errorMessage: '카드 거래 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 카드 거래 요약 통계 =====
|
||||
export async function getCardTransactionSummary(params?: {
|
||||
startDate?: string; endDate?: string;
|
||||
}): Promise<ActionResult<{ previousMonthTotal: number; currentMonthTotal: number; totalCount: number; totalAmount: number }>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/card-transactions/summary${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/card-transactions/summary', {
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
}),
|
||||
transform: (data: CardTransactionApiSummary) => ({
|
||||
previousMonthTotal: data.previous_month_total,
|
||||
currentMonthTotal: data.current_month_total,
|
||||
@@ -114,7 +100,7 @@ export async function getCardTransactionSummary(params?: {
|
||||
// ===== 카드 거래 단건 조회 =====
|
||||
export async function getCardTransactionById(id: string): Promise<ActionResult<CardTransaction>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/card-transactions/${id}`,
|
||||
url: buildApiUrl(`/api/v1/card-transactions/${id}`),
|
||||
transform: (data: CardTransactionApiItem) => transformItem(data),
|
||||
errorMessage: '조회에 실패했습니다.',
|
||||
});
|
||||
@@ -125,7 +111,7 @@ export async function createCardTransaction(data: {
|
||||
cardId?: number; usedAt: string; merchantName: string; amount: number; memo?: string; usageType?: string;
|
||||
}): Promise<ActionResult<CardTransaction>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/card-transactions`,
|
||||
url: buildApiUrl('/api/v1/card-transactions'),
|
||||
method: 'POST',
|
||||
body: {
|
||||
card_id: data.cardId, used_at: data.usedAt, merchant_name: data.merchantName,
|
||||
@@ -142,7 +128,7 @@ export async function updateCardTransaction(id: string, data: {
|
||||
usedAt?: string; merchantName?: string; amount?: number; memo?: string; usageType?: string;
|
||||
}): Promise<ActionResult<CardTransaction>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/card-transactions/${id}`,
|
||||
url: buildApiUrl(`/api/v1/card-transactions/${id}`),
|
||||
method: 'PUT',
|
||||
body: {
|
||||
used_at: data.usedAt, merchant_name: data.merchantName, amount: data.amount,
|
||||
@@ -156,7 +142,7 @@ export async function updateCardTransaction(id: string, data: {
|
||||
// ===== 카드 거래 삭제 =====
|
||||
export async function deleteCardTransaction(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/card-transactions/${id}`,
|
||||
url: buildApiUrl(`/api/v1/card-transactions/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -167,7 +153,7 @@ export async function getCardList(): Promise<{
|
||||
success: boolean; data: Array<{ id: number; name: string; cardNumber: string }>; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/cards`,
|
||||
url: buildApiUrl('/api/v1/cards'),
|
||||
transform: (data: { data?: { id: number; card_name: string; card_company: string; card_number_last4: string }[] } | { id: number; card_name: string; card_company: string; card_number_last4: string }[]) => {
|
||||
type CardApi = { id: number; card_name: string; card_company: string; card_number_last4: string };
|
||||
const cards: CardApi[] = Array.isArray(data) ? data : (data as { data?: CardApi[] })?.data || [];
|
||||
@@ -183,11 +169,11 @@ export async function bulkUpdateAccountCode(ids: number[], accountCode: string):
|
||||
success: boolean; updatedCount?: number; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/card-transactions/bulk-update-account`,
|
||||
url: buildApiUrl('/api/v1/card-transactions/bulk-update-account'),
|
||||
method: 'PUT',
|
||||
body: { ids, account_code: accountCode },
|
||||
transform: (data: { updated_count?: number }) => ({ updatedCount: data?.updated_count || 0 }),
|
||||
errorMessage: '계정과목 수정에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, updatedCount: result.data?.updatedCount, error: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { NoteReceivableItem, DailyAccountItem, MatchStatus } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface NoteReceivableItemApi {
|
||||
id: string;
|
||||
@@ -58,12 +57,8 @@ function transformDailyAccount(item: DailyAccountItemApi): DailyAccountItem {
|
||||
export async function getNoteReceivables(params?: {
|
||||
date?: string;
|
||||
}): Promise<{ success: boolean; data: NoteReceivableItem[]; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/daily-report/note-receivables${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/daily-report/note-receivables', { date: params?.date }),
|
||||
transform: (data: NoteReceivableItemApi[]) => (data || []).map(transformNoteReceivable),
|
||||
errorMessage: '어음 현황 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -74,12 +69,8 @@ export async function getNoteReceivables(params?: {
|
||||
export async function getDailyAccounts(params?: {
|
||||
date?: string;
|
||||
}): Promise<{ success: boolean; data: DailyAccountItem[]; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/daily-report/daily-accounts${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/daily-report/daily-accounts', { date: params?.date }),
|
||||
transform: (data: DailyAccountItemApi[]) => (data || []).map(transformDailyAccount),
|
||||
errorMessage: '계좌 현황 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -98,12 +89,8 @@ export async function getDailyReportSummary(params?: {
|
||||
krwTotals: { carryover: number; income: number; expense: number; balance: number };
|
||||
usdTotals: { carryover: number; income: number; expense: number; balance: number };
|
||||
}>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/daily-report/summary${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/daily-report/summary', { date: params?.date }),
|
||||
transform: (data: DailyReportSummaryApi) => ({
|
||||
date: data.date,
|
||||
dayOfWeek: data.day_of_week,
|
||||
@@ -136,12 +123,8 @@ export async function exportDailyReportExcel(params?: {
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const response = await fetch(
|
||||
`${API_URL}/api/v1/daily-report/export${queryString ? `?${queryString}` : ''}`,
|
||||
buildApiUrl('/api/v1/daily-report/export', { date: params?.date }),
|
||||
{ method: 'GET', headers }
|
||||
);
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { fetchVendorOptions, fetchBankAccountOptions } from '@/lib/api/shared-lookups';
|
||||
import type { DepositRecord, DepositType, DepositStatus } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface DepositApiData {
|
||||
id: number;
|
||||
@@ -28,11 +27,6 @@ interface DepositApiData {
|
||||
bank_account?: { id: number; bank_name: string; account_name: string } | null;
|
||||
}
|
||||
|
||||
type DepositPaginatedResponse = PaginatedApiResponse<DepositApiData>;
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== API → Frontend 변환 =====
|
||||
function transformApiToFrontend(apiData: DepositApiData): DepositRecord {
|
||||
return {
|
||||
@@ -74,40 +68,26 @@ function transformFrontendToApi(data: Partial<DepositRecord>): Record<string, un
|
||||
export async function getDeposits(params?: {
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
depositType?: string; vendor?: string; search?: string;
|
||||
}): Promise<{ success: boolean; data: DepositRecord[]; pagination: FrontendPagination; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.depositType && params.depositType !== 'all') searchParams.set('deposit_type', params.depositType);
|
||||
if (params?.vendor && params.vendor !== 'all') searchParams.set('vendor', params.vendor);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/deposits${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: DepositPaginatedResponse | DepositApiData[]) => {
|
||||
const isPaginated = !Array.isArray(data) && data && 'data' in data;
|
||||
const rawData = isPaginated ? (data as DepositPaginatedResponse).data : (Array.isArray(data) ? data : []);
|
||||
const items = rawData.map(transformApiToFrontend);
|
||||
const meta = isPaginated
|
||||
? (data as DepositPaginatedResponse)
|
||||
: { current_page: 1, last_page: 1, per_page: 20, total: items.length };
|
||||
return {
|
||||
items,
|
||||
pagination: { currentPage: meta.current_page || 1, lastPage: meta.last_page || 1, perPage: meta.per_page || 20, total: meta.total || items.length },
|
||||
};
|
||||
},
|
||||
}) {
|
||||
return executePaginatedAction<DepositApiData, DepositRecord>({
|
||||
url: buildApiUrl('/api/v1/deposits', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
deposit_type: params?.depositType !== 'all' ? params?.depositType : undefined,
|
||||
vendor: params?.vendor !== 'all' ? params?.vendor : undefined,
|
||||
search: params?.search,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '입금 내역 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 입금 내역 삭제 =====
|
||||
export async function deleteDeposit(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/deposits/${id}`,
|
||||
url: buildApiUrl(`/api/v1/deposits/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '입금 내역 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -116,7 +96,7 @@ export async function deleteDeposit(id: string): Promise<ActionResult> {
|
||||
// ===== 계정과목명 일괄 저장 =====
|
||||
export async function updateDepositTypes(ids: string[], depositType: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/deposits/bulk-update-type`,
|
||||
url: buildApiUrl('/api/v1/deposits/bulk-update-type'),
|
||||
method: 'PUT',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)), deposit_type: depositType },
|
||||
errorMessage: '계정과목명 저장에 실패했습니다.',
|
||||
@@ -126,7 +106,7 @@ export async function updateDepositTypes(ids: string[], depositType: string): Pr
|
||||
// ===== 입금 상세 조회 =====
|
||||
export async function getDepositById(id: string): Promise<ActionResult<DepositRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/deposits/${id}`,
|
||||
url: buildApiUrl(`/api/v1/deposits/${id}`),
|
||||
transform: (data: DepositApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '입금 내역 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -135,7 +115,7 @@ export async function getDepositById(id: string): Promise<ActionResult<DepositRe
|
||||
// ===== 입금 등록 =====
|
||||
export async function createDeposit(data: Partial<DepositRecord>): Promise<ActionResult<DepositRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/deposits`,
|
||||
url: buildApiUrl('/api/v1/deposits'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: DepositApiData) => transformApiToFrontend(data),
|
||||
@@ -146,7 +126,7 @@ export async function createDeposit(data: Partial<DepositRecord>): Promise<Actio
|
||||
// ===== 입금 수정 =====
|
||||
export async function updateDeposit(id: string, data: Partial<DepositRecord>): Promise<ActionResult<DepositRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/deposits/${id}`,
|
||||
url: buildApiUrl(`/api/v1/deposits/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: DepositApiData) => transformApiToFrontend(data),
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { fetchBankAccountDetailOptions } from '@/lib/api/shared-lookups';
|
||||
import type { ExpectedExpenseRecord, TransactionType, PaymentStatus, ApprovalStatus } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface ExpectedExpenseApiData {
|
||||
id: number;
|
||||
@@ -29,8 +28,6 @@ interface ExpectedExpenseApiData {
|
||||
bank_account?: { id: number; bank_name: string; account_name: string } | null;
|
||||
}
|
||||
|
||||
type ExpensePaginatedResponse = PaginatedApiResponse<ExpectedExpenseApiData>;
|
||||
|
||||
interface SummaryData {
|
||||
total_amount: number;
|
||||
total_count: number;
|
||||
@@ -39,9 +36,6 @@ interface SummaryData {
|
||||
by_month: Record<string, { count: number; amount: number }>;
|
||||
}
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 50, total: 0 };
|
||||
|
||||
// ===== API → Frontend 변환 =====
|
||||
function transformApiToFrontend(apiData: ExpectedExpenseApiData): ExpectedExpenseRecord {
|
||||
return {
|
||||
@@ -83,36 +77,30 @@ export async function getExpectedExpenses(params?: {
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
transactionType?: string; paymentStatus?: string; approvalStatus?: string;
|
||||
clientId?: string; search?: string; sortBy?: string; sortDir?: 'asc' | 'desc';
|
||||
}): Promise<{ success: boolean; data: ExpectedExpenseRecord[]; pagination: FrontendPagination; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.transactionType && params.transactionType !== 'all') searchParams.set('transaction_type', params.transactionType);
|
||||
if (params?.paymentStatus && params.paymentStatus !== 'all') searchParams.set('payment_status', params.paymentStatus);
|
||||
if (params?.approvalStatus && params.approvalStatus !== 'all') searchParams.set('approval_status', params.approvalStatus);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: ExpensePaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformApiToFrontend),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 50, total: data?.total || 0 },
|
||||
}) {
|
||||
return executePaginatedAction<ExpectedExpenseApiData, ExpectedExpenseRecord>({
|
||||
url: buildApiUrl('/api/v1/expected-expenses', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
transaction_type: params?.transactionType && params.transactionType !== 'all' ? params.transactionType : undefined,
|
||||
payment_status: params?.paymentStatus && params.paymentStatus !== 'all' ? params.paymentStatus : undefined,
|
||||
approval_status: params?.approvalStatus && params.approvalStatus !== 'all' ? params.approvalStatus : undefined,
|
||||
client_id: params?.clientId,
|
||||
search: params?.search,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '미지급비용 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 미지급비용 상세 조회 =====
|
||||
export async function getExpectedExpenseById(id: string): Promise<ActionResult<ExpectedExpenseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses/${id}`,
|
||||
url: buildApiUrl(`/api/v1/expected-expenses/${id}`),
|
||||
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '미지급비용 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -121,7 +109,7 @@ export async function getExpectedExpenseById(id: string): Promise<ActionResult<E
|
||||
// ===== 미지급비용 등록 =====
|
||||
export async function createExpectedExpense(data: Partial<ExpectedExpenseRecord>): Promise<ActionResult<ExpectedExpenseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses`,
|
||||
url: buildApiUrl('/api/v1/expected-expenses'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
|
||||
@@ -132,7 +120,7 @@ export async function createExpectedExpense(data: Partial<ExpectedExpenseRecord>
|
||||
// ===== 미지급비용 수정 =====
|
||||
export async function updateExpectedExpense(id: string, data: Partial<ExpectedExpenseRecord>): Promise<ActionResult<ExpectedExpenseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses/${id}`,
|
||||
url: buildApiUrl(`/api/v1/expected-expenses/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
|
||||
@@ -143,7 +131,7 @@ export async function updateExpectedExpense(id: string, data: Partial<ExpectedEx
|
||||
// ===== 미지급비용 삭제 =====
|
||||
export async function deleteExpectedExpense(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses/${id}`,
|
||||
url: buildApiUrl(`/api/v1/expected-expenses/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '미지급비용 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -154,7 +142,7 @@ export async function deleteExpectedExpenses(ids: string[]): Promise<{
|
||||
success: boolean; deletedCount?: number; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses`,
|
||||
url: buildApiUrl('/api/v1/expected-expenses'),
|
||||
method: 'DELETE',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
transform: (data: { deleted_count?: number }) => ({ deletedCount: data?.deleted_count }),
|
||||
@@ -168,7 +156,7 @@ export async function updateExpectedPaymentDate(ids: string[], expectedPaymentDa
|
||||
success: boolean; updatedCount?: number; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses/update-payment-date`,
|
||||
url: buildApiUrl('/api/v1/expected-expenses/update-payment-date'),
|
||||
method: 'PUT',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)), expected_payment_date: expectedPaymentDate },
|
||||
transform: (data: { updated_count?: number }) => ({ updatedCount: data?.updated_count }),
|
||||
@@ -181,14 +169,12 @@ export async function updateExpectedPaymentDate(ids: string[], expectedPaymentDa
|
||||
export async function getExpectedExpenseSummary(params?: {
|
||||
startDate?: string; endDate?: string; paymentStatus?: string;
|
||||
}): Promise<ActionResult<SummaryData>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.paymentStatus && params.paymentStatus !== 'all') searchParams.set('payment_status', params.paymentStatus);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return executeServerAction<SummaryData>({
|
||||
url: `${API_URL}/api/v1/expected-expenses/summary${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/expected-expenses/summary', {
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
payment_status: params?.paymentStatus && params.paymentStatus !== 'all' ? params.paymentStatus : undefined,
|
||||
}),
|
||||
errorMessage: '요약 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -198,7 +184,7 @@ export async function getClients(): Promise<{
|
||||
success: boolean; data: { id: string; name: string }[]; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients?per_page=100`,
|
||||
url: buildApiUrl('/api/v1/clients', { per_page: 100 }),
|
||||
transform: (data: { data?: { id: number; name: string }[] } | { id: number; name: string }[]) => {
|
||||
type ClientApi = { id: number; name: string };
|
||||
const clients: ClientApi[] = Array.isArray(data) ? data : (data as { data?: ClientApi[] })?.data || [];
|
||||
@@ -215,4 +201,4 @@ export async function getBankAccounts(): Promise<{
|
||||
}> {
|
||||
const result = await fetchBankAccountDetailOptions();
|
||||
return { success: result.success, data: result.data || [], error: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,11 @@
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { fetchVendorOptions, fetchBankAccountDetailOptions } from '@/lib/api/shared-lookups';
|
||||
import type { PurchaseRecord, PurchaseType } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 데이터 타입 =====
|
||||
interface PurchaseApiData {
|
||||
id: number;
|
||||
@@ -48,8 +47,6 @@ interface PurchaseApiData {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
type PurchaseApiPaginatedResponse = PaginatedApiResponse<PurchaseApiData>;
|
||||
|
||||
// ===== 변환 함수 =====
|
||||
|
||||
const VALID_PURCHASE_TYPES: PurchaseType[] = [
|
||||
@@ -96,39 +93,30 @@ function transformFrontendToApi(data: Partial<PurchaseRecord>): Record<string, u
|
||||
return result;
|
||||
}
|
||||
|
||||
interface PaginationMeta { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: PaginationMeta = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== 매입 목록 조회 =====
|
||||
export async function getPurchases(params?: {
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
clientId?: string; status?: string; search?: string;
|
||||
}): Promise<{ success: boolean; data: PurchaseRecord[]; pagination: PaginationMeta; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/purchases${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: PurchaseApiPaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformApiToFrontend),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
||||
}) {
|
||||
return executePaginatedAction<PurchaseApiData, PurchaseRecord>({
|
||||
url: buildApiUrl('/api/v1/purchases', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
client_id: params?.clientId,
|
||||
status: params?.status !== 'all' ? params?.status : undefined,
|
||||
search: params?.search,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '매입 목록 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 매입 상세 조회 =====
|
||||
export async function getPurchaseById(id: string): Promise<ActionResult<PurchaseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/purchases/${id}`,
|
||||
url: buildApiUrl(`/api/v1/purchases/${id}`),
|
||||
transform: (data: PurchaseApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '매입 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -137,7 +125,7 @@ export async function getPurchaseById(id: string): Promise<ActionResult<Purchase
|
||||
// ===== 매입 등록 =====
|
||||
export async function createPurchase(data: Partial<PurchaseRecord>): Promise<ActionResult<PurchaseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/purchases`,
|
||||
url: buildApiUrl('/api/v1/purchases'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: PurchaseApiData) => transformApiToFrontend(data),
|
||||
@@ -148,7 +136,7 @@ export async function createPurchase(data: Partial<PurchaseRecord>): Promise<Act
|
||||
// ===== 매입 수정 =====
|
||||
export async function updatePurchase(id: string, data: Partial<PurchaseRecord>): Promise<ActionResult<PurchaseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/purchases/${id}`,
|
||||
url: buildApiUrl(`/api/v1/purchases/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: PurchaseApiData) => transformApiToFrontend(data),
|
||||
@@ -173,7 +161,7 @@ export async function togglePurchaseTaxInvoice(
|
||||
// ===== 매입 삭제 =====
|
||||
export async function deletePurchase(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/purchases/${id}`,
|
||||
url: buildApiUrl(`/api/v1/purchases/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '매입 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -182,7 +170,7 @@ export async function deletePurchase(id: string): Promise<ActionResult> {
|
||||
// ===== 매입 확정 =====
|
||||
export async function confirmPurchase(id: string): Promise<ActionResult<PurchaseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/purchases/${id}/confirm`,
|
||||
url: buildApiUrl(`/api/v1/purchases/${id}/confirm`),
|
||||
method: 'PUT',
|
||||
transform: (data: PurchaseApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '매입 확정에 실패했습니다.',
|
||||
@@ -207,4 +195,4 @@ export async function getVendors(): Promise<{
|
||||
}> {
|
||||
const result = await fetchVendorOptions();
|
||||
return { success: result.success, data: result.data || [], error: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import type { VendorReceivables, CategoryType, MonthlyAmount, ReceivablesListResponse, MemoUpdateRequest } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface CategoryAmountApi {
|
||||
category: CategoryType;
|
||||
@@ -56,27 +55,19 @@ function transformItem(item: VendorReceivablesApi): VendorReceivables {
|
||||
};
|
||||
}
|
||||
|
||||
// ===== year 파라미터 헬퍼 =====
|
||||
function applyYearParam(searchParams: URLSearchParams, year?: number) {
|
||||
if (typeof year === 'number') {
|
||||
if (year === 0) searchParams.set('recent_year', 'true');
|
||||
else searchParams.set('year', String(year));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 채권 현황 목록 조회 =====
|
||||
export async function getReceivablesList(params?: {
|
||||
year?: number; search?: string; hasReceivable?: boolean;
|
||||
}): Promise<{ success: boolean; data: ReceivablesListResponse; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
applyYearParam(searchParams, params?.year);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.hasReceivable !== undefined) searchParams.set('has_receivable', params.hasReceivable ? 'true' : 'false');
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const yearValue = params?.year;
|
||||
const DEFAULT_DATA: ReceivablesListResponse = { monthLabels: [], items: [] };
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivables${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/receivables', {
|
||||
recent_year: typeof yearValue === 'number' && yearValue === 0 ? 'true' : undefined,
|
||||
year: typeof yearValue === 'number' && yearValue !== 0 ? yearValue : undefined,
|
||||
search: params?.search,
|
||||
has_receivable: params?.hasReceivable !== undefined ? String(params.hasReceivable) : undefined,
|
||||
}),
|
||||
transform: (data: ReceivablesListApiResponse) => ({
|
||||
monthLabels: data.month_labels || [],
|
||||
items: (data.items || []).map(transformItem),
|
||||
@@ -93,12 +84,12 @@ export async function getReceivablesSummary(params?: {
|
||||
totalCarryForward: number; totalSales: number; totalDeposits: number; totalBills: number;
|
||||
totalReceivables: number; vendorCount: number; overdueVendorCount: number;
|
||||
}>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
applyYearParam(searchParams, params?.year);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const yearValue = params?.year;
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivables/summary${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/receivables/summary', {
|
||||
recent_year: typeof yearValue === 'number' && yearValue === 0 ? 'true' : undefined,
|
||||
year: typeof yearValue === 'number' && yearValue !== 0 ? yearValue : undefined,
|
||||
}),
|
||||
transform: (data: ReceivablesSummaryApi) => ({
|
||||
totalCarryForward: data.total_carry_forward,
|
||||
totalSales: data.total_sales,
|
||||
@@ -117,7 +108,7 @@ export async function updateOverdueStatus(
|
||||
updates: Array<{ id: string; isOverdue: boolean }>
|
||||
): Promise<{ success: boolean; updatedCount?: number; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivables/overdue-status`,
|
||||
url: buildApiUrl('/api/v1/receivables/overdue-status'),
|
||||
method: 'PUT',
|
||||
body: { updates: updates.map(item => ({ id: parseInt(item.id, 10), is_overdue: item.isOverdue })) },
|
||||
transform: (data: { updated_count?: number }) => ({ updatedCount: data?.updated_count || updates.length }),
|
||||
@@ -131,7 +122,7 @@ export async function updateMemos(
|
||||
memos: MemoUpdateRequest[]
|
||||
): Promise<{ success: boolean; updatedCount?: number; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivables/memos`,
|
||||
url: buildApiUrl('/api/v1/receivables/memos'),
|
||||
method: 'PUT',
|
||||
body: { memos: memos.map(item => ({ id: parseInt(item.id, 10), memo: item.memo })) },
|
||||
transform: (data: { updated_count?: number }) => ({ updatedCount: data?.updated_count || memos.length }),
|
||||
@@ -160,20 +151,13 @@ export async function exportReceivablesExcel(params?: {
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
// year=0은 "최근 1년" 옵션 - recent_year 파라미터 사용
|
||||
const yearValue = params?.year;
|
||||
if (typeof yearValue === 'number') {
|
||||
if (yearValue === 0) {
|
||||
searchParams.set('recent_year', 'true');
|
||||
} else {
|
||||
searchParams.set('year', String(yearValue));
|
||||
}
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivables/export${queryString ? `?${queryString}` : ''}`;
|
||||
const url = buildApiUrl('/api/v1/receivables/export', {
|
||||
recent_year: typeof yearValue === 'number' && yearValue === 0 ? 'true' : undefined,
|
||||
year: typeof yearValue === 'number' && yearValue !== 0 ? yearValue : undefined,
|
||||
search: params?.search,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -16,44 +16,35 @@
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { SalesRecord, SaleApiData, SaleApiPaginatedResponse } from './types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { SalesRecord, SaleApiData } from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
interface PaginationMeta { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: PaginationMeta = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== 매출 목록 조회 =====
|
||||
export async function getSales(params?: {
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
clientId?: string; status?: string; search?: string;
|
||||
}): Promise<{ success: boolean; data: SalesRecord[]; pagination: PaginationMeta; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/sales${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: SaleApiPaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformApiToFrontend),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
||||
}) {
|
||||
return executePaginatedAction<SaleApiData, SalesRecord>({
|
||||
url: buildApiUrl('/api/v1/sales', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
client_id: params?.clientId,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
search: params?.search,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '매출 목록 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 매출 상세 조회 =====
|
||||
export async function getSaleById(id: string): Promise<ActionResult<SalesRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/sales/${id}`,
|
||||
url: buildApiUrl(`/api/v1/sales/${id}`),
|
||||
transform: (data: SaleApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '매출 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -62,7 +53,7 @@ export async function getSaleById(id: string): Promise<ActionResult<SalesRecord>
|
||||
// ===== 매출 등록 =====
|
||||
export async function createSale(data: Partial<SalesRecord>): Promise<ActionResult<SalesRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/sales`,
|
||||
url: buildApiUrl('/api/v1/sales'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: SaleApiData) => transformApiToFrontend(data),
|
||||
@@ -73,7 +64,7 @@ export async function createSale(data: Partial<SalesRecord>): Promise<ActionResu
|
||||
// ===== 매출 수정 =====
|
||||
export async function updateSale(id: string, data: Partial<SalesRecord>): Promise<ActionResult<SalesRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/sales/${id}`,
|
||||
url: buildApiUrl(`/api/v1/sales/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: SaleApiData) => transformApiToFrontend(data),
|
||||
@@ -100,7 +91,7 @@ export async function toggleSaleIssuance(
|
||||
// ===== 매출 삭제 =====
|
||||
export async function deleteSale(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/sales/${id}`,
|
||||
url: buildApiUrl(`/api/v1/sales/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '매출 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -109,7 +100,7 @@ export async function deleteSale(id: string): Promise<ActionResult> {
|
||||
// ===== 매출 확정 =====
|
||||
export async function confirmSale(id: string): Promise<ActionResult<SalesRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/sales/${id}/confirm`,
|
||||
url: buildApiUrl(`/api/v1/sales/${id}/confirm`),
|
||||
method: 'PUT',
|
||||
transform: (data: SaleApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '매출 확정에 실패했습니다.',
|
||||
@@ -124,13 +115,11 @@ export async function getSalesSummary(params?: {
|
||||
}): Promise<ActionResult<{
|
||||
totalAmount: number; totalCount: number; confirmedAmount: number; confirmedCount: number; draftAmount: number; draftCount: number;
|
||||
}>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/sales/summary${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/sales/summary', {
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
}),
|
||||
transform: (data: SalesSummaryApi) => ({
|
||||
totalAmount: parseFloat(data.total_amount) || 0, totalCount: data.total_count || 0,
|
||||
confirmedAmount: parseFloat(data.confirmed_amount) || 0, confirmedCount: data.confirmed_count || 0,
|
||||
@@ -145,11 +134,11 @@ export async function bulkUpdateSalesAccountCode(
|
||||
ids: number[], accountCode: string
|
||||
): Promise<{ success: boolean; updatedCount?: number; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/sales/bulk-update-account`,
|
||||
url: buildApiUrl('/api/v1/sales/bulk-update-account'),
|
||||
method: 'PUT',
|
||||
body: { ids, account_code: accountCode },
|
||||
transform: (data: { updated_count?: number }) => ({ updatedCount: data?.updated_count || 0 }),
|
||||
errorMessage: '계정과목 수정에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, updatedCount: result.data?.updatedCount, error: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import type { VendorLedgerItem, VendorLedgerDetail, VendorLedgerSummary, TransactionEntry } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface VendorLedgerApiItem {
|
||||
id: number;
|
||||
@@ -65,11 +64,6 @@ interface VendorLedgerApiDetail {
|
||||
transactions: VendorLedgerApiTransaction[];
|
||||
}
|
||||
|
||||
type VendorLedgerPaginatedResponse = PaginatedApiResponse<VendorLedgerApiItem>;
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== API → Frontend 변환 =====
|
||||
function transformListItem(item: VendorLedgerApiItem): VendorLedgerItem {
|
||||
const carryoverSales = typeof item.carryover_sales === 'string' ? parseFloat(item.carryover_sales) : item.carryover_sales;
|
||||
@@ -132,39 +126,31 @@ function transformDetail(data: VendorLedgerApiDetail): VendorLedgerDetail {
|
||||
export async function getVendorLedgerList(params?: {
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
search?: string; sortBy?: string; sortDir?: 'asc' | 'desc';
|
||||
}): Promise<{ success: boolean; data: VendorLedgerItem[]; pagination: FrontendPagination; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/vendor-ledger${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: VendorLedgerPaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformListItem),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
||||
}) {
|
||||
return executePaginatedAction<VendorLedgerApiItem, VendorLedgerItem>({
|
||||
url: buildApiUrl('/api/v1/vendor-ledger', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
search: params?.search,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
}),
|
||||
transform: transformListItem,
|
||||
errorMessage: '거래처원장 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 거래처원장 요약 통계 조회 =====
|
||||
export async function getVendorLedgerSummary(params?: {
|
||||
startDate?: string; endDate?: string;
|
||||
}): Promise<ActionResult<VendorLedgerSummary>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/vendor-ledger/summary${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/vendor-ledger/summary', {
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
}),
|
||||
transform: (data: VendorLedgerApiSummary) => ({
|
||||
carryoverBalance: data.carryover_balance,
|
||||
totalSales: data.total_sales,
|
||||
@@ -179,13 +165,11 @@ export async function getVendorLedgerSummary(params?: {
|
||||
export async function getVendorLedgerDetail(clientId: string, params?: {
|
||||
startDate?: string; endDate?: string;
|
||||
}): Promise<{ success: boolean; data?: VendorLedgerDetail; summary?: VendorLedgerSummary; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/vendor-ledger/${clientId}${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl(`/api/v1/vendor-ledger/${clientId}`, {
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
}),
|
||||
transform: (data: VendorLedgerApiDetail) => ({
|
||||
detail: transformDetail(data),
|
||||
summary: {
|
||||
@@ -221,13 +205,11 @@ export async function exportVendorLedgerExcel(params?: {
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/vendor-ledger/export${queryString ? `?${queryString}` : ''}`;
|
||||
const url = buildApiUrl('/api/v1/vendor-ledger/export', {
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
search: params?.search,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -281,12 +263,10 @@ export async function exportVendorLedgerDetailPdf(clientId: string, params?: {
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/vendor-ledger/${clientId}/export-pdf${queryString ? `?${queryString}` : ''}`;
|
||||
const url = buildApiUrl(`/api/v1/vendor-ledger/${clientId}/export-pdf`, {
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
Vendor,
|
||||
ClientApiData,
|
||||
@@ -22,8 +23,6 @@ import type {
|
||||
BadDebtStatus,
|
||||
} from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* API 데이터 → 프론트엔드 타입 변환
|
||||
*/
|
||||
@@ -132,14 +131,13 @@ export async function getClients(params?: {
|
||||
q?: string;
|
||||
only_active?: boolean;
|
||||
}): Promise<{ success: boolean; data: Vendor[]; total: number; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.only_active !== undefined) searchParams.set('only_active', String(params.only_active));
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/clients', {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
q: params?.q,
|
||||
only_active: params?.only_active,
|
||||
}),
|
||||
transform: (data: PaginatedResponse<ClientApiData>) => ({
|
||||
items: data.data.map(transformApiToFrontend),
|
||||
total: data.total,
|
||||
@@ -157,7 +155,7 @@ export async function getClients(params?: {
|
||||
// ===== 거래처 상세 조회 =====
|
||||
export async function getClientById(id: string): Promise<Vendor | null> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients/${id}`,
|
||||
url: buildApiUrl(`/api/v1/clients/${id}`),
|
||||
transform: (data: ClientApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '거래처 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -169,7 +167,7 @@ export async function createClient(
|
||||
data: Partial<Vendor>
|
||||
): Promise<ActionResult<Vendor>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients`,
|
||||
url: buildApiUrl('/api/v1/clients'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: ClientApiData) => transformApiToFrontend(data),
|
||||
@@ -183,7 +181,7 @@ export async function updateClient(
|
||||
data: Partial<Vendor>
|
||||
): Promise<ActionResult<Vendor>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients/${id}`,
|
||||
url: buildApiUrl(`/api/v1/clients/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: ClientApiData) => transformApiToFrontend(data),
|
||||
@@ -194,7 +192,7 @@ export async function updateClient(
|
||||
// ===== 거래처 삭제 =====
|
||||
export async function deleteClient(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients/${id}`,
|
||||
url: buildApiUrl(`/api/v1/clients/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '거래처 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -203,7 +201,7 @@ export async function deleteClient(id: string): Promise<ActionResult> {
|
||||
// ===== 거래처 활성/비활성 토글 =====
|
||||
export async function toggleClientActive(id: string): Promise<ActionResult<Vendor>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients/${id}/toggle`,
|
||||
url: buildApiUrl(`/api/v1/clients/${id}/toggle`),
|
||||
method: 'PATCH',
|
||||
transform: (data: ClientApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '상태 변경에 실패했습니다.',
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { fetchVendorOptions, fetchBankAccountOptions } from '@/lib/api/shared-lookups';
|
||||
import type { WithdrawalRecord, WithdrawalType } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface WithdrawalApiData {
|
||||
id: number;
|
||||
@@ -32,11 +31,6 @@ interface WithdrawalApiData {
|
||||
card?: { id: number; card_name: string } | null;
|
||||
}
|
||||
|
||||
type WithdrawalPaginatedResponse = PaginatedApiResponse<WithdrawalApiData>;
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== API → Frontend 변환 =====
|
||||
function transformApiToFrontend(apiData: WithdrawalApiData): WithdrawalRecord {
|
||||
return {
|
||||
@@ -77,40 +71,26 @@ function transformFrontendToApi(data: Partial<WithdrawalRecord>): Record<string,
|
||||
export async function getWithdrawals(params?: {
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
withdrawalType?: string; vendor?: string; search?: string;
|
||||
}): Promise<{ success: boolean; data: WithdrawalRecord[]; pagination: FrontendPagination; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.withdrawalType && params.withdrawalType !== 'all') searchParams.set('withdrawal_type', params.withdrawalType);
|
||||
if (params?.vendor && params.vendor !== 'all') searchParams.set('vendor', params.vendor);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/withdrawals${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: WithdrawalPaginatedResponse | WithdrawalApiData[]) => {
|
||||
const isPaginated = !Array.isArray(data) && data && 'data' in data;
|
||||
const rawData = isPaginated ? (data as WithdrawalPaginatedResponse).data : (Array.isArray(data) ? data : []);
|
||||
const items = rawData.map(transformApiToFrontend);
|
||||
const meta = isPaginated
|
||||
? (data as WithdrawalPaginatedResponse)
|
||||
: { current_page: 1, last_page: 1, per_page: 20, total: items.length };
|
||||
return {
|
||||
items,
|
||||
pagination: { currentPage: meta.current_page || 1, lastPage: meta.last_page || 1, perPage: meta.per_page || 20, total: meta.total || items.length },
|
||||
};
|
||||
},
|
||||
}) {
|
||||
return executePaginatedAction<WithdrawalApiData, WithdrawalRecord>({
|
||||
url: buildApiUrl('/api/v1/withdrawals', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
withdrawal_type: params?.withdrawalType !== 'all' ? params?.withdrawalType : undefined,
|
||||
vendor: params?.vendor !== 'all' ? params?.vendor : undefined,
|
||||
search: params?.search,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '출금 내역 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 출금 내역 삭제 =====
|
||||
export async function deleteWithdrawal(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/withdrawals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/withdrawals/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '출금 내역 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -119,7 +99,7 @@ export async function deleteWithdrawal(id: string): Promise<ActionResult> {
|
||||
// ===== 계정과목명 일괄 저장 =====
|
||||
export async function updateWithdrawalTypes(ids: string[], withdrawalType: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/withdrawals/bulk-update-type`,
|
||||
url: buildApiUrl('/api/v1/withdrawals/bulk-update-type'),
|
||||
method: 'PUT',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)), withdrawal_type: withdrawalType },
|
||||
errorMessage: '계정과목명 저장에 실패했습니다.',
|
||||
@@ -129,7 +109,7 @@ export async function updateWithdrawalTypes(ids: string[], withdrawalType: strin
|
||||
// ===== 출금 상세 조회 =====
|
||||
export async function getWithdrawalById(id: string): Promise<ActionResult<WithdrawalRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/withdrawals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/withdrawals/${id}`),
|
||||
transform: (data: WithdrawalApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '출금 내역 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -138,7 +118,7 @@ export async function getWithdrawalById(id: string): Promise<ActionResult<Withdr
|
||||
// ===== 출금 등록 =====
|
||||
export async function createWithdrawal(data: Partial<WithdrawalRecord>): Promise<ActionResult<WithdrawalRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/withdrawals`,
|
||||
url: buildApiUrl('/api/v1/withdrawals'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: WithdrawalApiData) => transformApiToFrontend(data),
|
||||
@@ -149,7 +129,7 @@ export async function createWithdrawal(data: Partial<WithdrawalRecord>): Promise
|
||||
// ===== 출금 수정 =====
|
||||
export async function updateWithdrawal(id: string, data: Partial<WithdrawalRecord>): Promise<ActionResult<WithdrawalRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/withdrawals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/withdrawals/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: WithdrawalApiData) => transformApiToFrontend(data),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { ApprovalRecord, ApprovalType, ApprovalStatus } from './types';
|
||||
|
||||
@@ -105,8 +106,6 @@ function transformApiToFrontend(data: InboxApiData): ApprovalRecord {
|
||||
};
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
@@ -115,20 +114,16 @@ export async function getInbox(params?: {
|
||||
page?: number; per_page?: number; search?: string; status?: string;
|
||||
approval_type?: string; sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: ApprovalRecord[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
const apiStatus = mapTabToApiStatus(params.status);
|
||||
if (apiStatus) searchParams.set('status', apiStatus);
|
||||
}
|
||||
if (params?.approval_type && params.approval_type !== 'all') searchParams.set('approval_type', params.approval_type);
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<InboxApiData>>({
|
||||
url: `${API_URL}/api/v1/approvals/inbox?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/approvals/inbox', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
search: params?.search,
|
||||
status: params?.status && params.status !== 'all' ? mapTabToApiStatus(params.status) : undefined,
|
||||
approval_type: params?.approval_type !== 'all' ? params?.approval_type : undefined,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '결재함 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -144,7 +139,7 @@ export async function getInbox(params?: {
|
||||
|
||||
export async function getInboxSummary(): Promise<InboxSummary | null> {
|
||||
const result = await executeServerAction<InboxSummary>({
|
||||
url: `${API_URL}/api/v1/approvals/inbox/summary`,
|
||||
url: buildApiUrl('/api/v1/approvals/inbox/summary'),
|
||||
errorMessage: '결재함 통계 조회에 실패했습니다.',
|
||||
});
|
||||
return result.success ? result.data || null : null;
|
||||
@@ -152,7 +147,7 @@ export async function getInboxSummary(): Promise<InboxSummary | null> {
|
||||
|
||||
export async function approveDocument(id: string, comment?: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/approve`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/approve`),
|
||||
method: 'POST',
|
||||
body: { comment: comment || '' },
|
||||
errorMessage: '승인 처리에 실패했습니다.',
|
||||
@@ -162,7 +157,7 @@ export async function approveDocument(id: string, comment?: string): Promise<Act
|
||||
export async function rejectDocument(id: string, comment: string): Promise<ActionResult> {
|
||||
if (!comment?.trim()) return { success: false, error: '반려 사유를 입력해주세요.' };
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/reject`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/reject`),
|
||||
method: 'POST',
|
||||
body: { comment },
|
||||
errorMessage: '반려 처리에 실패했습니다.',
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
ExpenseEstimateItem,
|
||||
ApprovalPerson,
|
||||
@@ -125,8 +126,6 @@ function transformEmployee(employee: EmployeeApiData): ApprovalPerson {
|
||||
// API 함수
|
||||
// ============================================
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* 파일 업로드
|
||||
* @param files 업로드할 파일 배열
|
||||
@@ -202,11 +201,8 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
|
||||
accountBalance: number;
|
||||
finalDifference: number;
|
||||
} | null> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (yearMonth) searchParams.set('year_month', yearMonth);
|
||||
|
||||
const result = await executeServerAction<ExpenseEstimateApiResponse>({
|
||||
url: `${API_URL}/api/v1/reports/expense-estimate?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/reports/expense-estimate', { year_month: yearMonth }),
|
||||
errorMessage: '비용견적서 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return null;
|
||||
@@ -222,12 +218,8 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
|
||||
* 직원 목록 조회 (결재선/참조 선택용)
|
||||
*/
|
||||
export async function getEmployees(search?: string): Promise<ApprovalPerson[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('per_page', '100');
|
||||
if (search) searchParams.set('search', search);
|
||||
|
||||
const result = await executeServerAction<{ data: EmployeeApiData[] }>({
|
||||
url: `${API_URL}/api/v1/employees?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/employees', { per_page: 100, search }),
|
||||
errorMessage: '직원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return [];
|
||||
@@ -276,7 +268,7 @@ export async function createApproval(formData: DocumentFormData): Promise<{
|
||||
};
|
||||
|
||||
const result = await executeServerAction<ApprovalCreateResponse>({
|
||||
url: `${API_URL}/api/v1/approvals`,
|
||||
url: buildApiUrl('/api/v1/approvals'),
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
errorMessage: '문서 저장에 실패했습니다.',
|
||||
@@ -293,7 +285,7 @@ export async function submitApproval(id: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/submit`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/submit`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '문서 상신에 실패했습니다.',
|
||||
@@ -349,7 +341,7 @@ export async function getApprovalById(id: number): Promise<{
|
||||
}> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await executeServerAction<any>({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
errorMessage: '문서 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -397,7 +389,7 @@ export async function updateApproval(id: number, formData: DocumentFormData): Pr
|
||||
};
|
||||
|
||||
const result = await executeServerAction<ApprovalCreateResponse>({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
method: 'PATCH',
|
||||
body: requestBody,
|
||||
errorMessage: '문서 수정에 실패했습니다.',
|
||||
@@ -449,7 +441,7 @@ export async function deleteApproval(id: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '문서 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { DraftRecord, DocumentStatus, Approver } from './types';
|
||||
|
||||
@@ -152,7 +153,10 @@ function transformApiToFrontend(data: ApprovalApiData): DraftRecord {
|
||||
};
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
const DRAFT_STATUS_MAP: Record<string, string> = {
|
||||
'draft': 'draft', 'pending': 'pending', 'inProgress': 'in_progress',
|
||||
'approved': 'approved', 'rejected': 'rejected',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
@@ -162,22 +166,15 @@ export async function getDrafts(params?: {
|
||||
page?: number; per_page?: number; search?: string; status?: string;
|
||||
sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: DraftRecord[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
const statusMap: Record<string, string> = {
|
||||
'draft': 'draft', 'pending': 'pending', 'inProgress': 'in_progress',
|
||||
'approved': 'approved', 'rejected': 'rejected',
|
||||
};
|
||||
searchParams.set('status', statusMap[params.status] || params.status);
|
||||
}
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<ApprovalApiData>>({
|
||||
url: `${API_URL}/api/v1/approvals/drafts?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/approvals/drafts', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
search: params?.search,
|
||||
status: params?.status && params.status !== 'all' ? (DRAFT_STATUS_MAP[params.status] || params.status) : undefined,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '기안함 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -193,7 +190,7 @@ export async function getDrafts(params?: {
|
||||
|
||||
export async function getDraftsSummary(): Promise<DraftsSummary | null> {
|
||||
const result = await executeServerAction<DraftsSummary>({
|
||||
url: `${API_URL}/api/v1/approvals/drafts/summary`,
|
||||
url: buildApiUrl('/api/v1/approvals/drafts/summary'),
|
||||
errorMessage: '기안함 현황 조회에 실패했습니다.',
|
||||
});
|
||||
return result.success ? result.data || null : null;
|
||||
@@ -201,7 +198,7 @@ export async function getDraftsSummary(): Promise<DraftsSummary | null> {
|
||||
|
||||
export async function getDraftById(id: string): Promise<DraftRecord | null> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
transform: (data: ApprovalApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '결재 문서 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -210,7 +207,7 @@ export async function getDraftById(id: string): Promise<DraftRecord | null> {
|
||||
|
||||
export async function deleteDraft(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '결재 문서 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -230,7 +227,7 @@ export async function deleteDrafts(ids: string[]): Promise<{ success: boolean; f
|
||||
|
||||
export async function submitDraft(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/submit`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/submit`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '결재 상신에 실패했습니다.',
|
||||
@@ -251,7 +248,7 @@ export async function submitDrafts(ids: string[]): Promise<{ success: boolean; f
|
||||
|
||||
export async function cancelDraft(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/cancel`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/cancel`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '결재 회수에 실패했습니다.',
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { ReferenceRecord, ApprovalType, DocumentStatus } from './types';
|
||||
@@ -82,8 +83,6 @@ function transformApiToFrontend(data: ReferenceApiData): ReferenceRecord {
|
||||
};
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
@@ -92,17 +91,16 @@ export async function getReferences(params?: {
|
||||
page?: number; per_page?: number; search?: string; is_read?: boolean;
|
||||
approval_type?: string; sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: ReferenceRecord[]; total: number; lastPage: number }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.is_read !== undefined) searchParams.set('is_read', params.is_read ? '1' : '0');
|
||||
if (params?.approval_type && params.approval_type !== 'all') searchParams.set('approval_type', params.approval_type);
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<ReferenceApiData>>({
|
||||
url: `${API_URL}/api/v1/approvals/reference?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/approvals/reference', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
search: params?.search,
|
||||
is_read: params?.is_read !== undefined ? (params.is_read ? '1' : '0') : undefined,
|
||||
approval_type: params?.approval_type !== 'all' ? params?.approval_type : undefined,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '참조 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -131,7 +129,7 @@ export async function getReferenceSummary(): Promise<{ all: number; read: number
|
||||
|
||||
export async function markAsRead(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/read`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/read`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '열람 처리에 실패했습니다.',
|
||||
@@ -140,7 +138,7 @@ export async function markAsRead(id: string): Promise<ActionResult> {
|
||||
|
||||
export async function markAsUnread(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/unread`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/unread`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '미열람 처리에 실패했습니다.',
|
||||
@@ -169,4 +167,4 @@ export async function markAsUnreadBulk(ids: string[]): Promise<{ success: boolea
|
||||
return { success: false, failedIds, error: `${failedIds.length}건의 미열람 처리에 실패했습니다.` };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import type { Board, BoardApiData, BoardFormData } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 변환 =====
|
||||
function transformApiToFrontend(apiData: BoardApiData): Board {
|
||||
const extraSettings = apiData.extra_settings || {};
|
||||
@@ -50,12 +49,11 @@ function transformFrontendToApi(data: BoardFormData & { boardCode?: string; desc
|
||||
export async function getBoards(filters?: {
|
||||
board_type?: string; search?: string;
|
||||
}): Promise<ActionResult<Board[]>> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.board_type) params.append('board_type', filters.board_type);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
const queryString = params.toString();
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/boards/tenant', {
|
||||
board_type: filters?.board_type,
|
||||
search: filters?.search,
|
||||
}),
|
||||
transform: (data: BoardApiData[]) => (Array.isArray(data) ? data : []).map(transformApiToFrontend),
|
||||
errorMessage: '게시판 목록 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -65,12 +63,11 @@ export async function getBoards(filters?: {
|
||||
export async function getTenantBoards(filters?: {
|
||||
board_type?: string; search?: string;
|
||||
}): Promise<ActionResult<Board[]>> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.board_type) params.append('board_type', filters.board_type);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
const queryString = params.toString();
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/boards/tenant', {
|
||||
board_type: filters?.board_type,
|
||||
search: filters?.search,
|
||||
}),
|
||||
transform: (data: BoardApiData[]) => data.map(transformApiToFrontend),
|
||||
errorMessage: '테넌트 게시판 목록 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -79,7 +76,7 @@ export async function getTenantBoards(filters?: {
|
||||
// ===== 게시판 상세 조회 (코드 기반) =====
|
||||
export async function getBoardByCode(code: string): Promise<ActionResult<Board>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${code}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${code}`),
|
||||
transform: (data: BoardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '게시판을 찾을 수 없습니다.',
|
||||
});
|
||||
@@ -88,7 +85,7 @@ export async function getBoardByCode(code: string): Promise<ActionResult<Board>>
|
||||
// ===== 게시판 상세 조회 (ID 기반) =====
|
||||
export async function getBoardById(id: string): Promise<ActionResult<Board>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${id}`),
|
||||
transform: (data: BoardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '게시판을 찾을 수 없습니다.',
|
||||
});
|
||||
@@ -99,7 +96,7 @@ export async function createBoard(
|
||||
data: BoardFormData & { boardCode: string; description?: string }
|
||||
): Promise<ActionResult<Board>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards`,
|
||||
url: buildApiUrl('/api/v1/boards'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: BoardApiData) => transformApiToFrontend(d),
|
||||
@@ -113,7 +110,7 @@ export async function updateBoard(
|
||||
data: BoardFormData & { boardCode?: string; description?: string }
|
||||
): Promise<ActionResult<Board>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data, true),
|
||||
transform: (d: BoardApiData) => transformApiToFrontend(d),
|
||||
@@ -124,7 +121,7 @@ export async function updateBoard(
|
||||
// ===== 게시판 삭제 =====
|
||||
export async function deleteBoard(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '게시판 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -143,4 +140,4 @@ export async function deleteBoardsBulk(ids: string[]): Promise<ActionResult> {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
PostApiData,
|
||||
PostPaginationResponse,
|
||||
@@ -10,22 +11,19 @@ import type {
|
||||
CommentsApiResponse,
|
||||
} from '@/components/customer-center/shared/types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 게시글 API =====
|
||||
|
||||
export async function getDynamicBoardPosts(
|
||||
boardCode: string, filters?: PostFilters
|
||||
): Promise<ActionResult<PostPaginationResponse>> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
const queryString = params.toString();
|
||||
return executeServerAction<PostPaginationResponse>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts`, {
|
||||
search: filters?.search,
|
||||
is_notice: filters?.is_notice,
|
||||
status: filters?.status,
|
||||
per_page: filters?.per_page,
|
||||
page: filters?.page,
|
||||
}),
|
||||
errorMessage: '게시글 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -34,7 +32,7 @@ export async function getDynamicBoardPost(
|
||||
boardCode: string, postId: number | string
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}`),
|
||||
errorMessage: '게시글을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
@@ -44,7 +42,7 @@ export async function createDynamicBoardPost(
|
||||
data: { title: string; content: string; is_secret?: boolean; is_notice?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts`),
|
||||
method: 'POST',
|
||||
body: data,
|
||||
errorMessage: '게시글 등록에 실패했습니다.',
|
||||
@@ -56,7 +54,7 @@ export async function updateDynamicBoardPost(
|
||||
data: { title?: string; content?: string; is_secret?: boolean; is_notice?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}`),
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
errorMessage: '게시글 수정에 실패했습니다.',
|
||||
@@ -67,7 +65,7 @@ export async function deleteDynamicBoardPost(
|
||||
boardCode: string, postId: number | string
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '게시글 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -79,7 +77,7 @@ export async function getDynamicBoardComments(
|
||||
boardCode: string, postId: number | string
|
||||
): Promise<ActionResult<CommentsApiResponse>> {
|
||||
return executeServerAction<CommentsApiResponse>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}/comments`),
|
||||
errorMessage: '댓글 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -88,7 +86,7 @@ export async function createDynamicBoardComment(
|
||||
boardCode: string, postId: number | string, content: string
|
||||
): Promise<ActionResult<CommentApiData>> {
|
||||
return executeServerAction<CommentApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}/comments`),
|
||||
method: 'POST',
|
||||
body: { content },
|
||||
errorMessage: '댓글 등록에 실패했습니다.',
|
||||
@@ -99,7 +97,7 @@ export async function updateDynamicBoardComment(
|
||||
boardCode: string, postId: number | string, commentId: number | string, content: string
|
||||
): Promise<ActionResult<CommentApiData>> {
|
||||
return executeServerAction<CommentApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`),
|
||||
method: 'PUT',
|
||||
body: { content },
|
||||
errorMessage: '댓글 수정에 실패했습니다.',
|
||||
@@ -110,8 +108,8 @@ export async function deleteDynamicBoardComment(
|
||||
boardCode: string, postId: number | string, commentId: number | string
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '댓글 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
PostApiData,
|
||||
PostPaginationResponse,
|
||||
@@ -9,8 +10,6 @@ import type {
|
||||
Post,
|
||||
} from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 변환 =====
|
||||
function transformApiToPost(apiData: PostApiData, boardName?: string): Post {
|
||||
return {
|
||||
@@ -41,25 +40,20 @@ function transformApiToPost(apiData: PostApiData, boardName?: string): Post {
|
||||
};
|
||||
}
|
||||
|
||||
function buildPostFilterParams(filters?: PostFilters): string {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.board_code) params.append('board_code', filters.board_code);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// ===== 게시글 목록 조회 =====
|
||||
export async function getPosts(
|
||||
boardCode: string,
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string; __authError?: boolean }> {
|
||||
const queryString = buildPostFilterParams(filters);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts`, {
|
||||
search: filters?.search,
|
||||
is_notice: filters?.is_notice,
|
||||
status: filters?.status,
|
||||
per_page: filters?.per_page,
|
||||
page: filters?.page,
|
||||
board_code: filters?.board_code,
|
||||
}),
|
||||
transform: (data: PostPaginationResponse) => ({
|
||||
raw: data,
|
||||
posts: data.data.map(post => transformApiToPost(post)),
|
||||
@@ -73,9 +67,15 @@ export async function getPosts(
|
||||
export async function getMyPosts(
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string; __authError?: boolean }> {
|
||||
const queryString = buildPostFilterParams(filters);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/my-posts${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/my-posts', {
|
||||
search: filters?.search,
|
||||
is_notice: filters?.is_notice,
|
||||
status: filters?.status,
|
||||
per_page: filters?.per_page,
|
||||
page: filters?.page,
|
||||
board_code: filters?.board_code,
|
||||
}),
|
||||
transform: (data: PostPaginationResponse) => ({
|
||||
raw: data,
|
||||
posts: data.data.map(post => transformApiToPost(post)),
|
||||
@@ -88,7 +88,7 @@ export async function getMyPosts(
|
||||
// ===== 게시글 상세 조회 =====
|
||||
export async function getPost(boardCode: string, postId: number | string): Promise<ActionResult<Post>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}`),
|
||||
transform: (data: PostApiData) => transformApiToPost(data),
|
||||
errorMessage: '게시글을 찾을 수 없습니다.',
|
||||
});
|
||||
@@ -100,7 +100,7 @@ export async function createPost(
|
||||
data: { title: string; content: string; is_notice?: boolean; is_secret?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<Post>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts`),
|
||||
method: 'POST',
|
||||
body: data,
|
||||
transform: (d: PostApiData) => transformApiToPost(d),
|
||||
@@ -115,7 +115,7 @@ export async function updatePost(
|
||||
data: { title?: string; content?: string; is_notice?: boolean; is_secret?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<Post>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}`),
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: (d: PostApiData) => transformApiToPost(d),
|
||||
@@ -126,7 +126,7 @@ export async function updatePost(
|
||||
// ===== 게시글 삭제 =====
|
||||
export async function deletePost(boardCode: string, postId: number | string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '게시글 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { toast } from 'sonner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { ContractDetail } from '../types';
|
||||
|
||||
interface ContractDocumentModalProps {
|
||||
@@ -22,9 +23,12 @@ export function ContractDocumentModal({
|
||||
onOpenChange,
|
||||
contract,
|
||||
}: ContractDocumentModalProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// 수정
|
||||
const handleEdit = () => {
|
||||
toast.info('수정 기능은 준비 중입니다.');
|
||||
onOpenChange(false);
|
||||
router.push(`/ko/construction/project/contract/${contract.id}/edit`);
|
||||
};
|
||||
|
||||
// 상신 (전자결재)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
DocumentViewer,
|
||||
DocumentHeader,
|
||||
@@ -7,7 +8,9 @@ import {
|
||||
} from '@/components/document-system';
|
||||
import { toast } from 'sonner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import type { HandoverReportDetail } from '../types';
|
||||
import { deleteHandoverReport } from '../actions';
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
|
||||
// 날짜 포맷팅 (년월)
|
||||
@@ -32,14 +35,17 @@ interface HandoverReportDocumentModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
report: HandoverReportDetail;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function HandoverReportDocumentModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
report,
|
||||
onSuccess,
|
||||
}: HandoverReportDocumentModalProps) {
|
||||
const router = useRouter();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
// 수정
|
||||
const handleEdit = () => {
|
||||
@@ -49,10 +55,23 @@ export function HandoverReportDocumentModal({
|
||||
|
||||
// 삭제
|
||||
const handleDelete = () => {
|
||||
toast.info('삭제 기능은 준비 중입니다.');
|
||||
setShowDeleteDialog(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
const result = await deleteHandoverReport(report.id);
|
||||
if (result.success) {
|
||||
toast.success('인수인계보고서가 삭제되었습니다.');
|
||||
setShowDeleteDialog(false);
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentViewer
|
||||
title="인수인계보고서"
|
||||
subtitle="인수인계보고서 상세"
|
||||
@@ -232,5 +251,13 @@ export function HandoverReportDocumentModal({
|
||||
</table>
|
||||
</div>
|
||||
</DocumentViewer>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
description="이 인수인계보고서를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { DocumentViewer, DocumentHeader } from '@/components/document-system';
|
||||
import { toast } from 'sonner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import type { OrderDetail, OrderDetailItem } from '../types';
|
||||
import { deleteOrder } from '../actions';
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateStr: string | null): string {
|
||||
@@ -38,14 +41,17 @@ interface OrderDocumentModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
order: OrderDetail;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function OrderDocumentModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
order,
|
||||
onSuccess,
|
||||
}: OrderDocumentModalProps) {
|
||||
const router = useRouter();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
// 수정
|
||||
const handleEdit = () => {
|
||||
@@ -55,7 +61,19 @@ export function OrderDocumentModal({
|
||||
|
||||
// 삭제
|
||||
const handleDelete = () => {
|
||||
toast.info('삭제 기능은 준비 중입니다.');
|
||||
setShowDeleteDialog(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
const result = await deleteOrder(order.id);
|
||||
if (result.success) {
|
||||
toast.success('발주서가 삭제되었습니다.');
|
||||
setShowDeleteDialog(false);
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 카테고리별 그룹화
|
||||
@@ -63,6 +81,7 @@ export function OrderDocumentModal({
|
||||
const categories = Array.from(groupedItems.entries());
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentViewer
|
||||
title="발주서"
|
||||
subtitle="발주서 상세"
|
||||
@@ -307,5 +326,13 @@ export function OrderDocumentModal({
|
||||
)}
|
||||
</div>
|
||||
</DocumentViewer>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
description="이 발주서를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
PostApiData,
|
||||
PostPaginationResponse,
|
||||
@@ -16,27 +17,20 @@ import type {
|
||||
CommentsApiResponse,
|
||||
} from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
function buildPostFilterParams(filters?: PostFilters): string {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// ===== 게시글 API =====
|
||||
|
||||
export async function getPosts(
|
||||
boardCode: SystemBoardCode,
|
||||
filters?: PostFilters
|
||||
): Promise<ActionResult<PostPaginationResponse>> {
|
||||
const queryString = buildPostFilterParams(filters);
|
||||
return executeServerAction<PostPaginationResponse>({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts`, {
|
||||
search: filters?.search,
|
||||
is_notice: filters?.is_notice,
|
||||
status: filters?.status,
|
||||
per_page: filters?.per_page,
|
||||
page: filters?.page,
|
||||
}),
|
||||
errorMessage: '게시글 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -46,7 +40,7 @@ export async function getPost(
|
||||
postId: number | string
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts/${postId}`),
|
||||
errorMessage: '게시글을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
@@ -56,7 +50,7 @@ export async function createPost(
|
||||
data: { title: string; content: string; is_secret?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts`),
|
||||
method: 'POST',
|
||||
body: data,
|
||||
errorMessage: '게시글 등록에 실패했습니다.',
|
||||
@@ -69,7 +63,7 @@ export async function updatePost(
|
||||
data: { title?: string; content?: string; is_secret?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts/${postId}`),
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
errorMessage: '게시글 수정에 실패했습니다.',
|
||||
@@ -81,7 +75,7 @@ export async function deletePost(
|
||||
postId: number | string
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts/${postId}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '게시글 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -94,7 +88,7 @@ export async function getComments(
|
||||
postId: number | string
|
||||
): Promise<ActionResult<CommentsApiResponse>> {
|
||||
return executeServerAction<CommentsApiResponse>({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts/${postId}/comments`),
|
||||
errorMessage: '댓글 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -105,7 +99,7 @@ export async function createComment(
|
||||
content: string
|
||||
): Promise<ActionResult<CommentApiData>> {
|
||||
return executeServerAction<CommentApiData>({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts/${postId}/comments`),
|
||||
method: 'POST',
|
||||
body: { content },
|
||||
errorMessage: '댓글 등록에 실패했습니다.',
|
||||
@@ -119,7 +113,7 @@ export async function updateComment(
|
||||
content: string
|
||||
): Promise<ActionResult<CommentApiData>> {
|
||||
return executeServerAction<CommentApiData>({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments/${commentId}`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts/${postId}/comments/${commentId}`),
|
||||
method: 'PUT',
|
||||
body: { content },
|
||||
errorMessage: '댓글 수정에 실패했습니다.',
|
||||
@@ -132,8 +126,8 @@ export async function deleteComment(
|
||||
commentId: number | string
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments/${commentId}`,
|
||||
url: buildApiUrl(`/api/v1/system-boards/${boardCode}/posts/${postId}/comments/${commentId}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '댓글 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
@@ -31,9 +32,6 @@ import type {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
* API 응답 데이터를 프론트엔드 형식으로 변환
|
||||
*/
|
||||
@@ -161,7 +159,7 @@ interface EmployeeApiData {
|
||||
|
||||
export async function getEmployeesForAttendance(): Promise<EmployeeOption[]> {
|
||||
const result = await executeServerAction<PaginatedApiResponse<EmployeeApiData>>({
|
||||
url: `${API_URL}/v1/employees?per_page=100&status=active`,
|
||||
url: buildApiUrl('/api/v1/employees', { per_page: 100, status: 'active' }),
|
||||
errorMessage: '사원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -185,20 +183,19 @@ export async function getAttendances(params?: {
|
||||
date_from?: string; date_to?: string; status?: string;
|
||||
department_id?: string; sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: AttendanceRecord[]; total: number; lastPage: number }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.user_id) searchParams.set('user_id', params.user_id);
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
if (params?.date_from) searchParams.set('date_from', params.date_from);
|
||||
if (params?.date_to) searchParams.set('date_to', params.date_to);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<AttendanceApiData>>({
|
||||
url: `${API_URL}/v1/attendances?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/attendances', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
user_id: params?.user_id,
|
||||
date: params?.date,
|
||||
date_from: params?.date_from,
|
||||
date_to: params?.date_to,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
department_id: params?.department_id,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '근태 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -213,7 +210,7 @@ export async function getAttendances(params?: {
|
||||
|
||||
export async function getAttendanceById(id: string): Promise<AttendanceRecord | null> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
url: buildApiUrl(`/api/v1/attendances/${id}`),
|
||||
transform: (data: AttendanceApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '근태 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -225,7 +222,7 @@ export async function createAttendance(
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances`,
|
||||
url: buildApiUrl('/api/v1/attendances'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: AttendanceApiData) => transformApiToFrontend(d),
|
||||
@@ -240,7 +237,7 @@ export async function updateAttendance(
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
url: buildApiUrl(`/api/v1/attendances/${id}`),
|
||||
method: 'PATCH',
|
||||
body: apiData,
|
||||
transform: (d: AttendanceApiData) => transformApiToFrontend(d),
|
||||
@@ -251,7 +248,7 @@ export async function updateAttendance(
|
||||
|
||||
export async function deleteAttendance(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
url: buildApiUrl(`/api/v1/attendances/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '근태 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -260,7 +257,7 @@ export async function deleteAttendance(id: string): Promise<{ success: boolean;
|
||||
|
||||
export async function deleteAttendances(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/bulk-delete`,
|
||||
url: buildApiUrl('/api/v1/attendances/bulk-delete'),
|
||||
method: 'POST',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
errorMessage: '근태 일괄 삭제에 실패했습니다.',
|
||||
@@ -271,11 +268,6 @@ export async function deleteAttendances(ids: string[]): Promise<{ success: boole
|
||||
export async function getMonthlyStats(params: {
|
||||
year: number; month: number; user_id?: string;
|
||||
}): Promise<AttendanceStats | null> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('year', String(params.year));
|
||||
searchParams.set('month', String(params.month));
|
||||
if (params.user_id) searchParams.set('user_id', params.user_id);
|
||||
|
||||
interface MonthlyStatsApiData {
|
||||
year: number; month: number; total_days: number;
|
||||
by_status: {
|
||||
@@ -286,7 +278,11 @@ export async function getMonthlyStats(params: {
|
||||
}
|
||||
|
||||
const result = await executeServerAction<MonthlyStatsApiData>({
|
||||
url: `${API_URL}/v1/attendances/monthly-stats?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/attendances/monthly-stats', {
|
||||
year: params.year,
|
||||
month: params.month,
|
||||
user_id: params.user_id,
|
||||
}),
|
||||
errorMessage: '월간 통계 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -322,15 +318,13 @@ export async function exportAttendanceExcel(params?: {
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.date_from) searchParams.set('date_from', params.date_from);
|
||||
if (params?.date_to) searchParams.set('date_to', params.date_to);
|
||||
if (params?.user_id) searchParams.set('user_id', params.user_id);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/attendances/export${queryString ? `?${queryString}` : ''}`;
|
||||
const url = buildApiUrl('/api/v1/attendances/export', {
|
||||
date_from: params?.date_from,
|
||||
date_to: params?.date_to,
|
||||
user_id: params?.user_id,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
department_id: params?.department_id,
|
||||
});
|
||||
|
||||
const response = await fetch(url, { method: 'GET', headers });
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import type { Card, CardFormData, CardStatus } from './types';
|
||||
|
||||
@@ -43,9 +44,6 @@ interface CardPaginationData {
|
||||
total: number;
|
||||
}
|
||||
|
||||
// API URL (without double /api)
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
// 상태 매핑: API → Frontend
|
||||
function mapApiStatusToFrontend(apiStatus: 'active' | 'inactive'): CardStatus {
|
||||
return apiStatus === 'active' ? 'active' : 'suspended';
|
||||
@@ -114,15 +112,13 @@ function transformFrontendToApi(data: CardFormData): Record<string, unknown> {
|
||||
export async function getCards(params?: {
|
||||
search?: string; status?: string; page?: number; per_page?: number;
|
||||
}): Promise<{ success: boolean; data?: Card[]; pagination?: { total: number; currentPage: number; lastPage: number }; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', mapFrontendStatusToApi(params.status as CardStatus));
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction<CardPaginationData>({
|
||||
url: `${API_URL}/v1/cards${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/cards', {
|
||||
search: params?.search,
|
||||
status: params?.status && params.status !== 'all' ? mapFrontendStatusToApi(params.status as CardStatus) : undefined,
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
}),
|
||||
errorMessage: '카드 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -144,7 +140,7 @@ export async function getCards(params?: {
|
||||
// ===== 카드 상세 조회 =====
|
||||
export async function getCard(id: string): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/cards/${id}`),
|
||||
transform: (data: CardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '카드 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
@@ -153,7 +149,7 @@ export async function getCard(id: string): Promise<ActionResult<Card>> {
|
||||
// ===== 카드 등록 =====
|
||||
export async function createCard(data: CardFormData): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards`,
|
||||
url: buildApiUrl('/api/v1/cards'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: CardApiData) => transformApiToFrontend(d),
|
||||
@@ -164,7 +160,7 @@ export async function createCard(data: CardFormData): Promise<ActionResult<Card>
|
||||
// ===== 카드 수정 =====
|
||||
export async function updateCard(id: string, data: CardFormData): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/cards/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: CardApiData) => transformApiToFrontend(d),
|
||||
@@ -175,7 +171,7 @@ export async function updateCard(id: string, data: CardFormData): Promise<Action
|
||||
// ===== 카드 삭제 =====
|
||||
export async function deleteCard(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/cards/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '카드 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -199,7 +195,7 @@ export async function deleteCards(ids: string[]): Promise<{ success: boolean; er
|
||||
// ===== 카드 상태 토글 =====
|
||||
export async function toggleCardStatus(id: string): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}/toggle`,
|
||||
url: buildApiUrl(`/api/v1/cards/${id}/toggle`),
|
||||
method: 'PATCH',
|
||||
transform: (data: CardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '상태 변경에 실패했습니다.',
|
||||
@@ -219,7 +215,10 @@ export async function getActiveEmployees(): Promise<{ success: boolean; data?: A
|
||||
}
|
||||
|
||||
const result = await executeServerAction<EmployeePaginationData>({
|
||||
url: `${API_URL}/v1/employees?status=active&per_page=50`,
|
||||
url: buildApiUrl('/api/v1/employees', {
|
||||
status: 'active',
|
||||
per_page: 50,
|
||||
}),
|
||||
errorMessage: '직원 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -233,4 +232,4 @@ export async function getActiveEmployees(): Promise<{ success: boolean; data?: A
|
||||
}));
|
||||
|
||||
return { success: true, data: employees };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
@@ -87,8 +88,6 @@ export interface UpdateDepartmentRequest {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
* API 응답을 프론트엔드 형식으로 변환 (재귀)
|
||||
*/
|
||||
@@ -124,14 +123,10 @@ function transformApiToFrontend(apiData: ApiDepartment, depth: number = 0): Depa
|
||||
export async function getDepartmentTree(params?: {
|
||||
withUsers?: boolean;
|
||||
}): Promise<ActionResult<DepartmentRecord[]>> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.withUsers) {
|
||||
queryParams.append('with_users', '1');
|
||||
}
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/tree${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/departments/tree', {
|
||||
with_users: params?.withUsers ? '1' : undefined,
|
||||
}),
|
||||
transform: (data: ApiDepartment[]) => data.map((dept) => transformApiToFrontend(dept, 0)),
|
||||
errorMessage: '부서 트리 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -145,7 +140,7 @@ export async function getDepartmentById(
|
||||
id: number
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/departments/${id}`),
|
||||
transform: (data: ApiDepartment) => transformApiToFrontend(data),
|
||||
errorMessage: '부서 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -159,7 +154,7 @@ export async function createDepartment(
|
||||
data: CreateDepartmentRequest
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments`,
|
||||
url: buildApiUrl('/api/v1/departments'),
|
||||
method: 'POST',
|
||||
body: {
|
||||
parent_id: data.parentId,
|
||||
@@ -183,7 +178,7 @@ export async function updateDepartment(
|
||||
data: UpdateDepartmentRequest
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/departments/${id}`),
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
parent_id: data.parentId === null ? 0 : data.parentId,
|
||||
@@ -206,7 +201,7 @@ export async function deleteDepartment(
|
||||
id: number
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/departments/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '부서 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
@@ -44,18 +45,17 @@ export async function getEmployees(params?: {
|
||||
department_id?: string; has_account?: boolean;
|
||||
sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: Employee[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
if (params?.has_account !== undefined) searchParams.set('has_account', String(params.has_account));
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<EmployeeApiData>>({
|
||||
url: `${API_URL}/api/v1/employees?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/employees', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
q: params?.q,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
department_id: params?.department_id,
|
||||
has_account: params?.has_account,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '직원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function getEmployees(params?: {
|
||||
|
||||
export async function getEmployeeById(id: string): Promise<Employee | null | { __authError: true }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
url: buildApiUrl(`/api/v1/employees/${id}`),
|
||||
transform: (data: EmployeeApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '직원 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -88,7 +88,7 @@ export async function createEmployee(
|
||||
}> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees`,
|
||||
url: buildApiUrl('/api/v1/employees'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: EmployeeApiData) => transformApiToFrontend(d),
|
||||
@@ -105,7 +105,7 @@ export async function updateEmployee(
|
||||
): Promise<{ success: boolean; data?: Employee; error?: string; __authError?: boolean }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
url: buildApiUrl(`/api/v1/employees/${id}`),
|
||||
method: 'PATCH',
|
||||
body: apiData,
|
||||
transform: (d: EmployeeApiData) => transformApiToFrontend(d),
|
||||
@@ -118,7 +118,7 @@ export async function updateEmployee(
|
||||
|
||||
export async function deleteEmployee(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
url: buildApiUrl(`/api/v1/employees/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '직원 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -129,7 +129,7 @@ export async function deleteEmployee(id: string): Promise<{ success: boolean; er
|
||||
|
||||
export async function deleteEmployees(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/bulk-delete`,
|
||||
url: buildApiUrl('/api/v1/employees/bulk-delete'),
|
||||
method: 'POST',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
errorMessage: '직원 일괄 삭제에 실패했습니다.',
|
||||
@@ -148,7 +148,7 @@ interface EmployeeStatsApiData {
|
||||
|
||||
export async function getEmployeeStats(): Promise<EmployeeStats | null | { __authError: true }> {
|
||||
const result = await executeServerAction<EmployeeStatsApiData>({
|
||||
url: `${API_URL}/api/v1/employees/stats`,
|
||||
url: buildApiUrl('/api/v1/employees/stats'),
|
||||
errorMessage: '직원 통계 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -177,11 +177,10 @@ export interface PositionItem {
|
||||
}
|
||||
|
||||
export async function getPositions(type?: 'rank' | 'title'): Promise<PositionItem[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (type) searchParams.set('type', type);
|
||||
|
||||
const result = await executeServerAction<PositionItem[]>({
|
||||
url: `${API_URL}/api/v1/positions?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/positions', {
|
||||
type,
|
||||
}),
|
||||
errorMessage: '직급/직책 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || [];
|
||||
@@ -201,7 +200,7 @@ export interface DepartmentItem {
|
||||
|
||||
export async function getDepartments(): Promise<DepartmentItem[]> {
|
||||
const result = await executeServerAction<DepartmentItem[] | { data: DepartmentItem[] }>({
|
||||
url: `${API_URL}/api/v1/departments`,
|
||||
url: buildApiUrl('/api/v1/departments'),
|
||||
errorMessage: '부서 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.data) return [];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import type { SalaryRecord, SalaryDetail, PaymentStatus } from './types';
|
||||
@@ -66,9 +67,6 @@ interface StatisticsApiData {
|
||||
completed_count: number;
|
||||
}
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
// API → Frontend 변환 (목록용)
|
||||
function transformApiToFrontend(apiData: SalaryApiData): SalaryRecord {
|
||||
const profile = apiData.employee_profile;
|
||||
@@ -144,20 +142,18 @@ export async function getSalaries(params?: {
|
||||
pagination?: { total: number; currentPage: number; lastPage: number };
|
||||
error?: string
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.employee_id) searchParams.set('employee_id', String(params.employee_id));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction<SalaryPaginationData>({
|
||||
url: `${API_URL}/v1/salaries${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/salaries', {
|
||||
search: params?.search,
|
||||
year: params?.year,
|
||||
month: params?.month,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
employee_id: params?.employee_id,
|
||||
start_date: params?.start_date,
|
||||
end_date: params?.end_date,
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
}),
|
||||
errorMessage: '급여 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -179,7 +175,7 @@ export async function getSalary(id: string): Promise<{
|
||||
success: boolean; data?: SalaryDetail; error?: string
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}`,
|
||||
url: buildApiUrl(`/api/v1/salaries/${id}`),
|
||||
transform: (data: SalaryApiData) => transformApiToDetail(data),
|
||||
errorMessage: '급여 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
@@ -192,7 +188,7 @@ export async function updateSalaryStatus(
|
||||
status: PaymentStatus
|
||||
): Promise<{ success: boolean; data?: SalaryRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}/status`,
|
||||
url: buildApiUrl(`/api/v1/salaries/${id}/status`),
|
||||
method: 'PATCH',
|
||||
body: { status },
|
||||
transform: (data: SalaryApiData) => transformApiToFrontend(data),
|
||||
@@ -207,7 +203,7 @@ export async function bulkUpdateSalaryStatus(
|
||||
status: PaymentStatus
|
||||
): Promise<{ success: boolean; updatedCount?: number; error?: string }> {
|
||||
const result = await executeServerAction<{ updated_count: number }>({
|
||||
url: `${API_URL}/v1/salaries/bulk-update-status`,
|
||||
url: buildApiUrl('/api/v1/salaries/bulk-update-status'),
|
||||
method: 'POST',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)), status },
|
||||
errorMessage: '일괄 상태 변경에 실패했습니다.',
|
||||
@@ -228,7 +224,7 @@ export async function updateSalary(
|
||||
}
|
||||
): Promise<{ success: boolean; data?: SalaryDetail; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}`,
|
||||
url: buildApiUrl(`/api/v1/salaries/${id}`),
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: (d: SalaryApiData) => transformApiToDetail(d),
|
||||
@@ -249,15 +245,13 @@ export async function getSalaryStatistics(params?: {
|
||||
};
|
||||
error?: string
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction<StatisticsApiData>({
|
||||
url: `${API_URL}/v1/salaries/statistics${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/salaries/statistics', {
|
||||
year: params?.year,
|
||||
month: params?.month,
|
||||
start_date: params?.start_date,
|
||||
end_date: params?.end_date,
|
||||
}),
|
||||
errorMessage: '통계 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -303,18 +297,14 @@ export async function exportSalaryExcel(params?: {
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.employee_id) searchParams.set('employee_id', String(params.employee_id));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/salaries/export${queryString ? `?${queryString}` : ''}`;
|
||||
const url = buildApiUrl('/api/v1/salaries/export', {
|
||||
year: params?.year,
|
||||
month: params?.month,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
employee_id: params?.employee_id,
|
||||
start_date: params?.start_date,
|
||||
end_date: params?.end_date,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Download,
|
||||
DollarSign,
|
||||
Check,
|
||||
Clock,
|
||||
@@ -382,6 +381,26 @@ export function SalaryManagement() {
|
||||
|
||||
itemsPerPage: itemsPerPage,
|
||||
|
||||
// 엑셀 다운로드 설정
|
||||
excelDownload: {
|
||||
columns: [
|
||||
{ header: '부서', key: 'department' },
|
||||
{ header: '직책', key: 'position' },
|
||||
{ header: '이름', key: 'employeeName' },
|
||||
{ header: '직급', key: 'rank' },
|
||||
{ header: '기본급', key: 'baseSalary' },
|
||||
{ header: '수당', key: 'allowance' },
|
||||
{ header: '초과근무', key: 'overtime' },
|
||||
{ header: '상여', key: 'bonus' },
|
||||
{ header: '공제', key: 'deduction' },
|
||||
{ header: '실지급액', key: 'netPayment' },
|
||||
{ header: '지급일', key: 'paymentDate' },
|
||||
{ header: '상태', key: 'status', transform: (value: unknown) => value === 'completed' ? '지급완료' : '지급예정' },
|
||||
],
|
||||
filename: '급여명세',
|
||||
sheetName: '급여',
|
||||
},
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
searchValue: searchQuery,
|
||||
@@ -428,16 +447,7 @@ export function SalaryManagement() {
|
||||
</>
|
||||
),
|
||||
|
||||
headerActions: () => (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{canExport && (
|
||||
<Button variant="outline" onClick={() => toast.info('엑셀 다운로드 기능은 준비 중입니다.')}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
headerActions: () => null,
|
||||
|
||||
renderTableRow: (item, index, globalIndex, handlers) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
|
||||
// ============================================
|
||||
@@ -165,9 +166,6 @@ interface ApiResponse<T> {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
* API 응답에서 프론트엔드 형식으로 변환
|
||||
*/
|
||||
@@ -237,23 +235,20 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{
|
||||
data?: { items: LeaveRecord[]; total: number; currentPage: number; lastPage: number };
|
||||
error?: string;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
if (params.userId) searchParams.append('user_id', params.userId.toString());
|
||||
if (params.status) searchParams.append('status', params.status);
|
||||
if (params.leaveType) searchParams.append('leave_type', params.leaveType);
|
||||
if (params.dateFrom) searchParams.append('date_from', params.dateFrom);
|
||||
if (params.dateTo) searchParams.append('date_to', params.dateTo);
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<PaginatedApiResponse<Record<string, unknown>>>({
|
||||
url: `${API_URL}/v1/leaves${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/leaves', {
|
||||
user_id: params?.userId,
|
||||
status: params?.status,
|
||||
leave_type: params?.leaveType,
|
||||
date_from: params?.dateFrom,
|
||||
date_to: params?.dateTo,
|
||||
year: params?.year,
|
||||
department_id: params?.departmentId,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
per_page: params?.perPage,
|
||||
page: params?.page,
|
||||
}),
|
||||
errorMessage: '휴가 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -269,7 +264,7 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{
|
||||
/** 휴가 상세 조회 */
|
||||
export async function getLeaveById(id: number): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}`),
|
||||
transform: (data: Record<string, unknown>) => transformApiToFrontend(data),
|
||||
errorMessage: '휴가 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -279,7 +274,7 @@ export async function getLeaveById(id: number): Promise<{ success: boolean; data
|
||||
/** 휴가 신청 */
|
||||
export async function createLeave(data: CreateLeaveRequest): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves`,
|
||||
url: buildApiUrl('/api/v1/leaves'),
|
||||
method: 'POST',
|
||||
body: { user_id: data.userId, leave_type: data.leaveType, start_date: data.startDate, end_date: data.endDate, days: data.days, reason: data.reason },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
@@ -291,7 +286,7 @@ export async function createLeave(data: CreateLeaveRequest): Promise<{ success:
|
||||
/** 휴가 승인 */
|
||||
export async function approveLeave(id: number, comment?: string): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}/approve`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}/approve`),
|
||||
method: 'POST',
|
||||
body: { comment },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
@@ -303,7 +298,7 @@ export async function approveLeave(id: number, comment?: string): Promise<{ succ
|
||||
/** 휴가 반려 */
|
||||
export async function rejectLeave(id: number, reason: string): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}/reject`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}/reject`),
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
@@ -315,7 +310,7 @@ export async function rejectLeave(id: number, reason: string): Promise<{ success
|
||||
/** 휴가 취소 */
|
||||
export async function cancelLeave(id: number, reason?: string): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}/cancel`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}/cancel`),
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
@@ -326,9 +321,8 @@ export async function cancelLeave(id: number, reason?: string): Promise<{ succes
|
||||
|
||||
/** 내 잔여 휴가 조회 */
|
||||
export async function getMyLeaveBalance(year?: number): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
const url = year ? `${API_URL}/v1/leaves/balance?year=${year}` : `${API_URL}/v1/leaves/balance`;
|
||||
const result = await executeServerAction({
|
||||
url,
|
||||
url: buildApiUrl('/api/v1/leaves/balance', { year }),
|
||||
transform: (data: Record<string, unknown>) => transformBalanceToFrontend(data),
|
||||
errorMessage: '잔여 휴가 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -337,9 +331,8 @@ export async function getMyLeaveBalance(year?: number): Promise<{ success: boole
|
||||
|
||||
/** 특정 사용자 잔여 휴가 조회 */
|
||||
export async function getUserLeaveBalance(userId: number, year?: number): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
const url = year ? `${API_URL}/v1/leaves/balance/${userId}?year=${year}` : `${API_URL}/v1/leaves/balance/${userId}`;
|
||||
const result = await executeServerAction({
|
||||
url,
|
||||
url: buildApiUrl(`/api/v1/leaves/balance/${userId}`, { year }),
|
||||
transform: (data: Record<string, unknown>) => transformBalanceToFrontend(data),
|
||||
errorMessage: '잔여 휴가 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -349,7 +342,7 @@ export async function getUserLeaveBalance(userId: number, year?: number): Promis
|
||||
/** 잔여 휴가 설정 (부여) */
|
||||
export async function setLeaveBalance(data: SetLeaveBalanceRequest): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/balance`,
|
||||
url: buildApiUrl('/api/v1/leaves/balance'),
|
||||
method: 'PUT',
|
||||
body: { user_id: data.userId, year: data.year, total_days: data.totalDays },
|
||||
transform: (d: Record<string, unknown>) => transformBalanceToFrontend(d),
|
||||
@@ -361,7 +354,7 @@ export async function setLeaveBalance(data: SetLeaveBalanceRequest): Promise<{ s
|
||||
/** 휴가 삭제 */
|
||||
export async function deleteLeave(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '휴가 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -402,19 +395,16 @@ export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise
|
||||
data?: { items: LeaveBalanceRecord[]; total: number; currentPage: number; lastPage: number };
|
||||
error?: string;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.search) searchParams.append('search', params.search);
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<PaginatedApiResponse<Record<string, unknown>>>({
|
||||
url: `${API_URL}/v1/leaves/balances${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/leaves/balances', {
|
||||
year: params?.year,
|
||||
department_id: params?.departmentId,
|
||||
search: params?.search,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
per_page: params?.perPage,
|
||||
page: params?.page,
|
||||
}),
|
||||
errorMessage: '휴가 사용현황 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -517,23 +507,20 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
|
||||
data?: { items: LeaveGrantRecord[]; total: number; currentPage: number; lastPage: number };
|
||||
error?: string;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
if (params.userId) searchParams.append('user_id', params.userId.toString());
|
||||
if (params.grantType) searchParams.append('grant_type', params.grantType);
|
||||
if (params.dateFrom) searchParams.append('date_from', params.dateFrom);
|
||||
if (params.dateTo) searchParams.append('date_to', params.dateTo);
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.search) searchParams.append('search', params.search);
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<PaginatedApiResponse<Record<string, unknown>>>({
|
||||
url: `${API_URL}/v1/leaves/grants${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/leaves/grants', {
|
||||
user_id: params?.userId,
|
||||
grant_type: params?.grantType,
|
||||
date_from: params?.dateFrom,
|
||||
date_to: params?.dateTo,
|
||||
year: params?.year,
|
||||
department_id: params?.departmentId,
|
||||
search: params?.search,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
per_page: params?.perPage,
|
||||
page: params?.page,
|
||||
}),
|
||||
errorMessage: '휴가 부여 이력 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -549,7 +536,7 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
|
||||
/** 휴가 부여 */
|
||||
export async function createLeaveGrant(data: CreateLeaveGrantRequest): Promise<{ success: boolean; data?: LeaveGrantRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/grants`,
|
||||
url: buildApiUrl('/api/v1/leaves/grants'),
|
||||
method: 'POST',
|
||||
body: { user_id: data.userId, grant_type: data.grantType, grant_date: data.grantDate, grant_days: data.grantDays, reason: data.reason },
|
||||
transform: (d: Record<string, unknown>) => transformGrantRecordToFrontend(d),
|
||||
@@ -561,7 +548,7 @@ export async function createLeaveGrant(data: CreateLeaveGrantRequest): Promise<{
|
||||
/** 휴가 부여 삭제 */
|
||||
export async function deleteLeaveGrant(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/grants/${id}`,
|
||||
url: buildApiUrl(`/api/v1/leaves/grants/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '휴가 부여 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -615,7 +602,7 @@ export interface EmployeeOption {
|
||||
export async function getActiveEmployees(): Promise<{ success: boolean; data?: EmployeeOption[]; error?: string }> {
|
||||
interface EmployeePaginatedApiResponse { data: Record<string, unknown>[]; total: number }
|
||||
const result = await executeServerAction<EmployeePaginatedApiResponse>({
|
||||
url: `${API_URL}/v1/employees?status=active&per_page=100`,
|
||||
url: buildApiUrl('/api/v1/employees', { status: 'active', per_page: 100 }),
|
||||
errorMessage: '직원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return { success: false, error: result.error };
|
||||
|
||||
@@ -18,11 +18,10 @@ const USE_MOCK_DATA = false;
|
||||
|
||||
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
|
||||
import type {
|
||||
ReceivingItem,
|
||||
@@ -361,8 +360,6 @@ interface ReceivingApiData {
|
||||
has_inspection_template?: boolean;
|
||||
}
|
||||
|
||||
type ReceivingApiPaginatedResponse = PaginatedApiResponse<ReceivingApiData>;
|
||||
|
||||
interface ReceivingApiStatsResponse {
|
||||
receiving_pending_count: number;
|
||||
shipping_count: number;
|
||||
@@ -513,14 +510,6 @@ function transformProcessDataToApi(
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ===== 입고 목록 조회 =====
|
||||
export async function getReceivings(params?: {
|
||||
page?: number;
|
||||
@@ -529,13 +518,7 @@ export async function getReceivings(params?: {
|
||||
endDate?: string;
|
||||
status?: string;
|
||||
search?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data: ReceivingItem[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
}) {
|
||||
// ===== 목데이터 모드 =====
|
||||
if (USE_MOCK_DATA) {
|
||||
let filteredData = [...MOCK_RECEIVING_LIST];
|
||||
@@ -565,7 +548,7 @@ export async function getReceivings(params?: {
|
||||
const paginatedData = filteredData.slice(startIndex, startIndex + perPage);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
success: true as const,
|
||||
data: paginatedData,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
@@ -576,29 +559,18 @@ export async function getReceivings(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
const result = await executeServerAction<ReceivingApiPaginatedResponse>({
|
||||
url: `${API_URL}/api/v1/receivings${queryString ? `?${queryString}` : ''}`,
|
||||
return executePaginatedAction<ReceivingApiData, ReceivingItem>({
|
||||
url: buildApiUrl('/api/v1/receivings', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
search: params?.search,
|
||||
}),
|
||||
transform: transformApiToListItem,
|
||||
errorMessage: '입고 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, data: [], pagination: emptyPagination, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, data: [], pagination: emptyPagination, error: result.error };
|
||||
|
||||
const pd = result.data;
|
||||
return {
|
||||
success: true,
|
||||
data: (pd.data || []).map(transformApiToListItem),
|
||||
pagination: { currentPage: pd.current_page, lastPage: pd.last_page, perPage: pd.per_page, total: pd.total },
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 입고 통계 조회 =====
|
||||
@@ -611,7 +583,7 @@ export async function getReceivingStats(): Promise<{
|
||||
if (USE_MOCK_DATA) return { success: true, data: MOCK_RECEIVING_STATS };
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivings/stats`,
|
||||
url: buildApiUrl('/api/v1/receivings/stats'),
|
||||
transform: (data: ReceivingApiStatsResponse) => transformApiToStats(data),
|
||||
errorMessage: '입고 통계 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -632,7 +604,7 @@ export async function getReceivingById(id: string): Promise<{
|
||||
}
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivings/${id}`,
|
||||
url: buildApiUrl(`/api/v1/receivings/${id}`),
|
||||
transform: (data: ReceivingApiData) => transformApiToDetail(data),
|
||||
errorMessage: '입고 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -646,7 +618,7 @@ export async function createReceiving(
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivings`,
|
||||
url: buildApiUrl('/api/v1/receivings'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: ReceivingApiData) => transformApiToDetail(d),
|
||||
@@ -663,7 +635,7 @@ export async function updateReceiving(
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivings/${id}`,
|
||||
url: buildApiUrl(`/api/v1/receivings/${id}`),
|
||||
method: 'PUT',
|
||||
body: apiData,
|
||||
transform: (d: ReceivingApiData) => transformApiToDetail(d),
|
||||
@@ -678,7 +650,7 @@ export async function deleteReceiving(
|
||||
id: string
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivings/${id}`,
|
||||
url: buildApiUrl(`/api/v1/receivings/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '입고 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -693,7 +665,7 @@ export async function processReceiving(
|
||||
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
|
||||
const apiData = transformProcessDataToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivings/${id}/process`,
|
||||
url: buildApiUrl(`/api/v1/receivings/${id}/process`),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: ReceivingApiData) => transformApiToDetail(d),
|
||||
@@ -739,13 +711,9 @@ export async function searchItems(query?: string): Promise<{
|
||||
return { success: true, data: filtered };
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (query) searchParams.set('search', query);
|
||||
searchParams.set('per_page', '50');
|
||||
|
||||
interface ItemApiData { data: Array<Record<string, string>> }
|
||||
const result = await executeServerAction<ItemApiData, ItemOption[]>({
|
||||
url: `${API_URL}/api/v1/items?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/items', { search: query, per_page: 50 }),
|
||||
transform: (d) => (d.data || []).map((item) => ({
|
||||
value: item.item_code,
|
||||
label: item.item_code,
|
||||
@@ -786,13 +754,9 @@ export async function searchSuppliers(query?: string): Promise<{
|
||||
return { success: true, data: filtered };
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (query) searchParams.set('search', query);
|
||||
searchParams.set('per_page', '50');
|
||||
|
||||
interface SupplierApiData { data: Array<Record<string, string>> }
|
||||
const result = await executeServerAction<SupplierApiData, SupplierOption[]>({
|
||||
url: `${API_URL}/api/v1/suppliers?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/suppliers', { search: query, per_page: 50 }),
|
||||
transform: (d) => (d.data || []).map((s) => ({
|
||||
value: s.name,
|
||||
label: s.name,
|
||||
@@ -1041,11 +1005,10 @@ export async function checkInspectionTemplate(itemId?: number): Promise<{
|
||||
}
|
||||
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('category', 'incoming_inspection');
|
||||
searchParams.append('item_id', String(itemId));
|
||||
|
||||
const url = `${API_URL}/api/v1/documents/resolve?${searchParams.toString()}`;
|
||||
const url = buildApiUrl('/api/v1/documents/resolve', {
|
||||
category: 'incoming_inspection',
|
||||
item_id: itemId,
|
||||
});
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
@@ -1240,12 +1203,11 @@ export async function getInspectionTemplate(params: {
|
||||
return { success: false, error: '품목 ID가 필요합니다.' };
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('category', 'incoming_inspection');
|
||||
searchParams.set('item_id', String(params.itemId));
|
||||
|
||||
const result = await executeServerAction<DocumentResolveResponse>({
|
||||
url: `${API_URL}/api/v1/documents/resolve?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/documents/resolve', {
|
||||
category: 'incoming_inspection',
|
||||
item_id: params.itemId,
|
||||
}),
|
||||
errorMessage: '검사 템플릿 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -1860,7 +1822,7 @@ export async function uploadInspectionFiles(files: File[]): Promise<{
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(
|
||||
`${API_URL}/api/v1/files/upload`,
|
||||
buildApiUrl('/api/v1/files/upload'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -1881,7 +1843,7 @@ export async function uploadInspectionFiles(files: File[]): Promise<{
|
||||
uploadedFiles.push({
|
||||
id: result.data.id,
|
||||
name: result.data.display_name || file.name,
|
||||
url: `${API_URL}/api/v1/files/${result.data.id}/download`,
|
||||
url: buildApiUrl(`/api/v1/files/${result.data.id}/download`),
|
||||
size: result.data.file_size,
|
||||
});
|
||||
}
|
||||
@@ -1911,7 +1873,7 @@ export async function saveInspectionData(params: {
|
||||
}> {
|
||||
// Step 1: POST /v1/documents/upsert - 검사 데이터 저장
|
||||
const docResult = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/documents/upsert`,
|
||||
url: buildApiUrl('/api/v1/documents/upsert'),
|
||||
method: 'POST',
|
||||
body: {
|
||||
template_id: params.templateId,
|
||||
@@ -1931,7 +1893,7 @@ export async function saveInspectionData(params: {
|
||||
const inspectionResultLabel = params.inspectionResult === 'pass' ? '합격' : params.inspectionResult === 'fail' ? '불합격' : null;
|
||||
|
||||
const recResult = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/receivings/${params.receivingId}`,
|
||||
url: buildApiUrl(`/api/v1/receivings/${params.receivingId}`),
|
||||
method: 'PUT',
|
||||
body: {
|
||||
status: 'receiving_pending',
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
StockItem,
|
||||
StockDetail,
|
||||
@@ -85,8 +86,6 @@ interface StockLotApiData {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
type ItemApiPaginatedResponse = PaginatedApiResponse<ItemApiData>;
|
||||
|
||||
interface StockApiStatsResponse {
|
||||
total_items: number;
|
||||
normal_count: number;
|
||||
@@ -220,59 +219,35 @@ function transformApiToStats(data: StockApiStatsResponse): StockStats {
|
||||
}
|
||||
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 재고 목록 조회 =====
|
||||
export async function getStocks(params?: {
|
||||
page?: number; perPage?: number; search?: string; itemType?: string;
|
||||
status?: string; useStatus?: string; location?: string;
|
||||
sortBy?: string; sortDir?: string; startDate?: string; endDate?: string;
|
||||
}): Promise<{ success: boolean; data: StockItem[]; pagination: PaginationMeta; error?: string; __authError?: boolean }> {
|
||||
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.itemType && params.itemType !== 'all') searchParams.set('item_type', params.itemType);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.useStatus && params.useStatus !== 'all') searchParams.set('is_active', params.useStatus === 'active' ? '1' : '0');
|
||||
if (params?.location) searchParams.set('location', params.location);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<ItemApiPaginatedResponse>({
|
||||
url: `${API_URL}/api/v1/stocks${queryString ? `?${queryString}` : ''}`,
|
||||
}) {
|
||||
return executePaginatedAction<ItemApiData, StockItem>({
|
||||
url: buildApiUrl('/api/v1/stocks', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
search: params?.search,
|
||||
item_type: params?.itemType !== 'all' ? params?.itemType : undefined,
|
||||
status: params?.status !== 'all' ? params?.status : undefined,
|
||||
is_active: params?.useStatus && params.useStatus !== 'all' ? (params.useStatus === 'active' ? '1' : '0') : undefined,
|
||||
location: params?.location,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
}),
|
||||
transform: transformApiToListItem,
|
||||
errorMessage: '재고 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (result.__authError) return { success: false, data: [], pagination: emptyPagination, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, data: [], pagination: emptyPagination, error: result.error };
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: (result.data.data || []).map(transformApiToListItem),
|
||||
pagination: {
|
||||
currentPage: result.data.current_page, lastPage: result.data.last_page,
|
||||
perPage: result.data.per_page, total: result.data.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 재고 통계 조회 =====
|
||||
export async function getStockStats(): Promise<{ success: boolean; data?: StockStats; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/stocks/stats`,
|
||||
url: buildApiUrl('/api/v1/stocks/stats'),
|
||||
transform: (data: StockApiStatsResponse) => transformApiToStats(data),
|
||||
errorMessage: '재고 통계 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -283,7 +258,7 @@ export async function getStockStats(): Promise<{ success: boolean; data?: StockS
|
||||
// ===== 품목유형별 통계 조회 =====
|
||||
export async function getStockStatsByType(): Promise<{ success: boolean; data?: StockApiStatsByTypeResponse; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction<StockApiStatsByTypeResponse>({
|
||||
url: `${API_URL}/api/v1/stocks/stats-by-type`,
|
||||
url: buildApiUrl('/api/v1/stocks/stats-by-type'),
|
||||
errorMessage: '품목유형별 통계 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -293,7 +268,7 @@ export async function getStockStatsByType(): Promise<{ success: boolean; data?:
|
||||
// ===== 재고 상세 조회 (Item 기준, LOT 포함) =====
|
||||
export async function getStockById(id: string): Promise<{ success: boolean; data?: StockDetail; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/stocks/${id}`,
|
||||
url: buildApiUrl(`/api/v1/stocks/${id}`),
|
||||
transform: (data: ItemApiData) => transformApiToDetail(data),
|
||||
errorMessage: '재고 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -306,7 +281,7 @@ export async function updateStock(
|
||||
id: string, data: { safetyStock: number; useStatus: 'active' | 'inactive' }
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/stocks/${id}`,
|
||||
url: buildApiUrl(`/api/v1/stocks/${id}`),
|
||||
method: 'PUT',
|
||||
body: { safety_stock: data.safetyStock, is_active: data.useStatus === 'active' },
|
||||
errorMessage: '재고 수정에 실패했습니다.',
|
||||
@@ -318,7 +293,7 @@ export async function updateStock(
|
||||
// ===== 재고 실사 (일괄 업데이트) =====
|
||||
export async function updateStockAudit(updates: { id: string; actualQty: number }[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/stocks/audit`,
|
||||
url: buildApiUrl('/api/v1/stocks/audit'),
|
||||
method: 'POST',
|
||||
body: { items: updates.map((u) => ({ item_id: u.id, actual_qty: u.actualQty })) },
|
||||
errorMessage: '재고 실사 저장에 실패했습니다.',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
|
||||
// ============================================================================
|
||||
@@ -777,8 +778,6 @@ function transformQuoteItemForSelect(apiItem: ApiQuoteItem): QuotationItem {
|
||||
// API 함수
|
||||
// ============================================================================
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* 수주 목록 조회
|
||||
*/
|
||||
@@ -797,21 +796,18 @@ export async function getOrders(params?: {
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.status) {
|
||||
const apiStatus = FRONTEND_TO_API_STATUS[params.status as OrderStatus];
|
||||
if (apiStatus) searchParams.set('status', apiStatus);
|
||||
}
|
||||
if (params?.order_type) searchParams.set('order_type', params.order_type);
|
||||
if (params?.client_id) searchParams.set('client_id', String(params.client_id));
|
||||
if (params?.date_from) searchParams.set('date_from', params.date_from);
|
||||
if (params?.date_to) searchParams.set('date_to', params.date_to);
|
||||
|
||||
const apiStatus = params?.status ? FRONTEND_TO_API_STATUS[params.status as OrderStatus] : undefined;
|
||||
const result = await executeServerAction<PaginatedApiResponse<ApiOrder>>({
|
||||
url: `${API_URL}/api/v1/orders?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/orders', {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
q: params?.q,
|
||||
status: apiStatus,
|
||||
order_type: params?.order_type,
|
||||
client_id: params?.client_id,
|
||||
date_from: params?.date_from,
|
||||
date_to: params?.date_to,
|
||||
}),
|
||||
errorMessage: '목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -837,7 +833,7 @@ export async function getOrderById(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/orders/${id}`,
|
||||
url: buildApiUrl(`/api/v1/orders/${id}`),
|
||||
transform: (data: ApiOrder) => transformApiToFrontend(data),
|
||||
errorMessage: '조회에 실패했습니다.',
|
||||
});
|
||||
@@ -856,7 +852,7 @@ export async function createOrder(data: OrderFormData | Record<string, unknown>)
|
||||
}> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/orders`,
|
||||
url: buildApiUrl('/api/v1/orders'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: ApiOrder) => transformApiToFrontend(d),
|
||||
@@ -877,7 +873,7 @@ export async function updateOrder(id: string, data: OrderFormData | Record<strin
|
||||
}> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/orders/${id}`,
|
||||
url: buildApiUrl(`/api/v1/orders/${id}`),
|
||||
method: 'PUT',
|
||||
body: apiData,
|
||||
transform: (d: ApiOrder) => transformApiToFrontend(d),
|
||||
@@ -896,7 +892,7 @@ export async function deleteOrder(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/orders/${id}`,
|
||||
url: buildApiUrl(`/api/v1/orders/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -918,7 +914,7 @@ export async function updateOrderStatus(id: string, status: OrderStatus): Promis
|
||||
return { success: false, error: '유효하지 않은 상태입니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/orders/${id}/status`,
|
||||
url: buildApiUrl(`/api/v1/orders/${id}/status`),
|
||||
method: 'PATCH',
|
||||
body: { status: apiStatus },
|
||||
transform: (d: ApiOrder) => transformApiToFrontend(d),
|
||||
@@ -938,7 +934,7 @@ export async function getOrderStats(): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<ApiOrderStats>({
|
||||
url: `${API_URL}/api/v1/orders/stats`,
|
||||
url: buildApiUrl('/api/v1/orders/stats'),
|
||||
errorMessage: '통계 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -1009,7 +1005,7 @@ export async function createOrderFromQuote(
|
||||
if (data?.memo) apiData.memo = data.memo;
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/orders/from-quote/${quoteId}`,
|
||||
url: buildApiUrl(`/api/v1/orders/from-quote/${quoteId}`),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: ApiOrder) => transformApiToFrontend(d),
|
||||
@@ -1049,7 +1045,7 @@ export async function createProductionOrder(
|
||||
if (data?.memo) apiData.memo = data.memo;
|
||||
|
||||
const result = await executeServerAction<ApiProductionOrderResponse>({
|
||||
url: `${API_URL}/api/v1/orders/${orderId}/production-order`,
|
||||
url: buildApiUrl(`/api/v1/orders/${orderId}/production-order`),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
errorMessage: '생산지시 생성에 실패했습니다.',
|
||||
@@ -1087,7 +1083,7 @@ export async function revertProductionOrder(orderId: string): Promise<{
|
||||
previous_status: string;
|
||||
}
|
||||
const result = await executeServerAction<RevertResponse>({
|
||||
url: `${API_URL}/api/v1/orders/${orderId}/revert-production`,
|
||||
url: buildApiUrl(`/api/v1/orders/${orderId}/revert-production`),
|
||||
method: 'POST',
|
||||
errorMessage: '생산지시 되돌리기에 실패했습니다.',
|
||||
});
|
||||
@@ -1118,7 +1114,7 @@ export async function revertOrderConfirmation(orderId: string): Promise<{
|
||||
}> {
|
||||
interface RevertConfirmResponse { order: ApiOrder; previous_status: string }
|
||||
const result = await executeServerAction<RevertConfirmResponse>({
|
||||
url: `${API_URL}/api/v1/orders/${orderId}/revert-confirmation`,
|
||||
url: buildApiUrl(`/api/v1/orders/${orderId}/revert-confirmation`),
|
||||
method: 'POST',
|
||||
errorMessage: '수주확정 되돌리기에 실패했습니다.',
|
||||
});
|
||||
@@ -1144,7 +1140,7 @@ export async function getQuoteByIdForSelect(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}?with_items=true`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}`, { with_items: true }),
|
||||
transform: (data: ApiQuoteForSelect) => transformQuoteForSelect(data),
|
||||
errorMessage: '견적 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -1166,16 +1162,15 @@ export async function getQuotesForSelect(params?: {
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('status', 'finalized');
|
||||
searchParams.set('with_items', 'true');
|
||||
searchParams.set('for_order', 'true');
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size || 50));
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<ApiQuoteForSelect>>({
|
||||
url: `${API_URL}/api/v1/quotes?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/quotes', {
|
||||
status: 'finalized',
|
||||
with_items: 'true',
|
||||
for_order: 'true',
|
||||
q: params?.q,
|
||||
page: params?.page,
|
||||
size: params?.size || 50,
|
||||
}),
|
||||
errorMessage: '견적 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
ShipmentItem,
|
||||
ShipmentDetail,
|
||||
@@ -112,8 +113,6 @@ interface ShipmentItemApiData {
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
type ShipmentApiPaginatedResponse = PaginatedApiResponse<ShipmentApiData>;
|
||||
|
||||
interface ShipmentApiStatsResponse {
|
||||
today_shipment_count: number;
|
||||
scheduled_count: number;
|
||||
@@ -294,60 +293,36 @@ function transformEditFormToApi(
|
||||
}
|
||||
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
interface PaginationMeta {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 출고 목록 조회 =====
|
||||
export async function getShipments(params?: {
|
||||
page?: number; perPage?: number; search?: string; status?: string;
|
||||
priority?: string; deliveryMethod?: string; scheduledFrom?: string; scheduledTo?: string;
|
||||
canShip?: boolean; depositConfirmed?: boolean; sortBy?: string; sortDir?: string;
|
||||
}): Promise<{ success: boolean; data: ShipmentItem[]; pagination: PaginationMeta; error?: string; __authError?: boolean }> {
|
||||
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.priority && params.priority !== 'all') searchParams.set('priority', params.priority);
|
||||
if (params?.deliveryMethod && params.deliveryMethod !== 'all') searchParams.set('delivery_method', params.deliveryMethod);
|
||||
if (params?.scheduledFrom) searchParams.set('scheduled_from', params.scheduledFrom);
|
||||
if (params?.scheduledTo) searchParams.set('scheduled_to', params.scheduledTo);
|
||||
if (params?.canShip !== undefined) searchParams.set('can_ship', String(params.canShip));
|
||||
if (params?.depositConfirmed !== undefined) searchParams.set('deposit_confirmed', String(params.depositConfirmed));
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<ShipmentApiPaginatedResponse>({
|
||||
url: `${API_URL}/api/v1/shipments${queryString ? `?${queryString}` : ''}`,
|
||||
}) {
|
||||
return executePaginatedAction<ShipmentApiData, ShipmentItem>({
|
||||
url: buildApiUrl('/api/v1/shipments', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
search: params?.search,
|
||||
status: params?.status !== 'all' ? params?.status : undefined,
|
||||
priority: params?.priority !== 'all' ? params?.priority : undefined,
|
||||
delivery_method: params?.deliveryMethod !== 'all' ? params?.deliveryMethod : undefined,
|
||||
scheduled_from: params?.scheduledFrom,
|
||||
scheduled_to: params?.scheduledTo,
|
||||
can_ship: params?.canShip,
|
||||
deposit_confirmed: params?.depositConfirmed,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
}),
|
||||
transform: transformApiToListItem,
|
||||
errorMessage: '출고 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (result.__authError) return { success: false, data: [], pagination: emptyPagination, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, data: [], pagination: emptyPagination, error: result.error };
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: (result.data.data || []).map(transformApiToListItem),
|
||||
pagination: {
|
||||
currentPage: result.data.current_page, lastPage: result.data.last_page,
|
||||
perPage: result.data.per_page, total: result.data.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 출고 통계 조회 =====
|
||||
export async function getShipmentStats(): Promise<{ success: boolean; data?: ShipmentStats; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/shipments/stats`,
|
||||
url: buildApiUrl('/api/v1/shipments/stats'),
|
||||
transform: (data: ShipmentApiStatsResponse & { total_count?: number }) => transformApiToStats(data),
|
||||
errorMessage: '출고 통계 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -358,7 +333,7 @@ export async function getShipmentStats(): Promise<{ success: boolean; data?: Shi
|
||||
// ===== 상태별 통계 조회 (탭용) =====
|
||||
export async function getShipmentStatsByStatus(): Promise<{ success: boolean; data?: ShipmentStatusStats; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/shipments/stats-by-status`,
|
||||
url: buildApiUrl('/api/v1/shipments/stats-by-status'),
|
||||
transform: (data: ShipmentApiStatsByStatusResponse) => transformApiToStatsByStatus(data),
|
||||
errorMessage: '상태별 통계 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -369,7 +344,7 @@ export async function getShipmentStatsByStatus(): Promise<{ success: boolean; da
|
||||
// ===== 출고 상세 조회 =====
|
||||
export async function getShipmentById(id: string): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/shipments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/shipments/${id}`),
|
||||
transform: (data: ShipmentApiData) => transformApiToDetail(data),
|
||||
errorMessage: '출고 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -383,7 +358,7 @@ export async function createShipment(
|
||||
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
|
||||
const apiData = transformCreateFormToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/shipments`,
|
||||
url: buildApiUrl('/api/v1/shipments'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: ShipmentApiData) => transformApiToDetail(d),
|
||||
@@ -399,7 +374,7 @@ export async function updateShipment(
|
||||
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
|
||||
const apiData = transformEditFormToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/shipments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/shipments/${id}`),
|
||||
method: 'PUT',
|
||||
body: apiData,
|
||||
transform: (d: ShipmentApiData) => transformApiToDetail(d),
|
||||
@@ -426,7 +401,7 @@ export async function updateShipmentStatus(
|
||||
if (additionalData?.confirmedArrival) apiData.confirmed_arrival = additionalData.confirmedArrival;
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/shipments/${id}/status`,
|
||||
url: buildApiUrl(`/api/v1/shipments/${id}/status`),
|
||||
method: 'PATCH',
|
||||
body: apiData,
|
||||
transform: (d: ShipmentApiData) => transformApiToDetail(d),
|
||||
@@ -439,7 +414,7 @@ export async function updateShipmentStatus(
|
||||
// ===== 출고 삭제 =====
|
||||
export async function deleteShipment(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/shipments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/shipments/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '출고 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -450,7 +425,7 @@ export async function deleteShipment(id: string): Promise<{ success: boolean; er
|
||||
// ===== LOT 옵션 조회 =====
|
||||
export async function getLotOptions(): Promise<{ success: boolean; data: LotOption[]; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction<LotOption[]>({
|
||||
url: `${API_URL}/api/v1/shipments/options/lots`,
|
||||
url: buildApiUrl('/api/v1/shipments/options/lots'),
|
||||
errorMessage: 'LOT 옵션 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, data: [], __authError: true };
|
||||
@@ -460,7 +435,7 @@ export async function getLotOptions(): Promise<{ success: boolean; data: LotOpti
|
||||
// ===== 물류사 옵션 조회 =====
|
||||
export async function getLogisticsOptions(): Promise<{ success: boolean; data: LogisticsOption[]; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction<LogisticsOption[]>({
|
||||
url: `${API_URL}/api/v1/shipments/options/logistics`,
|
||||
url: buildApiUrl('/api/v1/shipments/options/logistics'),
|
||||
errorMessage: '물류사 옵션 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, data: [], __authError: true };
|
||||
@@ -470,7 +445,7 @@ export async function getLogisticsOptions(): Promise<{ success: boolean; data: L
|
||||
// ===== 차량 톤수 옵션 조회 =====
|
||||
export async function getVehicleTonnageOptions(): Promise<{ success: boolean; data: VehicleTonnageOption[]; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction<VehicleTonnageOption[]>({
|
||||
url: `${API_URL}/api/v1/shipments/options/vehicle-tonnage`,
|
||||
url: buildApiUrl('/api/v1/shipments/options/vehicle-tonnage'),
|
||||
errorMessage: '차량 톤수 옵션 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, data: [], __authError: true };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { Process, ProcessFormData, ClassificationRule, IndividualItem, ProcessStep } from '@/types/process';
|
||||
|
||||
@@ -222,8 +223,6 @@ function transformFrontendToApi(data: ProcessFormData): Record<string, unknown>
|
||||
// API 함수
|
||||
// ============================================================================
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* 공정 목록 조회
|
||||
*/
|
||||
@@ -234,15 +233,14 @@ export async function getProcessList(params?: {
|
||||
status?: string;
|
||||
process_type?: string;
|
||||
}): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string; __authError?: boolean }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
if (params?.process_type) searchParams.set('process_type', params.process_type);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<ApiProcess>>({
|
||||
url: `${API_URL}/api/v1/processes?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/processes', {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
q: params?.q,
|
||||
status: params?.status,
|
||||
process_type: params?.process_type,
|
||||
}),
|
||||
errorMessage: '공정 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -263,7 +261,7 @@ export async function getProcessList(params?: {
|
||||
*/
|
||||
export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/processes/${id}`),
|
||||
transform: (data: ApiProcess) => transformApiToFrontend(data),
|
||||
errorMessage: '공정 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -276,7 +274,7 @@ export async function getProcessById(id: string): Promise<{ success: boolean; da
|
||||
*/
|
||||
export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes`,
|
||||
url: buildApiUrl('/api/v1/processes'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: ApiProcess) => transformApiToFrontend(d),
|
||||
@@ -291,7 +289,7 @@ export async function createProcess(data: ProcessFormData): Promise<{ success: b
|
||||
*/
|
||||
export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/processes/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: ApiProcess) => transformApiToFrontend(d),
|
||||
@@ -306,7 +304,7 @@ export async function updateProcess(id: string, data: ProcessFormData): Promise<
|
||||
*/
|
||||
export async function removeProcessItem(processId: string, remainingItemIds: number[]): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}`,
|
||||
url: buildApiUrl(`/api/v1/processes/${processId}`),
|
||||
method: 'PUT',
|
||||
body: { item_ids: remainingItemIds },
|
||||
transform: (d: ApiProcess) => transformApiToFrontend(d),
|
||||
@@ -321,7 +319,7 @@ export async function removeProcessItem(processId: string, remainingItemIds: num
|
||||
*/
|
||||
export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/processes/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '공정 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -334,7 +332,7 @@ export async function deleteProcess(id: string): Promise<{ success: boolean; err
|
||||
*/
|
||||
export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction<{ deleted_count: number }>({
|
||||
url: `${API_URL}/api/v1/processes`,
|
||||
url: buildApiUrl('/api/v1/processes'),
|
||||
method: 'DELETE',
|
||||
body: { ids: ids.map((id) => parseInt(id, 10)) },
|
||||
errorMessage: '공정 일괄 삭제에 실패했습니다.',
|
||||
@@ -349,7 +347,7 @@ export async function deleteProcesses(ids: string[]): Promise<{ success: boolean
|
||||
*/
|
||||
export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${id}/toggle`,
|
||||
url: buildApiUrl(`/api/v1/processes/${id}/toggle`),
|
||||
method: 'PATCH',
|
||||
transform: (d: ApiProcess) => transformApiToFrontend(d),
|
||||
errorMessage: '공정 상태 변경에 실패했습니다.',
|
||||
@@ -365,7 +363,7 @@ export async function reorderProcesses(
|
||||
processes: { id: string; order: number }[]
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/reorder`,
|
||||
url: buildApiUrl('/api/v1/processes/reorder'),
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
items: processes.map((p) => ({
|
||||
@@ -390,7 +388,7 @@ export async function getProcessOptions(): Promise<{
|
||||
}> {
|
||||
interface ApiOptionItem { id: number; process_code: string; process_name: string; process_type: string; department: string }
|
||||
const result = await executeServerAction<ApiOptionItem[]>({
|
||||
url: `${API_URL}/api/v1/processes/options`,
|
||||
url: buildApiUrl('/api/v1/processes/options'),
|
||||
errorMessage: '공정 옵션 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -418,7 +416,7 @@ export async function getProcessStats(): Promise<{
|
||||
}> {
|
||||
interface ApiStats { total: number; active: number; inactive: number; by_type: Record<string, number> }
|
||||
const result = await executeServerAction<ApiStats>({
|
||||
url: `${API_URL}/api/v1/processes/stats`,
|
||||
url: buildApiUrl('/api/v1/processes/stats'),
|
||||
errorMessage: '공정 통계 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -456,7 +454,7 @@ export async function getDepartmentOptions(): Promise<DepartmentOption[]> {
|
||||
];
|
||||
interface DeptResponse { data: Array<{ id: number; name: string }> }
|
||||
const result = await executeServerAction<DeptResponse>({
|
||||
url: `${API_URL}/api/v1/departments`,
|
||||
url: buildApiUrl('/api/v1/departments'),
|
||||
errorMessage: '부서 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return defaultOptions;
|
||||
@@ -507,15 +505,14 @@ interface GetItemListParams {
|
||||
* - 파라미터에 processName, processCategory 필터 추가 필요 (공정/구분 필터링용)
|
||||
*/
|
||||
export async function getItemList(params?: GetItemListParams): Promise<ItemOption[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('size', String(params?.size || 1000));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.itemType) searchParams.set('item_type', params.itemType);
|
||||
if (params?.excludeProcessId) searchParams.set('exclude_process_id', params.excludeProcessId);
|
||||
|
||||
interface ItemListResponse { data: Array<{ id: number; name: string; item_code?: string; item_type?: string; item_type_name?: string }> }
|
||||
const result = await executeServerAction<ItemListResponse>({
|
||||
url: `${API_URL}/api/v1/items?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/items', {
|
||||
size: params?.size || 1000,
|
||||
q: params?.q,
|
||||
item_type: params?.itemType,
|
||||
exclude_process_id: params?.excludeProcessId,
|
||||
}),
|
||||
errorMessage: '품목 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return [];
|
||||
@@ -534,7 +531,7 @@ export async function getItemList(params?: GetItemListParams): Promise<ItemOptio
|
||||
*/
|
||||
export async function getItemTypeOptions(): Promise<Array<{ value: string; label: string }>> {
|
||||
const result = await executeServerAction<Array<{ code: string; name: string }>>({
|
||||
url: `${API_URL}/api/v1/settings/common/item_type`,
|
||||
url: buildApiUrl('/api/v1/settings/common/item_type'),
|
||||
errorMessage: '품목 유형 옵션 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return [];
|
||||
@@ -561,7 +558,7 @@ export async function getDocumentTemplates(): Promise<{
|
||||
}> {
|
||||
interface ApiTemplateItem { id: number; name: string; category: string }
|
||||
const result = await executeServerAction<{ data: ApiTemplateItem[] }>({
|
||||
url: `${API_URL}/api/v1/document-templates?is_active=1&per_page=100`,
|
||||
url: buildApiUrl('/api/v1/document-templates', { is_active: 1, per_page: 100 }),
|
||||
errorMessage: '문서 양식 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return { success: false, error: result.error };
|
||||
@@ -621,7 +618,7 @@ export async function getProcessSteps(processId: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction<ApiProcessStep[]>({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps`,
|
||||
url: buildApiUrl(`/api/v1/processes/${processId}/steps`),
|
||||
errorMessage: '공정 단계 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -637,7 +634,7 @@ export async function getProcessStepById(processId: string, stepId: string): Pro
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
url: buildApiUrl(`/api/v1/processes/${processId}/steps/${stepId}`),
|
||||
transform: (d: ApiProcessStep) => transformStepApiToFrontend(d),
|
||||
errorMessage: '공정 단계 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -652,7 +649,7 @@ export async function createProcessStep(
|
||||
data: Omit<ProcessStep, 'id'>
|
||||
): Promise<{ success: boolean; data?: ProcessStep; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps`,
|
||||
url: buildApiUrl(`/api/v1/processes/${processId}/steps`),
|
||||
method: 'POST',
|
||||
body: {
|
||||
step_name: data.stepName,
|
||||
@@ -689,7 +686,7 @@ export async function updateProcessStep(
|
||||
if (data.completionType !== undefined) apiData.completion_type = data.completionType || null;
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
url: buildApiUrl(`/api/v1/processes/${processId}/steps/${stepId}`),
|
||||
method: 'PUT',
|
||||
body: apiData,
|
||||
transform: (d: ApiProcessStep) => transformStepApiToFrontend(d),
|
||||
@@ -706,7 +703,7 @@ export async function deleteProcessStep(
|
||||
stepId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
url: buildApiUrl(`/api/v1/processes/${processId}/steps/${stepId}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '공정 단계 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -721,7 +718,7 @@ export async function reorderProcessSteps(
|
||||
steps: { id: string; order: number }[]
|
||||
): Promise<{ success: boolean; data?: ProcessStep[]; error?: string }> {
|
||||
const result = await executeServerAction<ApiProcessStep[]>({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps/reorder`,
|
||||
url: buildApiUrl(`/api/v1/processes/${processId}/steps/reorder`),
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
items: steps.map((s) => ({
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { WorkOrder, WorkerStatus, DashboardStats, ProcessOption } from './types';
|
||||
|
||||
// ===== API 타입 =====
|
||||
@@ -75,8 +76,6 @@ function transformToProductionFormat(api: WorkOrderApiItem): WorkOrder {
|
||||
};
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 공정 옵션 목록 조회 =====
|
||||
export async function getProcessOptions(): Promise<{
|
||||
success: boolean;
|
||||
@@ -92,7 +91,7 @@ export async function getProcessOptions(): Promise<{
|
||||
}
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/options`,
|
||||
url: buildApiUrl('/api/v1/processes/options'),
|
||||
transform: (data: ProcessApiData[]) =>
|
||||
(data || []).map(p => ({
|
||||
id: p.id,
|
||||
@@ -126,11 +125,11 @@ export async function getDashboardData(processCode?: string): Promise<{
|
||||
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
|
||||
};
|
||||
|
||||
const params = new URLSearchParams({ per_page: '100' });
|
||||
if (processCode && processCode !== 'all') params.set('process_code', processCode);
|
||||
|
||||
const result = await executeServerAction<{ data: WorkOrderApiItem[] }>({
|
||||
url: `${API_URL}/api/v1/work-orders?${params.toString()}`,
|
||||
url: buildApiUrl('/api/v1/work-orders', {
|
||||
per_page: 100,
|
||||
process_code: processCode && processCode !== 'all' ? processCode : undefined,
|
||||
}),
|
||||
errorMessage: '데이터 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
WorkOrder,
|
||||
WorkOrderStats,
|
||||
@@ -66,30 +67,18 @@ export async function getWorkOrders(params?: {
|
||||
};
|
||||
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.processId && params.processId !== 'all') {
|
||||
// 'none': 공정 미지정 필터 (process_id IS NULL)
|
||||
searchParams.set('process_id', String(params.processId));
|
||||
}
|
||||
if (params?.processType) {
|
||||
searchParams.set('process_type', params.processType);
|
||||
}
|
||||
if (params?.priority && params.priority !== 'all') {
|
||||
searchParams.set('priority', params.priority);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const url = buildApiUrl('/api/v1/work-orders', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
// 'none': 공정 미지정 필터 (process_id IS NULL), 'all': 제외
|
||||
process_id: params?.processId && params.processId !== 'all' ? String(params.processId) : undefined,
|
||||
process_type: params?.processType,
|
||||
priority: params?.priority && params.priority !== 'all' ? params.priority : undefined,
|
||||
search: params?.search,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
});
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
@@ -145,7 +134,7 @@ export async function getWorkOrderStats(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/stats`;
|
||||
const url = buildApiUrl('/api/v1/work-orders/stats');
|
||||
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
@@ -188,7 +177,7 @@ export async function getWorkOrderById(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`;
|
||||
const url = buildApiUrl(`/api/v1/work-orders/${id}`);
|
||||
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
@@ -248,7 +237,7 @@ export async function createWorkOrder(
|
||||
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders`,
|
||||
buildApiUrl('/api/v1/work-orders'),
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
@@ -289,7 +278,7 @@ export async function updateWorkOrder(
|
||||
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
|
||||
buildApiUrl(`/api/v1/work-orders/${id}`),
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
@@ -324,7 +313,7 @@ export async function updateWorkOrder(
|
||||
export async function deleteWorkOrder(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
|
||||
buildApiUrl(`/api/v1/work-orders/${id}`),
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
@@ -357,7 +346,7 @@ export async function updateWorkOrderStatus(
|
||||
try {
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`,
|
||||
buildApiUrl(`/api/v1/work-orders/${id}/status`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status }),
|
||||
@@ -402,7 +391,7 @@ export async function assignWorkOrder(
|
||||
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/assign`,
|
||||
buildApiUrl(`/api/v1/work-orders/${id}/assign`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
@@ -441,7 +430,7 @@ export async function toggleBendingField(
|
||||
try {
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/bending/toggle`,
|
||||
buildApiUrl(`/api/v1/work-orders/${id}/bending/toggle`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ field }),
|
||||
@@ -484,7 +473,7 @@ export async function addWorkOrderIssue(
|
||||
try {
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/issues`,
|
||||
buildApiUrl(`/api/v1/work-orders/${id}/issues`),
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
@@ -523,7 +512,7 @@ export async function resolveWorkOrderIssue(
|
||||
try {
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`,
|
||||
buildApiUrl(`/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`),
|
||||
{ method: 'PATCH' }
|
||||
);
|
||||
|
||||
@@ -569,7 +558,7 @@ export async function updateWorkOrderItemStatus(
|
||||
try {
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/status`,
|
||||
buildApiUrl(`/api/v1/work-orders/${workOrderId}/items/${itemId}/status`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status }),
|
||||
@@ -646,7 +635,7 @@ export async function getInspectionReport(
|
||||
): Promise<{ success: boolean; data?: InspectionReportData; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-report`,
|
||||
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-report`),
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
@@ -681,7 +670,7 @@ export async function saveInspectionData(
|
||||
try {
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection`,
|
||||
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection`),
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@@ -720,7 +709,7 @@ export async function getInspectionTemplate(
|
||||
): Promise<{ success: boolean; data?: InspectionTemplateData; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-template`,
|
||||
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-template`),
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
@@ -757,7 +746,7 @@ export async function saveInspectionDocument(
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-document`,
|
||||
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-document`),
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
@@ -791,9 +780,8 @@ export async function resolveInspectionDocument(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const query = params?.step_id ? `?step_id=${params.step_id}` : '';
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-resolve${query}`,
|
||||
buildApiUrl(`/api/v1/work-orders/${workOrderId}/inspection-resolve`, { step_id: params?.step_id }),
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
@@ -835,16 +823,12 @@ export async function getSalesOrdersForWorkOrder(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
// 작업지시 생성 가능한 상태만 조회 (예: 회계확인 완료)
|
||||
searchParams.set('for_work_order', '1');
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const url = buildApiUrl('/api/v1/orders', {
|
||||
for_work_order: '1',
|
||||
q: params?.q,
|
||||
status: params?.status,
|
||||
});
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
@@ -922,7 +906,7 @@ export async function getDepartmentsWithUsers(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/tree?with_users=1`;
|
||||
const url = buildApiUrl('/api/v1/departments/tree', { with_users: 1 });
|
||||
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
@@ -993,7 +977,7 @@ export async function getProcessOptions(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/options`;
|
||||
const url = buildApiUrl('/api/v1/processes/options');
|
||||
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
@@ -1058,7 +1042,7 @@ export async function getMaterialInputLots(workOrderId: string): Promise<{
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/material-input-lots`,
|
||||
buildApiUrl(`/api/v1/work-orders/${workOrderId}/material-input-lots`),
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { ProcessType } from '../WorkOrders/types';
|
||||
import type {
|
||||
WorkResult,
|
||||
@@ -37,8 +38,6 @@ interface PaginationMeta {
|
||||
total: number;
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 작업실적 목록 조회 =====
|
||||
export async function getWorkResults(params?: {
|
||||
page?: number; size?: number; q?: string;
|
||||
@@ -47,21 +46,19 @@ export async function getWorkResults(params?: {
|
||||
isInspected?: boolean; isPackaged?: boolean;
|
||||
}): Promise<{ success: boolean; data: WorkResult[]; pagination: PaginationMeta; error?: string }> {
|
||||
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.processType && params.processType !== 'all') searchParams.set('process_type', params.processType);
|
||||
if (params?.workOrderId) searchParams.set('work_order_id', String(params.workOrderId));
|
||||
if (params?.workerId) searchParams.set('worker_id', String(params.workerId));
|
||||
if (params?.workDateFrom) searchParams.set('work_date_from', params.workDateFrom);
|
||||
if (params?.workDateTo) searchParams.set('work_date_to', params.workDateTo);
|
||||
if (params?.isInspected !== undefined) searchParams.set('is_inspected', params.isInspected ? '1' : '0');
|
||||
if (params?.isPackaged !== undefined) searchParams.set('is_packaged', params.isPackaged ? '1' : '0');
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<WorkResultApiPaginatedResponse>({
|
||||
url: `${API_URL}/api/v1/work-results${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/work-results', {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
q: params?.q,
|
||||
process_type: params?.processType && params.processType !== 'all' ? params.processType : undefined,
|
||||
work_order_id: params?.workOrderId,
|
||||
worker_id: params?.workerId,
|
||||
work_date_from: params?.workDateFrom,
|
||||
work_date_to: params?.workDateTo,
|
||||
is_inspected: params?.isInspected !== undefined ? (params.isInspected ? '1' : '0') : undefined,
|
||||
is_packaged: params?.isPackaged !== undefined ? (params.isPackaged ? '1' : '0') : undefined,
|
||||
}),
|
||||
errorMessage: '작업실적 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -85,14 +82,12 @@ export async function getWorkResults(params?: {
|
||||
export async function getWorkResultStats(params?: {
|
||||
workDateFrom?: string; workDateTo?: string; processType?: ProcessType | 'all';
|
||||
}): Promise<{ success: boolean; data?: WorkResultStats; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.workDateFrom) searchParams.set('work_date_from', params.workDateFrom);
|
||||
if (params?.workDateTo) searchParams.set('work_date_to', params.workDateTo);
|
||||
if (params?.processType && params.processType !== 'all') searchParams.set('process_type', params.processType);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/work-results/stats${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/work-results/stats', {
|
||||
work_date_from: params?.workDateFrom,
|
||||
work_date_to: params?.workDateTo,
|
||||
process_type: params?.processType && params.processType !== 'all' ? params.processType : undefined,
|
||||
}),
|
||||
transform: (data: WorkResultStatsApi) => transformStatsApiToFrontend(data),
|
||||
errorMessage: '통계 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -104,7 +99,7 @@ export async function getWorkResultById(id: string): Promise<{
|
||||
success: boolean; data?: WorkResult; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/work-results/${id}`,
|
||||
url: buildApiUrl(`/api/v1/work-results/${id}`),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '작업실적 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -131,7 +126,7 @@ export async function createWorkResult(data: {
|
||||
if (data.memo) apiData.memo = data.memo;
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/work-results`,
|
||||
url: buildApiUrl('/api/v1/work-results'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: transformApiToFrontend,
|
||||
@@ -147,7 +142,7 @@ export async function updateWorkResult(
|
||||
): Promise<{ success: boolean; data?: WorkResult; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/work-results/${id}`,
|
||||
url: buildApiUrl(`/api/v1/work-results/${id}`),
|
||||
method: 'PUT',
|
||||
body: apiData,
|
||||
transform: transformApiToFrontend,
|
||||
@@ -159,7 +154,7 @@ export async function updateWorkResult(
|
||||
// ===== 작업실적 삭제 =====
|
||||
export async function deleteWorkResult(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/work-results/${id}`,
|
||||
url: buildApiUrl(`/api/v1/work-results/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '작업실적 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -171,7 +166,7 @@ export async function toggleInspection(id: string): Promise<{
|
||||
success: boolean; data?: WorkResult; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/work-results/${id}/inspection`,
|
||||
url: buildApiUrl(`/api/v1/work-results/${id}/inspection`),
|
||||
method: 'PATCH',
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '검사 상태 변경에 실패했습니다.',
|
||||
@@ -184,7 +179,7 @@ export async function togglePackaging(id: string): Promise<{
|
||||
success: boolean; data?: WorkResult; error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/work-results/${id}/packaging`,
|
||||
url: buildApiUrl(`/api/v1/work-results/${id}/packaging`),
|
||||
method: 'PATCH',
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '포장 상태 변경에 실패했습니다.',
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
ProductInspection,
|
||||
InspectionStats,
|
||||
@@ -286,8 +287,6 @@ function transformFormToApi(data: InspectionFormData): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
const API_BASE = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections`;
|
||||
|
||||
// ===== 제품검사 목록 조회 =====
|
||||
|
||||
export async function getInspections(params?: {
|
||||
@@ -306,19 +305,15 @@ export async function getInspections(params?: {
|
||||
}> {
|
||||
const defaultPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('per_page', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.status && params.status !== '전체') {
|
||||
searchParams.set('status', mapFrontendStatus(params.status));
|
||||
}
|
||||
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
|
||||
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<PaginatedResponse>({
|
||||
url: `${API_BASE}${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/inspections', {
|
||||
page: params?.page,
|
||||
per_page: params?.size,
|
||||
q: params?.q,
|
||||
status: params?.status && params.status !== '전체' ? mapFrontendStatus(params.status) : undefined,
|
||||
date_from: params?.dateFrom,
|
||||
date_to: params?.dateTo,
|
||||
}),
|
||||
errorMessage: '제품검사 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -373,13 +368,11 @@ export async function getInspectionStats(params?: {
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
|
||||
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<InspectionStatsApi>({
|
||||
url: `${API_BASE}/stats${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/inspections/stats', {
|
||||
date_from: params?.dateFrom,
|
||||
date_to: params?.dateTo,
|
||||
}),
|
||||
errorMessage: '제품검사 통계 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -412,17 +405,13 @@ export async function getInspectionCalendar(params?: {
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.inspector) searchParams.set('inspector', params.inspector);
|
||||
if (params?.status && params.status !== '전체') {
|
||||
searchParams.set('status', mapFrontendStatus(params.status));
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<CalendarItemApi[]>({
|
||||
url: `${API_BASE}/calendar${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/inspections/calendar', {
|
||||
year: params?.year,
|
||||
month: params?.month,
|
||||
inspector: params?.inspector,
|
||||
status: params?.status && params.status !== '전체' ? mapFrontendStatus(params.status) : undefined,
|
||||
}),
|
||||
errorMessage: '캘린더 스케줄 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -454,7 +443,7 @@ export async function getInspectionById(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<ProductInspectionApi>({
|
||||
url: `${API_BASE}/${id}`,
|
||||
url: buildApiUrl(`/api/v1/inspections/${id}`),
|
||||
errorMessage: '제품검사 상세 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -482,7 +471,7 @@ export async function createInspection(data: InspectionFormData): Promise<{
|
||||
}> {
|
||||
const apiData = transformFormToApi(data);
|
||||
const result = await executeServerAction<ProductInspectionApi>({
|
||||
url: API_BASE,
|
||||
url: buildApiUrl('/api/v1/inspections'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
errorMessage: '제품검사 등록에 실패했습니다.',
|
||||
@@ -569,7 +558,7 @@ export async function updateInspection(
|
||||
}
|
||||
|
||||
const result = await executeServerAction<ProductInspectionApi>({
|
||||
url: `${API_BASE}/${id}`,
|
||||
url: buildApiUrl(`/api/v1/inspections/${id}`),
|
||||
method: 'PUT',
|
||||
body: apiData,
|
||||
errorMessage: '제품검사 수정에 실패했습니다.',
|
||||
@@ -589,7 +578,7 @@ export async function deleteInspection(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_BASE}/${id}`,
|
||||
url: buildApiUrl(`/api/v1/inspections/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '제품검사 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -614,7 +603,7 @@ export async function completeInspection(
|
||||
}
|
||||
|
||||
const result = await executeServerAction<ProductInspectionApi>({
|
||||
url: `${API_BASE}/${id}/complete`,
|
||||
url: buildApiUrl(`/api/v1/inspections/${id}/complete`),
|
||||
method: 'PATCH',
|
||||
body: apiData,
|
||||
errorMessage: '검사 완료 처리에 실패했습니다.',
|
||||
@@ -636,12 +625,8 @@ export async function getOrderSelectList(params?: {
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<OrderSelectItemApi[]>({
|
||||
url: `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/select${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/orders/select', { q: params?.q }),
|
||||
errorMessage: '수주 선택 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
*/
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
PerformanceReport,
|
||||
PerformanceReportStats,
|
||||
@@ -38,8 +39,6 @@ interface PaginationMeta {
|
||||
total: number;
|
||||
}
|
||||
|
||||
const API_BASE = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/performance-reports`;
|
||||
|
||||
// ===== 분기별 실적신고 목록 조회 =====
|
||||
|
||||
export async function getPerformanceReports(params?: {
|
||||
@@ -57,19 +56,15 @@ export async function getPerformanceReports(params?: {
|
||||
}> {
|
||||
const defaultPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('per_page', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.quarter && params.quarter !== '전체') {
|
||||
searchParams.set('quarter', params.quarter);
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
interface ApiListData { items?: PerformanceReport[]; current_page?: number; last_page?: number; per_page?: number; total?: number }
|
||||
const result = await executeServerAction<ApiListData>({
|
||||
url: `${API_BASE}${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/performance-reports', {
|
||||
page: params?.page,
|
||||
per_page: params?.size,
|
||||
q: params?.q,
|
||||
year: params?.year,
|
||||
quarter: params?.quarter && params.quarter !== '전체' ? params.quarter : undefined,
|
||||
}),
|
||||
errorMessage: '실적신고 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -120,15 +115,11 @@ export async function getPerformanceReportStats(params?: {
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.quarter && params.quarter !== '전체') {
|
||||
searchParams.set('quarter', params.quarter);
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<PerformanceReportStats>({
|
||||
url: `${API_BASE}/stats${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/performance-reports/stats', {
|
||||
year: params?.year,
|
||||
quarter: params?.quarter && params.quarter !== '전체' ? params.quarter : undefined,
|
||||
}),
|
||||
errorMessage: '실적신고 통계 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -154,15 +145,13 @@ export async function getMissedReports(params?: {
|
||||
}> {
|
||||
const defaultPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('per_page', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
interface ApiMissedData { items?: MissedReport[]; current_page?: number; last_page?: number; per_page?: number; total?: number }
|
||||
const result = await executeServerAction<ApiMissedData>({
|
||||
url: `${API_BASE}/missed${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/performance-reports/missed', {
|
||||
page: params?.page,
|
||||
per_page: params?.size,
|
||||
q: params?.q,
|
||||
}),
|
||||
errorMessage: '누락체크 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -208,7 +197,7 @@ export async function confirmReports(ids: string[]): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_BASE}/confirm`,
|
||||
url: buildApiUrl('/api/v1/performance-reports/confirm'),
|
||||
method: 'PATCH',
|
||||
body: { ids },
|
||||
errorMessage: '확정 처리에 실패했습니다.',
|
||||
@@ -225,7 +214,7 @@ export async function unconfirmReports(ids: string[]): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_BASE}/unconfirm`,
|
||||
url: buildApiUrl('/api/v1/performance-reports/unconfirm'),
|
||||
method: 'PATCH',
|
||||
body: { ids },
|
||||
errorMessage: '확정 해제에 실패했습니다.',
|
||||
@@ -242,7 +231,7 @@ export async function distributeReports(ids: string[]): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_BASE}/distribute`,
|
||||
url: buildApiUrl('/api/v1/performance-reports/distribute'),
|
||||
method: 'POST',
|
||||
body: { ids },
|
||||
errorMessage: '배포에 실패했습니다.',
|
||||
@@ -259,7 +248,7 @@ export async function updateMemo(ids: string[], memo: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_BASE}/memo`,
|
||||
url: buildApiUrl('/api/v1/performance-reports/memo'),
|
||||
method: 'PATCH',
|
||||
body: { ids, memo },
|
||||
errorMessage: '메모 저장에 실패했습니다.',
|
||||
|
||||
@@ -57,7 +57,7 @@ import { formatAmount, formatAmountManwon } from '@/utils/formatAmount';
|
||||
import type { Quote, QuoteFilterType } from './types';
|
||||
import { PRODUCT_CATEGORY_LABELS } from './types';
|
||||
import { getQuotes, deleteQuote, bulkDeleteQuotes } from './actions';
|
||||
import type { PaginationMeta } from './actions';
|
||||
import type { PaginationMeta } from '@/lib/api/types';
|
||||
|
||||
// ===== Props 타입 =====
|
||||
interface QuoteManagementClientProps {
|
||||
|
||||
@@ -23,68 +23,35 @@
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
Quote,
|
||||
QuoteApiData,
|
||||
QuoteApiPaginatedResponse,
|
||||
QuoteListParams,
|
||||
QuoteStatus,
|
||||
ProductCategory,
|
||||
BomCalculationResult,
|
||||
} from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
export interface PaginationMeta {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 견적 목록 조회 =====
|
||||
export async function getQuotes(params?: QuoteListParams): Promise<{
|
||||
success: boolean;
|
||||
data: Quote[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('size', String(params.perPage));
|
||||
if (params?.search) searchParams.set('q', params.search);
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
if (params?.productCategory) searchParams.set('product_category', params.productCategory);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
|
||||
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortOrder) searchParams.set('sort_order', params.sortOrder);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<QuoteApiPaginatedResponse>({
|
||||
url: `${API_URL}/api/v1/quotes${queryString ? `?${queryString}` : ''}`,
|
||||
export async function getQuotes(params?: QuoteListParams) {
|
||||
return executePaginatedAction<QuoteApiData, Quote>({
|
||||
url: buildApiUrl('/api/v1/quotes', {
|
||||
page: params?.page,
|
||||
size: params?.perPage,
|
||||
q: params?.search,
|
||||
status: params?.status,
|
||||
product_category: params?.productCategory,
|
||||
client_id: params?.clientId,
|
||||
date_from: params?.dateFrom,
|
||||
date_to: params?.dateTo,
|
||||
sort_by: params?.sortBy,
|
||||
sort_order: params?.sortOrder,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '견적 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (result.__authError) return { success: false, data: [], pagination: emptyPagination, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, data: [], pagination: emptyPagination, error: result.error };
|
||||
|
||||
const paginatedData = result.data;
|
||||
return {
|
||||
success: true,
|
||||
data: (paginatedData.data || []).map(transformApiToFrontend),
|
||||
pagination: {
|
||||
currentPage: paginatedData.current_page,
|
||||
lastPage: paginatedData.last_page,
|
||||
perPage: paginatedData.per_page,
|
||||
total: paginatedData.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 견적 상세 조회 =====
|
||||
@@ -95,7 +62,7 @@ export async function getQuoteById(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}`),
|
||||
transform: (data: QuoteApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '견적 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -108,7 +75,7 @@ export async function createQuote(
|
||||
data: Record<string, unknown>
|
||||
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes`,
|
||||
url: buildApiUrl('/api/v1/quotes'),
|
||||
method: 'POST',
|
||||
body: data,
|
||||
transform: (d: QuoteApiData) => transformApiToFrontend(d),
|
||||
@@ -124,7 +91,7 @@ export async function updateQuote(
|
||||
data: Record<string, unknown>
|
||||
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}`),
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: (d: QuoteApiData) => transformApiToFrontend(d),
|
||||
@@ -137,7 +104,7 @@ export async function updateQuote(
|
||||
// ===== 견적 삭제 =====
|
||||
export async function deleteQuote(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '견적 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -148,7 +115,7 @@ export async function deleteQuote(id: string): Promise<{ success: boolean; error
|
||||
// ===== 견적 일괄 삭제 =====
|
||||
export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/bulk`,
|
||||
url: buildApiUrl('/api/v1/quotes/bulk'),
|
||||
method: 'DELETE',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
errorMessage: '견적 일괄 삭제에 실패했습니다.',
|
||||
@@ -165,7 +132,7 @@ export async function finalizeQuote(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/finalize`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/finalize`),
|
||||
method: 'POST',
|
||||
transform: (d: QuoteApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '견적 확정에 실패했습니다.',
|
||||
@@ -182,7 +149,7 @@ export async function cancelFinalizeQuote(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/cancel-finalize`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/cancel-finalize`),
|
||||
method: 'POST',
|
||||
transform: (d: QuoteApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '견적 확정 취소에 실패했습니다.',
|
||||
@@ -204,7 +171,7 @@ export async function convertQuoteToOrder(id: string): Promise<{
|
||||
order?: { id: number };
|
||||
}
|
||||
const result = await executeServerAction<ConvertResponse>({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/convert`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/convert`),
|
||||
method: 'POST',
|
||||
errorMessage: '수주 전환에 실패했습니다.',
|
||||
});
|
||||
@@ -227,7 +194,7 @@ export async function getQuoteNumberPreview(): Promise<{
|
||||
}> {
|
||||
interface PreviewResponse { quote_number?: string }
|
||||
const result = await executeServerAction<PreviewResponse | string>({
|
||||
url: `${API_URL}/api/v1/quotes/number/preview`,
|
||||
url: buildApiUrl('/api/v1/quotes/number/preview'),
|
||||
errorMessage: '견적번호 미리보기에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -246,7 +213,7 @@ export async function generateQuotePdf(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/pdf`;
|
||||
const url = buildApiUrl(`/api/v1/quotes/${id}/pdf`);
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
@@ -296,7 +263,7 @@ export async function sendQuoteEmail(
|
||||
emailData: { email: string; subject?: string; message?: string }
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/send/email`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/send/email`),
|
||||
method: 'POST',
|
||||
body: emailData,
|
||||
errorMessage: '이메일 발송에 실패했습니다.',
|
||||
@@ -311,7 +278,7 @@ export async function sendQuoteKakao(
|
||||
kakaoData: { phone: string; templateId?: string }
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/send/kakao`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/send/kakao`),
|
||||
method: 'POST',
|
||||
body: kakaoData,
|
||||
errorMessage: '카카오 발송에 실패했습니다.',
|
||||
@@ -338,15 +305,14 @@ export async function getFinishedGoods(category?: string): Promise<{
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('item_type', 'FG');
|
||||
searchParams.set('has_bom', '1');
|
||||
if (category) searchParams.set('item_category', category);
|
||||
searchParams.set('size', '5000');
|
||||
|
||||
interface FGApiResponse { data?: Record<string, unknown>[] }
|
||||
const result = await executeServerAction<FGApiResponse | Record<string, unknown>[]>({
|
||||
url: `${API_URL}/api/v1/items?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/items', {
|
||||
item_type: 'FG',
|
||||
has_bom: '1',
|
||||
item_category: category,
|
||||
size: '5000',
|
||||
}),
|
||||
errorMessage: '완제품 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -414,7 +380,7 @@ export async function calculateBomBulk(items: BomCalculateItem[], debug: boolean
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<BomBulkResponse>({
|
||||
url: `${API_URL}/api/v1/quotes/calculate/bom/bulk`,
|
||||
url: buildApiUrl('/api/v1/quotes/calculate/bom/bulk'),
|
||||
method: 'POST',
|
||||
body: { items, debug },
|
||||
errorMessage: 'BOM 계산에 실패했습니다.',
|
||||
@@ -436,7 +402,7 @@ export async function getItemPrices(itemCodes: string[]): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<Record<string, ItemPriceResult>>({
|
||||
url: `${API_URL}/api/v1/quotes/items/prices`,
|
||||
url: buildApiUrl('/api/v1/quotes/items/prices'),
|
||||
method: 'POST',
|
||||
body: { item_codes: itemCodes },
|
||||
errorMessage: '단가 조회에 실패했습니다.',
|
||||
@@ -534,7 +500,7 @@ export async function getQuoteReferenceData(): Promise<{
|
||||
}> {
|
||||
interface RefApiData { site_names?: string[]; location_codes?: string[] }
|
||||
const result = await executeServerAction<RefApiData>({
|
||||
url: `${API_URL}/api/v1/quotes/reference-data`,
|
||||
url: buildApiUrl('/api/v1/quotes/reference-data'),
|
||||
errorMessage: '참조 데이터 조회에 실패했습니다.',
|
||||
});
|
||||
const empty: QuoteReferenceData = { siteNames: [], locationCodes: [] };
|
||||
@@ -566,7 +532,7 @@ export async function getItemCategoryTree(): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<ItemCategoryNode[]>({
|
||||
url: `${API_URL}/api/v1/categories/tree?code_group=item_category&only_active=true`,
|
||||
url: buildApiUrl('/api/v1/categories/tree', { code_group: 'item_category', only_active: true }),
|
||||
errorMessage: '카테고리 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, data: [], __authError: true };
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { ComprehensiveAnalysisData } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface TodayIssueItemApi {
|
||||
id: string;
|
||||
@@ -105,12 +104,10 @@ function transformAnalysisData(data: ComprehensiveAnalysisDataApi): Comprehensiv
|
||||
export async function getComprehensiveAnalysis(params?: {
|
||||
date?: string;
|
||||
}): Promise<ActionResult<ComprehensiveAnalysisData>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/comprehensive-analysis${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/comprehensive-analysis', {
|
||||
date: params?.date,
|
||||
}),
|
||||
transform: (data: ComprehensiveAnalysisDataApi) => transformAnalysisData(data),
|
||||
errorMessage: '종합 분석 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -119,7 +116,7 @@ export async function getComprehensiveAnalysis(params?: {
|
||||
// ===== 이슈 승인 =====
|
||||
export async function approveIssue(issueId: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${issueId}/approve`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${issueId}/approve`),
|
||||
method: 'POST',
|
||||
errorMessage: '승인에 실패했습니다.',
|
||||
});
|
||||
@@ -128,7 +125,7 @@ export async function approveIssue(issueId: string): Promise<ActionResult> {
|
||||
// ===== 이슈 반려 =====
|
||||
export async function rejectIssue(issueId: string, reason?: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${issueId}/reject`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${issueId}/reject`),
|
||||
method: 'POST',
|
||||
body: { comment: reason },
|
||||
errorMessage: '반려에 실패했습니다.',
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import type { Account, AccountFormData, AccountStatus } from './types';
|
||||
import { BANK_LABELS } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface BankAccountApiData {
|
||||
id: number;
|
||||
@@ -61,14 +60,12 @@ export async function getBankAccounts(params?: {
|
||||
success: boolean; data?: Account[]; meta?: { currentPage: number; lastPage: number; perPage: number; total: number };
|
||||
error?: string; __authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', params.page.toString());
|
||||
if (params?.perPage) searchParams.set('per_page', params.perPage.toString());
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-accounts${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/bank-accounts', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
search: params?.search,
|
||||
}),
|
||||
transform: (data: BankAccountPaginatedResponse) => ({
|
||||
accounts: (data?.data || []).map(transformApiToFrontend),
|
||||
meta: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
||||
@@ -81,7 +78,7 @@ export async function getBankAccounts(params?: {
|
||||
// ===== 계좌 상세 조회 =====
|
||||
export async function getBankAccount(id: number): Promise<ActionResult<Account>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-accounts/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bank-accounts/${id}`),
|
||||
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '계좌 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -90,7 +87,7 @@ export async function getBankAccount(id: number): Promise<ActionResult<Account>>
|
||||
// ===== 계좌 생성 =====
|
||||
export async function createBankAccount(data: AccountFormData): Promise<ActionResult<Account>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-accounts`,
|
||||
url: buildApiUrl('/api/v1/bank-accounts'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: BankAccountApiData) => transformApiToFrontend(d),
|
||||
@@ -101,7 +98,7 @@ export async function createBankAccount(data: AccountFormData): Promise<ActionRe
|
||||
// ===== 계좌 수정 =====
|
||||
export async function updateBankAccount(id: number, data: Partial<AccountFormData>): Promise<ActionResult<Account>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-accounts/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bank-accounts/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: BankAccountApiData) => transformApiToFrontend(d),
|
||||
@@ -112,7 +109,7 @@ export async function updateBankAccount(id: number, data: Partial<AccountFormDat
|
||||
// ===== 계좌 삭제 =====
|
||||
export async function deleteBankAccount(id: number): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-accounts/${id}`,
|
||||
url: buildApiUrl(`/api/v1/bank-accounts/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '계좌 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -121,7 +118,7 @@ export async function deleteBankAccount(id: number): Promise<ActionResult> {
|
||||
// ===== 계좌 상태 토글 =====
|
||||
export async function toggleBankAccountStatus(id: number): Promise<ActionResult<Account>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-accounts/${id}/toggle`,
|
||||
url: buildApiUrl(`/api/v1/bank-accounts/${id}/toggle`),
|
||||
method: 'PATCH',
|
||||
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '상태 변경에 실패했습니다.',
|
||||
@@ -131,7 +128,7 @@ export async function toggleBankAccountStatus(id: number): Promise<ActionResult<
|
||||
// ===== 대표 계좌 설정 =====
|
||||
export async function setPrimaryBankAccount(id: number): Promise<ActionResult<Account>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-accounts/${id}/set-primary`,
|
||||
url: buildApiUrl(`/api/v1/bank-accounts/${id}/set-primary`),
|
||||
method: 'PATCH',
|
||||
transform: (data: BankAccountApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '대표 계좌 설정에 실패했습니다.',
|
||||
@@ -158,4 +155,4 @@ export async function deleteBankAccounts(ids: number[]): Promise<{
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaymentApiData, PaymentHistory } from './types';
|
||||
import { transformApiToFrontend } from './utils';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
type PaymentPaginatedResponse = PaginatedApiResponse<PaymentApiData>;
|
||||
|
||||
interface PaymentStatementApiData {
|
||||
statement_no: string;
|
||||
issued_at: string;
|
||||
@@ -37,40 +34,28 @@ interface StatementData {
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
|
||||
// ===== 결제 목록 조회 =====
|
||||
export async function getPayments(params?: {
|
||||
page?: number; perPage?: number; status?: string; startDate?: string; endDate?: string; search?: string;
|
||||
}): Promise<{
|
||||
success: boolean; data: PaymentHistory[]; pagination: FrontendPagination;
|
||||
error?: string; __authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.append('page', String(params.page));
|
||||
if (params?.perPage) searchParams.append('per_page', String(params.perPage));
|
||||
if (params?.status) searchParams.append('status', params.status);
|
||||
if (params?.startDate) searchParams.append('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.append('end_date', params.endDate);
|
||||
if (params?.search) searchParams.append('search', params.search);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/payments${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: PaymentPaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformApiToFrontend),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, total: data?.total || 0 },
|
||||
}) {
|
||||
return executePaginatedAction<PaymentApiData, PaymentHistory>({
|
||||
url: buildApiUrl('/api/v1/payments', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
status: params?.status,
|
||||
start_date: params?.startDate,
|
||||
end_date: params?.endDate,
|
||||
search: params?.search,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '결제 내역을 불러오는데 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error, __authError: result.__authError };
|
||||
}
|
||||
|
||||
// ===== 결제 명세서 조회 =====
|
||||
export async function getPaymentStatement(id: string): Promise<ActionResult<StatementData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/payments/${id}/statement`,
|
||||
url: buildApiUrl(`/api/v1/payments/${id}/statement`),
|
||||
transform: (data: PaymentStatementApiData): StatementData => ({
|
||||
statementNo: data.statement_no,
|
||||
issuedAt: data.issued_at,
|
||||
|
||||
@@ -2,32 +2,29 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import type { Role, RoleStats, PermissionMatrix, MenuTreeItem, PaginatedResponse } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ========== Role CRUD ==========
|
||||
|
||||
export async function fetchRoles(params?: {
|
||||
page?: number; size?: number; q?: string; is_hidden?: boolean;
|
||||
}): Promise<ActionResult<PaginatedResponse<Role>>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', params.page.toString());
|
||||
if (params?.size) searchParams.set('per_page', params.size.toString());
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.is_hidden !== undefined) searchParams.set('is_hidden', params.is_hidden.toString());
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return executeServerAction<PaginatedResponse<Role>>({
|
||||
url: `${API_URL}/api/v1/roles${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/roles', {
|
||||
page: params?.page,
|
||||
per_page: params?.size,
|
||||
q: params?.q,
|
||||
is_hidden: params?.is_hidden,
|
||||
}),
|
||||
errorMessage: '역할 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchRole(id: number): Promise<ActionResult<Role>> {
|
||||
return executeServerAction<Role>({
|
||||
url: `${API_URL}/api/v1/roles/${id}`,
|
||||
url: buildApiUrl(`/api/v1/roles/${id}`),
|
||||
errorMessage: '역할 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -36,7 +33,7 @@ export async function createRole(data: {
|
||||
name: string; description?: string; is_hidden?: boolean;
|
||||
}): Promise<ActionResult<Role>> {
|
||||
const result = await executeServerAction<Role>({
|
||||
url: `${API_URL}/api/v1/roles`,
|
||||
url: buildApiUrl('/api/v1/roles'),
|
||||
method: 'POST',
|
||||
body: data,
|
||||
errorMessage: '역할 생성에 실패했습니다.',
|
||||
@@ -49,7 +46,7 @@ export async function updateRole(id: number, data: {
|
||||
name?: string; description?: string; is_hidden?: boolean;
|
||||
}): Promise<ActionResult<Role>> {
|
||||
const result = await executeServerAction<Role>({
|
||||
url: `${API_URL}/api/v1/roles/${id}`,
|
||||
url: buildApiUrl(`/api/v1/roles/${id}`),
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
errorMessage: '역할 수정에 실패했습니다.',
|
||||
@@ -63,7 +60,7 @@ export async function updateRole(id: number, data: {
|
||||
|
||||
export async function deleteRole(id: number): Promise<ActionResult> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/roles/${id}`,
|
||||
url: buildApiUrl(`/api/v1/roles/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '역할 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -73,14 +70,14 @@ export async function deleteRole(id: number): Promise<ActionResult> {
|
||||
|
||||
export async function fetchRoleStats(): Promise<ActionResult<RoleStats>> {
|
||||
return executeServerAction<RoleStats>({
|
||||
url: `${API_URL}/api/v1/roles/stats`,
|
||||
url: buildApiUrl('/api/v1/roles/stats'),
|
||||
errorMessage: '역할 통계 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchActiveRoles(): Promise<ActionResult<Role[]>> {
|
||||
return executeServerAction<Role[]>({
|
||||
url: `${API_URL}/api/v1/roles/active`,
|
||||
url: buildApiUrl('/api/v1/roles/active'),
|
||||
errorMessage: '활성 역할 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -91,14 +88,14 @@ export async function fetchPermissionMenus(): Promise<ActionResult<{
|
||||
menus: MenuTreeItem[]; permission_types: string[];
|
||||
}>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/role-permissions/menus`,
|
||||
url: buildApiUrl('/api/v1/role-permissions/menus'),
|
||||
errorMessage: '메뉴 트리 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPermissionMatrix(roleId: number): Promise<ActionResult<PermissionMatrix>> {
|
||||
return executeServerAction<PermissionMatrix>({
|
||||
url: `${API_URL}/api/v1/roles/${roleId}/permissions/matrix`,
|
||||
url: buildApiUrl(`/api/v1/roles/${roleId}/permissions/matrix`),
|
||||
errorMessage: '권한 매트릭스 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -107,7 +104,7 @@ export async function togglePermission(roleId: number, menuId: number, permissio
|
||||
granted: boolean; propagated_to: number[];
|
||||
}>> {
|
||||
const result = await executeServerAction<{ granted: boolean; propagated_to: number[] }>({
|
||||
url: `${API_URL}/api/v1/roles/${roleId}/permissions/toggle`,
|
||||
url: buildApiUrl(`/api/v1/roles/${roleId}/permissions/toggle`),
|
||||
method: 'POST',
|
||||
body: { menu_id: menuId, permission_type: permissionType },
|
||||
errorMessage: '권한 토글에 실패했습니다.',
|
||||
@@ -118,7 +115,7 @@ export async function togglePermission(roleId: number, menuId: number, permissio
|
||||
|
||||
async function rolePermissionAction(roleId: number, action: string, errorMessage: string): Promise<ActionResult<{ count: number }>> {
|
||||
const result = await executeServerAction<{ count: number }>({
|
||||
url: `${API_URL}/api/v1/roles/${roleId}/permissions/${action}`,
|
||||
url: buildApiUrl(`/api/v1/roles/${roleId}/permissions/${action}`),
|
||||
method: 'POST',
|
||||
errorMessage,
|
||||
});
|
||||
@@ -136,4 +133,4 @@ export async function denyAllPermissions(roleId: number): Promise<ActionResult<{
|
||||
|
||||
export async function resetPermissions(roleId: number): Promise<ActionResult<{ count: number }>> {
|
||||
return rolePermissionAction(roleId, 'reset', '권한 초기화에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { Popup, PopupFormData } from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi, type PopupApiData } from './utils';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
@@ -32,15 +31,12 @@ export async function getPopups(params?: {
|
||||
size?: number;
|
||||
status?: string;
|
||||
}): Promise<Popup[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/popups', {
|
||||
page: params?.page,
|
||||
size: params?.size,
|
||||
status: params?.status !== 'all' ? params?.status : undefined,
|
||||
}),
|
||||
transform: (data: PaginatedApiResponse<PopupApiData>) => data.data.map(transformApiToFrontend),
|
||||
errorMessage: '팝업 목록 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -52,7 +48,7 @@ export async function getPopups(params?: {
|
||||
*/
|
||||
export async function getPopupById(id: string): Promise<Popup | null> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups/${id}`,
|
||||
url: buildApiUrl(`/api/v1/popups/${id}`),
|
||||
transform: (data: PopupApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '팝업 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -66,7 +62,7 @@ export async function createPopup(
|
||||
data: PopupFormData
|
||||
): Promise<ActionResult<Popup>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups`,
|
||||
url: buildApiUrl('/api/v1/popups'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: PopupApiData) => transformApiToFrontend(data),
|
||||
@@ -82,7 +78,7 @@ export async function updatePopup(
|
||||
data: PopupFormData
|
||||
): Promise<ActionResult<Popup>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups/${id}`,
|
||||
url: buildApiUrl(`/api/v1/popups/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: PopupApiData) => transformApiToFrontend(data),
|
||||
@@ -95,7 +91,7 @@ export async function updatePopup(
|
||||
*/
|
||||
export async function deletePopup(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/popups/${id}`,
|
||||
url: buildApiUrl(`/api/v1/popups/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '팝업 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user