From cbb38d48b9796b37c7487e65c9a2d8c49a0e88d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Thu, 12 Feb 2026 20:59:59 +0900 Subject: [PATCH] =?UTF-8?q?refactor(WEB):=20=EC=A0=84=EC=B2=B4=20actions.t?= =?UTF-8?q?s=EC=97=90=20=EA=B3=B5=ED=86=B5=20API=20=EC=9C=A0=ED=8B=B8=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildApiUrl / executePaginatedAction 패턴으로 전환 (40+ actions 파일) - 직접 URLSearchParams 조립 → buildApiUrl 유틸 사용 - 수동 페이지네이션 메타 변환 → executePaginatedAction 자동 처리 - HandoverReportDocumentModal, OrderDocumentModal 개선 - 급여관리 SalaryManagement 코드 개선 - CLAUDE.md Server Action 공통 유틸 규칙 정리 Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 53 +++++---- ...025-02-10] frontend-improvement-roadmap.md | 3 +- claudedocs/_index.md | 26 +++- .../accounting/BadDebtCollection/actions.ts | 38 +++--- .../BankTransactionInquiry/actions.ts | 56 ++++----- .../accounting/BillManagement/actions.ts | 78 +++++------- .../CardTransactionInquiry/actions.ts | 64 ++++------ .../accounting/DailyReport/actions.ts | 27 +---- .../accounting/DepositManagement/actions.ts | 58 +++------ .../ExpectedExpenseManagement/actions.ts | 74 +++++------- .../accounting/PurchaseManagement/actions.ts | 50 +++----- .../accounting/ReceivablesStatus/actions.ts | 56 ++++----- .../accounting/SalesManagement/actions.ts | 61 ++++------ .../accounting/VendorLedger/actions.ts | 80 +++++-------- .../accounting/VendorManagement/actions.ts | 26 ++-- .../WithdrawalManagement/actions.ts | 58 +++------ .../approval/ApprovalBox/actions.ts | 31 ++--- .../approval/DocumentCreate/actions.ts | 24 ++-- src/components/approval/DraftBox/actions.ts | 39 +++--- .../approval/ReferenceBox/actions.ts | 28 ++--- .../board/BoardManagement/actions.ts | 33 +++--- src/components/board/DynamicBoard/actions.ts | 36 +++--- src/components/board/actions.ts | 42 +++---- .../contract/modals/ContractDocumentModal.tsx | 6 +- .../modals/HandoverReportDocumentModal.tsx | 29 ++++- .../modals/OrderDocumentModal.tsx | 29 ++++- .../customer-center/shared/actions.ts | 40 +++---- .../hr/AttendanceManagement/actions.ts | 68 +++++------ src/components/hr/CardManagement/actions.ts | 35 +++--- .../hr/DepartmentManagement/actions.ts | 21 ++-- .../hr/EmployeeManagement/actions.ts | 43 ++++--- src/components/hr/SalaryManagement/actions.ts | 70 +++++------ src/components/hr/SalaryManagement/index.tsx | 32 +++-- .../hr/VacationManagement/actions.ts | 109 ++++++++--------- .../material/ReceivingManagement/actions.ts | 106 ++++++----------- .../material/StockStatus/actions.ts | 71 ++++------- src/components/orders/actions.ts | 67 +++++------ .../outbound/ShipmentManagement/actions.ts | 83 +++++-------- src/components/process-management/actions.ts | 69 ++++++----- .../production/ProductionDashboard/actions.ts | 13 +- .../production/WorkOrders/actions.ts | 90 ++++++-------- .../production/WorkResults/actions.ts | 53 ++++----- .../quality/InspectionManagement/actions.ts | 65 ++++------ .../PerformanceReportManagement/actions.ts | 53 ++++----- .../quotes/QuoteManagementClient.tsx | 2 +- src/components/quotes/actions.ts | 112 ++++++------------ src/components/reports/actions.ts | 15 +-- .../settings/AccountManagement/actions.ts | 29 ++--- .../PaymentHistoryManagement/actions.ts | 41 ++----- .../settings/PermissionManagement/actions.ts | 39 +++--- .../settings/PopupManagement/actions.ts | 24 ++-- 51 files changed, 1050 insertions(+), 1405 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f2d36e19..72ba961b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -326,36 +326,45 @@ const form = useForm({ --- -## 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()}` +``` --- diff --git a/claudedocs/[PLAN-2025-02-10] frontend-improvement-roadmap.md b/claudedocs/[PLAN-2025-02-10] frontend-improvement-roadmap.md index 2638651d..bb98546b 100644 --- a/claudedocs/[PLAN-2025-02-10] frontend-improvement-roadmap.md +++ b/claudedocs/[PLAN-2025-02-10] frontend-improvement-roadmap.md @@ -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개 정리 | ✅ 완료 | 이전 세션 | diff --git a/claudedocs/_index.md b/claudedocs/_index.md index ea24616c..e122de36 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -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) diff --git a/src/components/accounting/BadDebtCollection/actions.ts b/src/components/accounting/BadDebtCollection/actions.ts index 3aec88dc..c90cb9c0 100644 --- a/src/components/accounting/BadDebtCollection/actions.ts +++ b/src/components/accounting/BadDebtCollection/actions.ts @@ -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 { - 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) => data.data.map(transformApiToFrontend), errorMessage: '악성채권 목록 조회에 실패했습니다.', }); @@ -178,7 +172,7 @@ export async function getBadDebts(params?: { // ===== 악성채권 상세 조회 ===== export async function getBadDebtById(id: string): Promise { 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 // ===== 악성채권 통계 조회 ===== export async function getBadDebtSummary(): Promise { const result = await executeServerAction({ - 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 ): Promise> { 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 ): Promise> { 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 { 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 { // ===== 악성채권 활성화 토글 ===== export async function toggleBadDebt(id: string): Promise> { 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> { 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 { return executeServerAction({ - url: `${API_URL}/api/v1/bad-debts/${badDebtId}/memos/${memoId}`, + url: buildApiUrl(`/api/v1/bad-debts/${badDebtId}/memos/${memoId}`), method: 'DELETE', errorMessage: '메모 삭제에 실패했습니다.', }); -} \ No newline at end of file +} diff --git a/src/components/accounting/BankTransactionInquiry/actions.ts b/src/components/accounting/BankTransactionInquiry/actions.ts index bec2c878..75ed09d7 100644 --- a/src/components/accounting/BankTransactionInquiry/actions.ts +++ b/src/components/accounting/BankTransactionInquiry/actions.ts @@ -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; - -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({ + 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> { - 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 }; -} \ No newline at end of file +} diff --git a/src/components/accounting/BillManagement/actions.ts b/src/components/accounting/BillManagement/actions.ts index 8888bfc0..35b9d1de 100644 --- a/src/components/accounting/BillManagement/actions.ts +++ b/src/components/accounting/BillManagement/actions.ts @@ -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; - 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({ + 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> { 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> { // ===== 어음 등록 ===== export async function createBill(data: Partial): Promise> { 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): Promise): Promise> { 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): Promise // ===== 어음 삭제 ===== export async function deleteBill(id: string): Promise { 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 { // ===== 어음 상태 변경 ===== export async function updateBillStatus(id: string, status: BillStatus): Promise> { 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; 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> { 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 || []; diff --git a/src/components/accounting/CardTransactionInquiry/actions.ts b/src/components/accounting/CardTransactionInquiry/actions.ts index c4a5de7f..342c6b4d 100644 --- a/src/components/accounting/CardTransactionInquiry/actions.ts +++ b/src/components/accounting/CardTransactionInquiry/actions.ts @@ -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; - -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({ + 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> { - 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> { 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> { 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> { 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 { 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 }; -} \ No newline at end of file +} diff --git a/src/components/accounting/DailyReport/actions.ts b/src/components/accounting/DailyReport/actions.ts index ac84f635..732a2271 100644 --- a/src/components/accounting/DailyReport/actions.ts +++ b/src/components/accounting/DailyReport/actions.ts @@ -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 } ); diff --git a/src/components/accounting/DepositManagement/actions.ts b/src/components/accounting/DepositManagement/actions.ts index bbb13202..1dca4fd8 100644 --- a/src/components/accounting/DepositManagement/actions.ts +++ b/src/components/accounting/DepositManagement/actions.ts @@ -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; - -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): Record { - 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({ + 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 { 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 { // ===== 계정과목명 일괄 저장 ===== export async function updateDepositTypes(ids: string[], depositType: string): Promise { 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> { 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): Promise> { 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): Promise): Promise> { 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), diff --git a/src/components/accounting/ExpectedExpenseManagement/actions.ts b/src/components/accounting/ExpectedExpenseManagement/actions.ts index 9b0fde5c..32dc8f8d 100644 --- a/src/components/accounting/ExpectedExpenseManagement/actions.ts +++ b/src/components/accounting/ExpectedExpenseManagement/actions.ts @@ -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; - interface SummaryData { total_amount: number; total_count: number; @@ -39,9 +36,6 @@ interface SummaryData { by_month: Record; } -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({ + 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> { 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): Promise> { 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 // ===== 미지급비용 수정 ===== export async function updateExpectedExpense(id: string, data: Partial): Promise> { 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 { 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> { - 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({ - 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 }; -} \ No newline at end of file +} diff --git a/src/components/accounting/PurchaseManagement/actions.ts b/src/components/accounting/PurchaseManagement/actions.ts index 62ce09de..b3d445ad 100644 --- a/src/components/accounting/PurchaseManagement/actions.ts +++ b/src/components/accounting/PurchaseManagement/actions.ts @@ -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; - // ===== 변환 함수 ===== const VALID_PURCHASE_TYPES: PurchaseType[] = [ @@ -96,39 +93,30 @@ function transformFrontendToApi(data: Partial): Record { - 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({ + 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> { 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): Promise> { 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): Promise): Promise> { 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 { 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 { // ===== 매입 확정 ===== export async function confirmPurchase(id: string): Promise> { 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 }; -} \ No newline at end of file +} diff --git a/src/components/accounting/ReceivablesStatus/actions.ts b/src/components/accounting/ReceivablesStatus/actions.ts index 0d15c09a..dfb03f1a 100644 --- a/src/components/accounting/ReceivablesStatus/actions.ts +++ b/src/components/accounting/ReceivablesStatus/actions.ts @@ -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', diff --git a/src/components/accounting/SalesManagement/actions.ts b/src/components/accounting/SalesManagement/actions.ts index ff31c387..2fde1b13 100644 --- a/src/components/accounting/SalesManagement/actions.ts +++ b/src/components/accounting/SalesManagement/actions.ts @@ -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({ + 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> { 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 // ===== 매출 등록 ===== export async function createSale(data: Partial): Promise> { 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): Promise): Promise> { 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 { 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 { // ===== 매출 확정 ===== export async function confirmSale(id: string): Promise> { 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> { - 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 }; -} \ No newline at end of file +} diff --git a/src/components/accounting/VendorLedger/actions.ts b/src/components/accounting/VendorLedger/actions.ts index 8d21b50f..3b88ea11 100644 --- a/src/components/accounting/VendorLedger/actions.ts +++ b/src/components/accounting/VendorLedger/actions.ts @@ -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; - -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({ + 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> { - 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', diff --git a/src/components/accounting/VendorManagement/actions.ts b/src/components/accounting/VendorManagement/actions.ts index 3fac4452..ce52cba3 100644 --- a/src/components/accounting/VendorManagement/actions.ts +++ b/src/components/accounting/VendorManagement/actions.ts @@ -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) => ({ items: data.data.map(transformApiToFrontend), total: data.total, @@ -157,7 +155,7 @@ export async function getClients(params?: { // ===== 거래처 상세 조회 ===== export async function getClientById(id: string): Promise { 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 ): Promise> { 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 ): Promise> { 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 { 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 { // ===== 거래처 활성/비활성 토글 ===== export async function toggleClientActive(id: string): Promise> { 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: '상태 변경에 실패했습니다.', diff --git a/src/components/accounting/WithdrawalManagement/actions.ts b/src/components/accounting/WithdrawalManagement/actions.ts index 217bb061..7fd32403 100644 --- a/src/components/accounting/WithdrawalManagement/actions.ts +++ b/src/components/accounting/WithdrawalManagement/actions.ts @@ -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; - -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): Record { - 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({ + 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 { 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 { // ===== 계정과목명 일괄 저장 ===== export async function updateWithdrawalTypes(ids: string[], withdrawalType: string): Promise { 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> { 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): Promise> { 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): Promise // ===== 출금 수정 ===== export async function updateWithdrawal(id: string, data: Partial): Promise> { 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), diff --git a/src/components/approval/ApprovalBox/actions.ts b/src/components/approval/ApprovalBox/actions.ts index 119d07f3..17b2a1a8 100644 --- a/src/components/approval/ApprovalBox/actions.ts +++ b/src/components/approval/ApprovalBox/actions.ts @@ -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>({ - 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 { const result = await executeServerAction({ - 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 { export async function approveDocument(id: string, comment?: string): Promise { 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 { 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: '반려 처리에 실패했습니다.', diff --git a/src/components/approval/DocumentCreate/actions.ts b/src/components/approval/DocumentCreate/actions.ts index a2c61c33..3fa675ee 100644 --- a/src/components/approval/DocumentCreate/actions.ts +++ b/src/components/approval/DocumentCreate/actions.ts @@ -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({ - 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 { - 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({ - 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({ - 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({ - 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: '문서 삭제에 실패했습니다.', }); diff --git a/src/components/approval/DraftBox/actions.ts b/src/components/approval/DraftBox/actions.ts index b1c70aee..fd6193ef 100644 --- a/src/components/approval/DraftBox/actions.ts +++ b/src/components/approval/DraftBox/actions.ts @@ -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 = { + '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 = { - '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>({ - 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 { const result = await executeServerAction({ - 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 { export async function getDraftById(id: string): Promise { 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 { export async function deleteDraft(id: string): Promise { 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 { 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 { return executeServerAction({ - url: `${API_URL}/api/v1/approvals/${id}/cancel`, + url: buildApiUrl(`/api/v1/approvals/${id}/cancel`), method: 'POST', body: {}, errorMessage: '결재 회수에 실패했습니다.', diff --git a/src/components/approval/ReferenceBox/actions.ts b/src/components/approval/ReferenceBox/actions.ts index 01709300..9ad152ef 100644 --- a/src/components/approval/ReferenceBox/actions.ts +++ b/src/components/approval/ReferenceBox/actions.ts @@ -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>({ - 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 { 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 { export async function markAsUnread(id: string): Promise { 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 }; -} \ No newline at end of file +} diff --git a/src/components/board/BoardManagement/actions.ts b/src/components/board/BoardManagement/actions.ts index 774e5ff7..7c353da7 100644 --- a/src/components/board/BoardManagement/actions.ts +++ b/src/components/board/BoardManagement/actions.ts @@ -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> { - 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> { - 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> { 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> // ===== 게시판 상세 조회 (ID 기반) ===== export async function getBoardById(id: string): Promise> { 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> { 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> { 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 { 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 { if (isNextRedirectError(error)) throw error; return { success: false, error: '서버 오류가 발생했습니다.' }; } -} \ No newline at end of file +} diff --git a/src/components/board/DynamicBoard/actions.ts b/src/components/board/DynamicBoard/actions.ts index 98d36b65..b3944660 100644 --- a/src/components/board/DynamicBoard/actions.ts +++ b/src/components/board/DynamicBoard/actions.ts @@ -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> { - 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({ - 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> { return executeServerAction({ - 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 } ): Promise> { return executeServerAction({ - 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 } ): Promise> { return executeServerAction({ - 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 { 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> { return executeServerAction({ - 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> { return executeServerAction({ - 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> { return executeServerAction({ - 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 { 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: '댓글 삭제에 실패했습니다.', }); -} \ No newline at end of file +} diff --git a/src/components/board/actions.ts b/src/components/board/actions.ts index 97ed111b..fa28b62a 100644 --- a/src/components/board/actions.ts +++ b/src/components/board/actions.ts @@ -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> { 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 } ): Promise> { 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 } ): Promise> { 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 { return executeServerAction({ - url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`, + url: buildApiUrl(`/api/v1/boards/${boardCode}/posts/${postId}`), method: 'DELETE', errorMessage: '게시글 삭제에 실패했습니다.', }); diff --git a/src/components/business/construction/contract/modals/ContractDocumentModal.tsx b/src/components/business/construction/contract/modals/ContractDocumentModal.tsx index 70afc193..1d07a0aa 100644 --- a/src/components/business/construction/contract/modals/ContractDocumentModal.tsx +++ b/src/components/business/construction/contract/modals/ContractDocumentModal.tsx @@ -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`); }; // 상신 (전자결재) diff --git a/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx b/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx index 578c016c..2ac52b6d 100644 --- a/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx +++ b/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx @@ -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 ( + <> + + + ); } \ No newline at end of file diff --git a/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx b/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx index 01a94670..16a71bf8 100644 --- a/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx +++ b/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx @@ -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 ( + <> + + + ); } \ No newline at end of file diff --git a/src/components/customer-center/shared/actions.ts b/src/components/customer-center/shared/actions.ts index e5c08ecc..941dfc58 100644 --- a/src/components/customer-center/shared/actions.ts +++ b/src/components/customer-center/shared/actions.ts @@ -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> { - const queryString = buildPostFilterParams(filters); return executeServerAction({ - 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> { return executeServerAction({ - 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 } ): Promise> { return executeServerAction({ - 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 } ): Promise> { return executeServerAction({ - 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 { 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> { return executeServerAction({ - 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> { return executeServerAction({ - 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> { 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: 'PUT', body: { content }, errorMessage: '댓글 수정에 실패했습니다.', @@ -132,8 +126,8 @@ export async function deleteComment( commentId: number | string ): Promise { 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: '댓글 삭제에 실패했습니다.', }); -} \ No newline at end of file +} diff --git a/src/components/hr/AttendanceManagement/actions.ts b/src/components/hr/AttendanceManagement/actions.ts index 4a630295..c54e4a68 100644 --- a/src/components/hr/AttendanceManagement/actions.ts +++ b/src/components/hr/AttendanceManagement/actions.ts @@ -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 { const result = await executeServerAction>({ - 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>({ - 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 { 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 { - 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({ - 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 }); diff --git a/src/components/hr/CardManagement/actions.ts b/src/components/hr/CardManagement/actions.ts index 2584425a..1c4843d7 100644 --- a/src/components/hr/CardManagement/actions.ts +++ b/src/components/hr/CardManagement/actions.ts @@ -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 { 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({ - 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> { 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> { // ===== 카드 등록 ===== export async function createCard(data: CardFormData): Promise> { 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 // ===== 카드 수정 ===== export async function updateCard(id: string, data: CardFormData): Promise> { 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 { 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> { 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({ - 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 }; -} \ No newline at end of file +} diff --git a/src/components/hr/DepartmentManagement/actions.ts b/src/components/hr/DepartmentManagement/actions.ts index a863ebd1..109eb89f 100644 --- a/src/components/hr/DepartmentManagement/actions.ts +++ b/src/components/hr/DepartmentManagement/actions.ts @@ -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> { - 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> { 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> { 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> { 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 { return executeServerAction({ - url: `${API_URL}/v1/departments/${id}`, + url: buildApiUrl(`/api/v1/departments/${id}`), method: 'DELETE', errorMessage: '부서 삭제에 실패했습니다.', }); diff --git a/src/components/hr/EmployeeManagement/actions.ts b/src/components/hr/EmployeeManagement/actions.ts index 61d1016e..550ebe5d 100644 --- a/src/components/hr/EmployeeManagement/actions.ts +++ b/src/components/hr/EmployeeManagement/actions.ts @@ -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>({ - 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 { 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 { const result = await executeServerAction({ - 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 { - const searchParams = new URLSearchParams(); - if (type) searchParams.set('type', type); - const result = await executeServerAction({ - 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 { const result = await executeServerAction({ - url: `${API_URL}/api/v1/departments`, + url: buildApiUrl('/api/v1/departments'), errorMessage: '부서 조회에 실패했습니다.', }); if (!result.data) return []; diff --git a/src/components/hr/SalaryManagement/actions.ts b/src/components/hr/SalaryManagement/actions.ts index 6a5247c5..8745b6a8 100644 --- a/src/components/hr/SalaryManagement/actions.ts +++ b/src/components/hr/SalaryManagement/actions.ts @@ -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({ - 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({ - 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', diff --git a/src/components/hr/SalaryManagement/index.tsx b/src/components/hr/SalaryManagement/index.tsx index ca803df2..e6429650 100644 --- a/src/components/hr/SalaryManagement/index.tsx +++ b/src/components/hr/SalaryManagement/index.tsx @@ -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: () => ( -
- {canExport && ( - - )} -
- ), + headerActions: () => null, renderTableRow: (item, index, globalIndex, handlers) => { const { isSelected, onToggle } = handlers; diff --git a/src/components/hr/VacationManagement/actions.ts b/src/components/hr/VacationManagement/actions.ts index 58f8c408..f0f68aac 100644 --- a/src/components/hr/VacationManagement/actions.ts +++ b/src/components/hr/VacationManagement/actions.ts @@ -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 { 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>>({ - 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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) => 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>>({ - 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>>({ - 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) => 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[]; total: number } const result = await executeServerAction({ - 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 }; diff --git a/src/components/material/ReceivingManagement/actions.ts b/src/components/material/ReceivingManagement/actions.ts index 460ded23..a8c27ddd 100644 --- a/src/components/material/ReceivingManagement/actions.ts +++ b/src/components/material/ReceivingManagement/actions.ts @@ -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; - 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({ - url: `${API_URL}/api/v1/receivings${queryString ? `?${queryString}` : ''}`, + return executePaginatedAction({ + 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> } const result = await executeServerAction({ - 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> } const result = await executeServerAction({ - 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({ - 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', diff --git a/src/components/material/StockStatus/actions.ts b/src/components/material/StockStatus/actions.ts index ea912018..365513ff 100644 --- a/src/components/material/StockStatus/actions.ts +++ b/src/components/material/StockStatus/actions.ts @@ -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; - 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({ - url: `${API_URL}/api/v1/stocks${queryString ? `?${queryString}` : ''}`, +}) { + return executePaginatedAction({ + 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({ - 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: '재고 실사 저장에 실패했습니다.', diff --git a/src/components/orders/actions.ts b/src/components/orders/actions.ts index c9ef8fb8..ce4b5f04 100644 --- a/src/components/orders/actions.ts +++ b/src/components/orders/actions.ts @@ -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>({ - 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) }> { 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 { 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({ - 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({ - 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({ - 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({ - 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>({ - 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 }; diff --git a/src/components/outbound/ShipmentManagement/actions.ts b/src/components/outbound/ShipmentManagement/actions.ts index 9dc54c7a..b78d771d 100644 --- a/src/components/outbound/ShipmentManagement/actions.ts +++ b/src/components/outbound/ShipmentManagement/actions.ts @@ -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; - 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({ - url: `${API_URL}/api/v1/shipments${queryString ? `?${queryString}` : ''}`, +}) { + return executePaginatedAction({ + 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({ - 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({ - 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({ - 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 }; diff --git a/src/components/process-management/actions.ts b/src/components/process-management/actions.ts index 8758a2d3..2f59a5cb 100644 --- a/src/components/process-management/actions.ts +++ b/src/components/process-management/actions.ts @@ -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 // 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>({ - 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({ - 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 } const result = await executeServerAction({ - 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 { ]; interface DeptResponse { data: Array<{ id: number; name: string }> } const result = await executeServerAction({ - 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 { - 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({ - 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> { const result = await executeServerAction>({ - 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({ - 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 ): 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({ - url: `${API_URL}/api/v1/processes/${processId}/steps/reorder`, + url: buildApiUrl(`/api/v1/processes/${processId}/steps/reorder`), method: 'PATCH', body: { items: steps.map((s) => ({ diff --git a/src/components/production/ProductionDashboard/actions.ts b/src/components/production/ProductionDashboard/actions.ts index f9bae954..1003f69e 100644 --- a/src/components/production/ProductionDashboard/actions.ts +++ b/src/components/production/ProductionDashboard/actions.ts @@ -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: '데이터 조회에 실패했습니다.', }); diff --git a/src/components/production/WorkOrders/actions.ts b/src/components/production/WorkOrders/actions.ts index 357d938d..3af799a8 100644 --- a/src/components/production/WorkOrders/actions.ts +++ b/src/components/production/WorkOrders/actions.ts @@ -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' } ); diff --git a/src/components/production/WorkResults/actions.ts b/src/components/production/WorkResults/actions.ts index 2e0e7258..c0d73bd6 100644 --- a/src/components/production/WorkResults/actions.ts +++ b/src/components/production/WorkResults/actions.ts @@ -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({ - 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: '포장 상태 변경에 실패했습니다.', diff --git a/src/components/quality/InspectionManagement/actions.ts b/src/components/quality/InspectionManagement/actions.ts index a2cb3c00..b8669063 100644 --- a/src/components/quality/InspectionManagement/actions.ts +++ b/src/components/quality/InspectionManagement/actions.ts @@ -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 { }; } -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({ - 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({ - 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({ - 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({ - 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({ - 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({ - 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({ - 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({ - url: `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/select${queryString ? `?${queryString}` : ''}`, + url: buildApiUrl('/api/v1/orders/select', { q: params?.q }), errorMessage: '수주 선택 목록 조회에 실패했습니다.', }); diff --git a/src/components/quality/PerformanceReportManagement/actions.ts b/src/components/quality/PerformanceReportManagement/actions.ts index dd9c8d41..51c57246 100644 --- a/src/components/quality/PerformanceReportManagement/actions.ts +++ b/src/components/quality/PerformanceReportManagement/actions.ts @@ -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({ - 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({ - 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({ - 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: '메모 저장에 실패했습니다.', diff --git a/src/components/quotes/QuoteManagementClient.tsx b/src/components/quotes/QuoteManagementClient.tsx index 8e79c05c..ce9db2eb 100644 --- a/src/components/quotes/QuoteManagementClient.tsx +++ b/src/components/quotes/QuoteManagementClient.tsx @@ -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 { diff --git a/src/components/quotes/actions.ts b/src/components/quotes/actions.ts index edf8e392..3ca7e558 100644 --- a/src/components/quotes/actions.ts +++ b/src/components/quotes/actions.ts @@ -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({ - url: `${API_URL}/api/v1/quotes${queryString ? `?${queryString}` : ''}`, +export async function getQuotes(params?: QuoteListParams) { + return executePaginatedAction({ + 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 ): 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 ): 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({ - 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({ - 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[] } const result = await executeServerAction[]>({ - 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({ - 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>({ - 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({ - 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({ - 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 }; diff --git a/src/components/reports/actions.ts b/src/components/reports/actions.ts index 7c46c0b4..1085f86c 100644 --- a/src/components/reports/actions.ts +++ b/src/components/reports/actions.ts @@ -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> { - 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 { 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 { // ===== 이슈 반려 ===== export async function rejectIssue(issueId: string, reason?: string): Promise { return executeServerAction({ - url: `${API_URL}/api/v1/approvals/${issueId}/reject`, + url: buildApiUrl(`/api/v1/approvals/${issueId}/reject`), method: 'POST', body: { comment: reason }, errorMessage: '반려에 실패했습니다.', diff --git a/src/components/settings/AccountManagement/actions.ts b/src/components/settings/AccountManagement/actions.ts index c72d5656..52b7e0a9 100644 --- a/src/components/settings/AccountManagement/actions.ts +++ b/src/components/settings/AccountManagement/actions.ts @@ -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> { 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> // ===== 계좌 생성 ===== export async function createBankAccount(data: AccountFormData): Promise> { 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): Promise> { 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 { 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 { // ===== 계좌 상태 토글 ===== export async function toggleBankAccountStatus(id: number): Promise> { 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> { 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: '서버 오류가 발생했습니다.' }; } -} \ No newline at end of file +} diff --git a/src/components/settings/PaymentHistoryManagement/actions.ts b/src/components/settings/PaymentHistoryManagement/actions.ts index 19686684..af7048f4 100644 --- a/src/components/settings/PaymentHistoryManagement/actions.ts +++ b/src/components/settings/PaymentHistoryManagement/actions.ts @@ -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; - 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({ + 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> { 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, diff --git a/src/components/settings/PermissionManagement/actions.ts b/src/components/settings/PermissionManagement/actions.ts index b41c5725..6ca463b7 100644 --- a/src/components/settings/PermissionManagement/actions.ts +++ b/src/components/settings/PermissionManagement/actions.ts @@ -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>> { - 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>({ - 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> { return executeServerAction({ - 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> { const result = await executeServerAction({ - 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> { const result = await executeServerAction({ - 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 { 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 { export async function fetchRoleStats(): Promise> { return executeServerAction({ - url: `${API_URL}/api/v1/roles/stats`, + url: buildApiUrl('/api/v1/roles/stats'), errorMessage: '역할 통계 조회에 실패했습니다.', }); } export async function fetchActiveRoles(): Promise> { return executeServerAction({ - url: `${API_URL}/api/v1/roles/active`, + url: buildApiUrl('/api/v1/roles/active'), errorMessage: '활성 역할 목록 조회에 실패했습니다.', }); } @@ -91,14 +88,14 @@ export async function fetchPermissionMenus(): Promise> { 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> { return executeServerAction({ - 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> { 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> { return rolePermissionAction(roleId, 'reset', '권한 초기화에 실패했습니다.'); -} \ No newline at end of file +} diff --git a/src/components/settings/PopupManagement/actions.ts b/src/components/settings/PopupManagement/actions.ts index 90c904a3..17d15eed 100644 --- a/src/components/settings/PopupManagement/actions.ts +++ b/src/components/settings/PopupManagement/actions.ts @@ -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 { - 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) => data.data.map(transformApiToFrontend), errorMessage: '팝업 목록 조회에 실패했습니다.', }); @@ -52,7 +48,7 @@ export async function getPopups(params?: { */ export async function getPopupById(id: string): Promise { 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> { 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> { 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 { return executeServerAction({ - url: `${API_URL}/api/v1/popups/${id}`, + url: buildApiUrl(`/api/v1/popups/${id}`), method: 'DELETE', errorMessage: '팝업 삭제에 실패했습니다.', });