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:
@@ -57,7 +57,7 @@ import { formatAmount, formatAmountManwon } from '@/utils/formatAmount';
|
||||
import type { Quote, QuoteFilterType } from './types';
|
||||
import { PRODUCT_CATEGORY_LABELS } from './types';
|
||||
import { getQuotes, deleteQuote, bulkDeleteQuotes } from './actions';
|
||||
import type { PaginationMeta } from './actions';
|
||||
import type { PaginationMeta } from '@/lib/api/types';
|
||||
|
||||
// ===== Props 타입 =====
|
||||
interface QuoteManagementClientProps {
|
||||
|
||||
@@ -23,68 +23,35 @@
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
Quote,
|
||||
QuoteApiData,
|
||||
QuoteApiPaginatedResponse,
|
||||
QuoteListParams,
|
||||
QuoteStatus,
|
||||
ProductCategory,
|
||||
BomCalculationResult,
|
||||
} from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
export interface PaginationMeta {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 견적 목록 조회 =====
|
||||
export async function getQuotes(params?: QuoteListParams): Promise<{
|
||||
success: boolean;
|
||||
data: Quote[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('size', String(params.perPage));
|
||||
if (params?.search) searchParams.set('q', params.search);
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
if (params?.productCategory) searchParams.set('product_category', params.productCategory);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
|
||||
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortOrder) searchParams.set('sort_order', params.sortOrder);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<QuoteApiPaginatedResponse>({
|
||||
url: `${API_URL}/api/v1/quotes${queryString ? `?${queryString}` : ''}`,
|
||||
export async function getQuotes(params?: QuoteListParams) {
|
||||
return executePaginatedAction<QuoteApiData, Quote>({
|
||||
url: buildApiUrl('/api/v1/quotes', {
|
||||
page: params?.page,
|
||||
size: params?.perPage,
|
||||
q: params?.search,
|
||||
status: params?.status,
|
||||
product_category: params?.productCategory,
|
||||
client_id: params?.clientId,
|
||||
date_from: params?.dateFrom,
|
||||
date_to: params?.dateTo,
|
||||
sort_by: params?.sortBy,
|
||||
sort_order: params?.sortOrder,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '견적 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (result.__authError) return { success: false, data: [], pagination: emptyPagination, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, data: [], pagination: emptyPagination, error: result.error };
|
||||
|
||||
const paginatedData = result.data;
|
||||
return {
|
||||
success: true,
|
||||
data: (paginatedData.data || []).map(transformApiToFrontend),
|
||||
pagination: {
|
||||
currentPage: paginatedData.current_page,
|
||||
lastPage: paginatedData.last_page,
|
||||
perPage: paginatedData.per_page,
|
||||
total: paginatedData.total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 견적 상세 조회 =====
|
||||
@@ -95,7 +62,7 @@ export async function getQuoteById(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}`),
|
||||
transform: (data: QuoteApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '견적 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -108,7 +75,7 @@ export async function createQuote(
|
||||
data: Record<string, unknown>
|
||||
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes`,
|
||||
url: buildApiUrl('/api/v1/quotes'),
|
||||
method: 'POST',
|
||||
body: data,
|
||||
transform: (d: QuoteApiData) => transformApiToFrontend(d),
|
||||
@@ -124,7 +91,7 @@ export async function updateQuote(
|
||||
data: Record<string, unknown>
|
||||
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}`),
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: (d: QuoteApiData) => transformApiToFrontend(d),
|
||||
@@ -137,7 +104,7 @@ export async function updateQuote(
|
||||
// ===== 견적 삭제 =====
|
||||
export async function deleteQuote(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '견적 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -148,7 +115,7 @@ export async function deleteQuote(id: string): Promise<{ success: boolean; error
|
||||
// ===== 견적 일괄 삭제 =====
|
||||
export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/bulk`,
|
||||
url: buildApiUrl('/api/v1/quotes/bulk'),
|
||||
method: 'DELETE',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
errorMessage: '견적 일괄 삭제에 실패했습니다.',
|
||||
@@ -165,7 +132,7 @@ export async function finalizeQuote(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/finalize`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/finalize`),
|
||||
method: 'POST',
|
||||
transform: (d: QuoteApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '견적 확정에 실패했습니다.',
|
||||
@@ -182,7 +149,7 @@ export async function cancelFinalizeQuote(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/cancel-finalize`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/cancel-finalize`),
|
||||
method: 'POST',
|
||||
transform: (d: QuoteApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '견적 확정 취소에 실패했습니다.',
|
||||
@@ -204,7 +171,7 @@ export async function convertQuoteToOrder(id: string): Promise<{
|
||||
order?: { id: number };
|
||||
}
|
||||
const result = await executeServerAction<ConvertResponse>({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/convert`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/convert`),
|
||||
method: 'POST',
|
||||
errorMessage: '수주 전환에 실패했습니다.',
|
||||
});
|
||||
@@ -227,7 +194,7 @@ export async function getQuoteNumberPreview(): Promise<{
|
||||
}> {
|
||||
interface PreviewResponse { quote_number?: string }
|
||||
const result = await executeServerAction<PreviewResponse | string>({
|
||||
url: `${API_URL}/api/v1/quotes/number/preview`,
|
||||
url: buildApiUrl('/api/v1/quotes/number/preview'),
|
||||
errorMessage: '견적번호 미리보기에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
@@ -246,7 +213,7 @@ export async function generateQuotePdf(id: string): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/pdf`;
|
||||
const url = buildApiUrl(`/api/v1/quotes/${id}/pdf`);
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
@@ -296,7 +263,7 @@ export async function sendQuoteEmail(
|
||||
emailData: { email: string; subject?: string; message?: string }
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/send/email`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/send/email`),
|
||||
method: 'POST',
|
||||
body: emailData,
|
||||
errorMessage: '이메일 발송에 실패했습니다.',
|
||||
@@ -311,7 +278,7 @@ export async function sendQuoteKakao(
|
||||
kakaoData: { phone: string; templateId?: string }
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/quotes/${id}/send/kakao`,
|
||||
url: buildApiUrl(`/api/v1/quotes/${id}/send/kakao`),
|
||||
method: 'POST',
|
||||
body: kakaoData,
|
||||
errorMessage: '카카오 발송에 실패했습니다.',
|
||||
@@ -338,15 +305,14 @@ export async function getFinishedGoods(category?: string): Promise<{
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('item_type', 'FG');
|
||||
searchParams.set('has_bom', '1');
|
||||
if (category) searchParams.set('item_category', category);
|
||||
searchParams.set('size', '5000');
|
||||
|
||||
interface FGApiResponse { data?: Record<string, unknown>[] }
|
||||
const result = await executeServerAction<FGApiResponse | Record<string, unknown>[]>({
|
||||
url: `${API_URL}/api/v1/items?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/items', {
|
||||
item_type: 'FG',
|
||||
has_bom: '1',
|
||||
item_category: category,
|
||||
size: '5000',
|
||||
}),
|
||||
errorMessage: '완제품 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -414,7 +380,7 @@ export async function calculateBomBulk(items: BomCalculateItem[], debug: boolean
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<BomBulkResponse>({
|
||||
url: `${API_URL}/api/v1/quotes/calculate/bom/bulk`,
|
||||
url: buildApiUrl('/api/v1/quotes/calculate/bom/bulk'),
|
||||
method: 'POST',
|
||||
body: { items, debug },
|
||||
errorMessage: 'BOM 계산에 실패했습니다.',
|
||||
@@ -436,7 +402,7 @@ export async function getItemPrices(itemCodes: string[]): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<Record<string, ItemPriceResult>>({
|
||||
url: `${API_URL}/api/v1/quotes/items/prices`,
|
||||
url: buildApiUrl('/api/v1/quotes/items/prices'),
|
||||
method: 'POST',
|
||||
body: { item_codes: itemCodes },
|
||||
errorMessage: '단가 조회에 실패했습니다.',
|
||||
@@ -534,7 +500,7 @@ export async function getQuoteReferenceData(): Promise<{
|
||||
}> {
|
||||
interface RefApiData { site_names?: string[]; location_codes?: string[] }
|
||||
const result = await executeServerAction<RefApiData>({
|
||||
url: `${API_URL}/api/v1/quotes/reference-data`,
|
||||
url: buildApiUrl('/api/v1/quotes/reference-data'),
|
||||
errorMessage: '참조 데이터 조회에 실패했습니다.',
|
||||
});
|
||||
const empty: QuoteReferenceData = { siteNames: [], locationCodes: [] };
|
||||
@@ -566,7 +532,7 @@ export async function getItemCategoryTree(): Promise<{
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
const result = await executeServerAction<ItemCategoryNode[]>({
|
||||
url: `${API_URL}/api/v1/categories/tree?code_group=item_category&only_active=true`,
|
||||
url: buildApiUrl('/api/v1/categories/tree', { code_group: 'item_category', only_active: true }),
|
||||
errorMessage: '카테고리 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, data: [], __authError: true };
|
||||
|
||||
Reference in New Issue
Block a user