refactor(WEB): 전체 actions.ts에 공통 API 유틸 적용
- buildApiUrl / executePaginatedAction 패턴으로 전환 (40+ actions 파일) - 직접 URLSearchParams 조립 → buildApiUrl 유틸 사용 - 수동 페이지네이션 메타 변환 → executePaginatedAction 자동 처리 - HandoverReportDocumentModal, OrderDocumentModal 개선 - 급여관리 SalaryManagement 코드 개선 - CLAUDE.md Server Action 공통 유틸 규칙 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { ApprovalRecord, ApprovalType, ApprovalStatus } from './types';
|
||||
|
||||
@@ -105,8 +106,6 @@ function transformApiToFrontend(data: InboxApiData): ApprovalRecord {
|
||||
};
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
@@ -115,20 +114,16 @@ export async function getInbox(params?: {
|
||||
page?: number; per_page?: number; search?: string; status?: string;
|
||||
approval_type?: string; sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: ApprovalRecord[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
const apiStatus = mapTabToApiStatus(params.status);
|
||||
if (apiStatus) searchParams.set('status', apiStatus);
|
||||
}
|
||||
if (params?.approval_type && params.approval_type !== 'all') searchParams.set('approval_type', params.approval_type);
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<InboxApiData>>({
|
||||
url: `${API_URL}/api/v1/approvals/inbox?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/approvals/inbox', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
search: params?.search,
|
||||
status: params?.status && params.status !== 'all' ? mapTabToApiStatus(params.status) : undefined,
|
||||
approval_type: params?.approval_type !== 'all' ? params?.approval_type : undefined,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '결재함 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -144,7 +139,7 @@ export async function getInbox(params?: {
|
||||
|
||||
export async function getInboxSummary(): Promise<InboxSummary | null> {
|
||||
const result = await executeServerAction<InboxSummary>({
|
||||
url: `${API_URL}/api/v1/approvals/inbox/summary`,
|
||||
url: buildApiUrl('/api/v1/approvals/inbox/summary'),
|
||||
errorMessage: '결재함 통계 조회에 실패했습니다.',
|
||||
});
|
||||
return result.success ? result.data || null : null;
|
||||
@@ -152,7 +147,7 @@ export async function getInboxSummary(): Promise<InboxSummary | null> {
|
||||
|
||||
export async function approveDocument(id: string, comment?: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/approve`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/approve`),
|
||||
method: 'POST',
|
||||
body: { comment: comment || '' },
|
||||
errorMessage: '승인 처리에 실패했습니다.',
|
||||
@@ -162,7 +157,7 @@ export async function approveDocument(id: string, comment?: string): Promise<Act
|
||||
export async function rejectDocument(id: string, comment: string): Promise<ActionResult> {
|
||||
if (!comment?.trim()) return { success: false, error: '반려 사유를 입력해주세요.' };
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/reject`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/reject`),
|
||||
method: 'POST',
|
||||
body: { comment },
|
||||
errorMessage: '반려 처리에 실패했습니다.',
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
ExpenseEstimateItem,
|
||||
ApprovalPerson,
|
||||
@@ -125,8 +126,6 @@ function transformEmployee(employee: EmployeeApiData): ApprovalPerson {
|
||||
// API 함수
|
||||
// ============================================
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* 파일 업로드
|
||||
* @param files 업로드할 파일 배열
|
||||
@@ -202,11 +201,8 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
|
||||
accountBalance: number;
|
||||
finalDifference: number;
|
||||
} | null> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (yearMonth) searchParams.set('year_month', yearMonth);
|
||||
|
||||
const result = await executeServerAction<ExpenseEstimateApiResponse>({
|
||||
url: `${API_URL}/api/v1/reports/expense-estimate?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/reports/expense-estimate', { year_month: yearMonth }),
|
||||
errorMessage: '비용견적서 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return null;
|
||||
@@ -222,12 +218,8 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
|
||||
* 직원 목록 조회 (결재선/참조 선택용)
|
||||
*/
|
||||
export async function getEmployees(search?: string): Promise<ApprovalPerson[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('per_page', '100');
|
||||
if (search) searchParams.set('search', search);
|
||||
|
||||
const result = await executeServerAction<{ data: EmployeeApiData[] }>({
|
||||
url: `${API_URL}/api/v1/employees?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/employees', { per_page: 100, search }),
|
||||
errorMessage: '직원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return [];
|
||||
@@ -276,7 +268,7 @@ export async function createApproval(formData: DocumentFormData): Promise<{
|
||||
};
|
||||
|
||||
const result = await executeServerAction<ApprovalCreateResponse>({
|
||||
url: `${API_URL}/api/v1/approvals`,
|
||||
url: buildApiUrl('/api/v1/approvals'),
|
||||
method: 'POST',
|
||||
body: requestBody,
|
||||
errorMessage: '문서 저장에 실패했습니다.',
|
||||
@@ -293,7 +285,7 @@ export async function submitApproval(id: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/submit`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/submit`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '문서 상신에 실패했습니다.',
|
||||
@@ -349,7 +341,7 @@ export async function getApprovalById(id: number): Promise<{
|
||||
}> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await executeServerAction<any>({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
errorMessage: '문서 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -397,7 +389,7 @@ export async function updateApproval(id: number, formData: DocumentFormData): Pr
|
||||
};
|
||||
|
||||
const result = await executeServerAction<ApprovalCreateResponse>({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
method: 'PATCH',
|
||||
body: requestBody,
|
||||
errorMessage: '문서 수정에 실패했습니다.',
|
||||
@@ -449,7 +441,7 @@ export async function deleteApproval(id: number): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '문서 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { DraftRecord, DocumentStatus, Approver } from './types';
|
||||
|
||||
@@ -152,7 +153,10 @@ function transformApiToFrontend(data: ApprovalApiData): DraftRecord {
|
||||
};
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
const DRAFT_STATUS_MAP: Record<string, string> = {
|
||||
'draft': 'draft', 'pending': 'pending', 'inProgress': 'in_progress',
|
||||
'approved': 'approved', 'rejected': 'rejected',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
@@ -162,22 +166,15 @@ export async function getDrafts(params?: {
|
||||
page?: number; per_page?: number; search?: string; status?: string;
|
||||
sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: DraftRecord[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
const statusMap: Record<string, string> = {
|
||||
'draft': 'draft', 'pending': 'pending', 'inProgress': 'in_progress',
|
||||
'approved': 'approved', 'rejected': 'rejected',
|
||||
};
|
||||
searchParams.set('status', statusMap[params.status] || params.status);
|
||||
}
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<ApprovalApiData>>({
|
||||
url: `${API_URL}/api/v1/approvals/drafts?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/approvals/drafts', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
search: params?.search,
|
||||
status: params?.status && params.status !== 'all' ? (DRAFT_STATUS_MAP[params.status] || params.status) : undefined,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '기안함 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -193,7 +190,7 @@ export async function getDrafts(params?: {
|
||||
|
||||
export async function getDraftsSummary(): Promise<DraftsSummary | null> {
|
||||
const result = await executeServerAction<DraftsSummary>({
|
||||
url: `${API_URL}/api/v1/approvals/drafts/summary`,
|
||||
url: buildApiUrl('/api/v1/approvals/drafts/summary'),
|
||||
errorMessage: '기안함 현황 조회에 실패했습니다.',
|
||||
});
|
||||
return result.success ? result.data || null : null;
|
||||
@@ -201,7 +198,7 @@ export async function getDraftsSummary(): Promise<DraftsSummary | null> {
|
||||
|
||||
export async function getDraftById(id: string): Promise<DraftRecord | null> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
transform: (data: ApprovalApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '결재 문서 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -210,7 +207,7 @@ export async function getDraftById(id: string): Promise<DraftRecord | null> {
|
||||
|
||||
export async function deleteDraft(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '결재 문서 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -230,7 +227,7 @@ export async function deleteDrafts(ids: string[]): Promise<{ success: boolean; f
|
||||
|
||||
export async function submitDraft(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/submit`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/submit`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '결재 상신에 실패했습니다.',
|
||||
@@ -251,7 +248,7 @@ export async function submitDrafts(ids: string[]): Promise<{ success: boolean; f
|
||||
|
||||
export async function cancelDraft(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/cancel`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/cancel`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '결재 회수에 실패했습니다.',
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import type { ReferenceRecord, ApprovalType, DocumentStatus } from './types';
|
||||
@@ -82,8 +83,6 @@ function transformApiToFrontend(data: ReferenceApiData): ReferenceRecord {
|
||||
};
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
@@ -92,17 +91,16 @@ export async function getReferences(params?: {
|
||||
page?: number; per_page?: number; search?: string; is_read?: boolean;
|
||||
approval_type?: string; sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: ReferenceRecord[]; total: number; lastPage: number }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.is_read !== undefined) searchParams.set('is_read', params.is_read ? '1' : '0');
|
||||
if (params?.approval_type && params.approval_type !== 'all') searchParams.set('approval_type', params.approval_type);
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<ReferenceApiData>>({
|
||||
url: `${API_URL}/api/v1/approvals/reference?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/approvals/reference', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
search: params?.search,
|
||||
is_read: params?.is_read !== undefined ? (params.is_read ? '1' : '0') : undefined,
|
||||
approval_type: params?.approval_type !== 'all' ? params?.approval_type : undefined,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '참조 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -131,7 +129,7 @@ export async function getReferenceSummary(): Promise<{ all: number; read: number
|
||||
|
||||
export async function markAsRead(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/read`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/read`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '열람 처리에 실패했습니다.',
|
||||
@@ -140,7 +138,7 @@ export async function markAsRead(id: string): Promise<ActionResult> {
|
||||
|
||||
export async function markAsUnread(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/approvals/${id}/unread`,
|
||||
url: buildApiUrl(`/api/v1/approvals/${id}/unread`),
|
||||
method: 'POST',
|
||||
body: {},
|
||||
errorMessage: '미열람 처리에 실패했습니다.',
|
||||
@@ -169,4 +167,4 @@ export async function markAsUnreadBulk(ids: string[]): Promise<{ success: boolea
|
||||
return { success: false, failedIds, error: `${failedIds.length}건의 미열람 처리에 실패했습니다.` };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user