refactor(WEB): Server Action 공통화 및 보안 강화

- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일)
- sanitize 유틸 추가 (XSS 방지)
- middleware CSP 헤더 추가 및 Open Redirect 방지
- 프록시 라우트 로깅 개발환경 한정으로 변경
- 프로덕션 불필요 console.log 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-09 16:14:06 +09:00
parent d014227e9c
commit 55e0791e16
85 changed files with 7211 additions and 17638 deletions

View File

@@ -1,6 +1,6 @@
'use server';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import { executeServerAction } from '@/lib/api/execute-server-action';
// ============================================================================
// API 타입 정의
@@ -782,6 +782,8 @@ function transformQuoteItemForSelect(apiItem: ApiQuoteItem): QuotationItem {
// API 함수
// ============================================================================
const API_URL = process.env.NEXT_PUBLIC_API_URL;
/**
* 수주 목록 조회
*/
@@ -800,54 +802,34 @@ export async function getOrders(params?: {
error?: string;
__authError?: boolean;
}> {
try {
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) {
// Frontend status를 API 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 { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders?${searchParams.toString()}`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '목록 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<ApiOrder>> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '목록 조회에 실패했습니다.' };
}
return {
success: true,
data: {
items: result.data.data.map(transformApiToFrontend),
total: result.data.total,
page: result.data.current_page,
totalPages: result.data.last_page,
},
};
} catch (error) {
console.error('[getOrders] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
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 result = await executeServerAction<PaginatedResponse<ApiOrder>>({
url: `${API_URL}/api/v1/orders?${searchParams.toString()}`,
errorMessage: '목록 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
if (!result.success || !result.data) return { success: false, error: result.error };
return {
success: true,
data: {
items: result.data.data.map(transformApiToFrontend),
total: result.data.total,
page: result.data.current_page,
totalPages: result.data.last_page,
},
};
}
/**
@@ -859,31 +841,13 @@ export async function getOrderById(id: string): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '조회에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '조회에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[getOrderById] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/orders/${id}`,
transform: (data: ApiOrder) => transformApiToFrontend(data),
errorMessage: '조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
/**
@@ -895,33 +859,16 @@ export async function createOrder(data: OrderFormData | Record<string, unknown>)
error?: string;
__authError?: boolean;
}> {
try {
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders`,
{ method: 'POST', body: JSON.stringify(apiData) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '등록에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '등록에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[createOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const apiData = transformFrontendToApi(data);
const result = await executeServerAction({
url: `${API_URL}/api/v1/orders`,
method: 'POST',
body: apiData,
transform: (d: ApiOrder) => transformApiToFrontend(d),
errorMessage: '등록에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
/**
@@ -933,33 +880,16 @@ export async function updateOrder(id: string, data: OrderFormData | Record<strin
error?: string;
__authError?: boolean;
}> {
try {
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`,
{ method: 'PUT', body: JSON.stringify(apiData) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '수정에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '수정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[updateOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const apiData = transformFrontendToApi(data);
const result = await executeServerAction({
url: `${API_URL}/api/v1/orders/${id}`,
method: 'PUT',
body: apiData,
transform: (d: ApiOrder) => transformApiToFrontend(d),
errorMessage: '수정에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
/**
@@ -970,31 +900,13 @@ export async function deleteOrder(id: string): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`,
{ method: 'DELETE' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '삭제에 실패했습니다.' };
}
const result: ApiResponse<string> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
console.error('[deleteOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/orders/${id}`,
method: 'DELETE',
errorMessage: '삭제에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, error: result.error };
}
/**
@@ -1006,36 +918,19 @@ export async function updateOrderStatus(id: string, status: OrderStatus): Promis
error?: string;
__authError?: boolean;
}> {
try {
const apiStatus = FRONTEND_TO_API_STATUS[status];
if (!apiStatus) {
return { success: false, error: '유효하지 않은 상태입니다.' };
}
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}/status`,
{ method: 'PATCH', body: JSON.stringify({ status: apiStatus }) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '상태 변경에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[updateOrderStatus] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
const apiStatus = FRONTEND_TO_API_STATUS[status];
if (!apiStatus) {
return { success: false, error: '유효하지 않은 상태입니다.' };
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/orders/${id}/status`,
method: 'PATCH',
body: { status: apiStatus },
transform: (d: ApiOrder) => transformApiToFrontend(d),
errorMessage: '상태 변경에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
/**
@@ -1047,43 +942,25 @@ export async function getOrderStats(): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/stats`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '통계 조회에 실패했습니다.' };
}
const result: ApiResponse<ApiOrderStats> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '통계 조회에 실패했습니다.' };
}
return {
success: true,
data: {
total: result.data.total,
draft: result.data.draft,
confirmed: result.data.confirmed,
inProgress: result.data.in_progress,
completed: result.data.completed,
cancelled: result.data.cancelled,
totalAmount: result.data.total_amount,
confirmedAmount: result.data.confirmed_amount,
},
};
} catch (error) {
console.error('[getOrderStats] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction<ApiOrderStats>({
url: `${API_URL}/api/v1/orders/stats`,
errorMessage: '통계 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
if (!result.success || !result.data) return { success: false, error: result.error };
return {
success: true,
data: {
total: result.data.total,
draft: result.data.draft,
confirmed: result.data.confirmed,
inProgress: result.data.in_progress,
completed: result.data.completed,
cancelled: result.data.cancelled,
totalAmount: result.data.total_amount,
confirmedAmount: result.data.confirmed_amount,
},
};
}
/**
@@ -1132,35 +1009,19 @@ export async function createOrderFromQuote(
error?: string;
__authError?: boolean;
}> {
try {
const apiData: Record<string, unknown> = {};
if (data?.deliveryDate) apiData.delivery_date = data.deliveryDate;
if (data?.memo) apiData.memo = data.memo;
const apiData: Record<string, unknown> = {};
if (data?.deliveryDate) apiData.delivery_date = data.deliveryDate;
if (data?.memo) apiData.memo = data.memo;
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/from-quote/${quoteId}`,
{ method: 'POST', body: JSON.stringify(apiData) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '수주 생성에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '수주 생성에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[createOrderFromQuote] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/orders/from-quote/${quoteId}`,
method: 'POST',
body: apiData,
transform: (d: ApiOrder) => transformApiToFrontend(d),
errorMessage: '수주 생성에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
/**
@@ -1175,66 +1036,41 @@ export async function createProductionOrder(
error?: string;
__authError?: boolean;
}> {
try {
const apiData: Record<string, unknown> = {};
// 다중 공정 ID (우선) 또는 단일 공정 ID
if (data?.processIds && data.processIds.length > 0) {
apiData.process_ids = data.processIds;
} else if (data?.processId) {
apiData.process_id = data.processId;
}
if (data?.priority) apiData.priority = data.priority;
// 다중 담당자 ID (우선) 또는 단일 담당자 ID
if (data?.assigneeIds && data.assigneeIds.length > 0) {
apiData.assignee_ids = data.assigneeIds;
} else if (data?.assigneeId) {
apiData.assignee_id = data.assigneeId;
}
if (data?.teamId) apiData.team_id = data.teamId;
if (data?.departmentId) apiData.department_id = data.departmentId;
if (data?.scheduledDate) apiData.scheduled_date = data.scheduledDate;
if (data?.memo) apiData.memo = data.memo;
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${orderId}/production-order`,
{ method: 'POST', body: JSON.stringify(apiData) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '생산지시 생성에 실패했습니다.' };
}
const result: ApiResponse<ApiProductionOrderResponse> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '생산지시 생성에 실패했습니다.' };
}
// 다중 또는 단일 작업지시 응답 처리
const responseData: ProductionOrderResult = {
order: transformApiToFrontend(result.data.order),
};
if (result.data.work_orders && result.data.work_orders.length > 0) {
// 다중 작업지시 응답
responseData.workOrders = result.data.work_orders.map(transformWorkOrderApiToFrontend);
} else if (result.data.work_order) {
// 단일 작업지시 응답 (하위 호환성)
responseData.workOrder = transformWorkOrderApiToFrontend(result.data.work_order);
}
return {
success: true,
data: responseData,
};
} catch (error) {
console.error('[createProductionOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
const apiData: Record<string, unknown> = {};
if (data?.processIds && data.processIds.length > 0) {
apiData.process_ids = data.processIds;
} else if (data?.processId) {
apiData.process_id = data.processId;
}
if (data?.priority) apiData.priority = data.priority;
if (data?.assigneeIds && data.assigneeIds.length > 0) {
apiData.assignee_ids = data.assigneeIds;
} else if (data?.assigneeId) {
apiData.assignee_id = data.assigneeId;
}
if (data?.teamId) apiData.team_id = data.teamId;
if (data?.departmentId) apiData.department_id = data.departmentId;
if (data?.scheduledDate) apiData.scheduled_date = data.scheduledDate;
if (data?.memo) apiData.memo = data.memo;
const result = await executeServerAction<ApiProductionOrderResponse>({
url: `${API_URL}/api/v1/orders/${orderId}/production-order`,
method: 'POST',
body: apiData,
errorMessage: '생산지시 생성에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
if (!result.success || !result.data) return { success: false, error: result.error };
const responseData: ProductionOrderResult = {
order: transformApiToFrontend(result.data.order),
};
if (result.data.work_orders && result.data.work_orders.length > 0) {
responseData.workOrders = result.data.work_orders.map(transformWorkOrderApiToFrontend);
} else if (result.data.work_order) {
responseData.workOrder = transformWorkOrderApiToFrontend(result.data.work_order);
}
return { success: true, data: responseData };
}
/**
@@ -1244,60 +1080,36 @@ export async function revertProductionOrder(orderId: string): Promise<{
success: boolean;
data?: {
order: Order;
deletedCounts: {
workResults: number;
workOrderItems: number;
workOrders: number;
};
deletedCounts: { workResults: number; workOrderItems: number; workOrders: number };
previousStatus: string;
};
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${orderId}/revert-production`,
{ method: 'POST' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '생산지시 되돌리기에 실패했습니다.' };
}
const result: ApiResponse<{
order: ApiOrder;
deleted_counts: {
work_results: number;
work_order_items: number;
work_orders: number;
};
previous_status: string;
}> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '생산지시 되돌리기에 실패했습니다.' };
}
return {
success: true,
data: {
order: transformApiToFrontend(result.data.order),
deletedCounts: {
workResults: result.data.deleted_counts.work_results,
workOrderItems: result.data.deleted_counts.work_order_items,
workOrders: result.data.deleted_counts.work_orders,
},
previousStatus: result.data.previous_status,
},
};
} catch (error) {
console.error('[revertProductionOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
interface RevertResponse {
order: ApiOrder;
deleted_counts: { work_results: number; work_order_items: number; work_orders: number };
previous_status: string;
}
const result = await executeServerAction<RevertResponse>({
url: `${API_URL}/api/v1/orders/${orderId}/revert-production`,
method: 'POST',
errorMessage: '생산지시 되돌리기에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
if (!result.success || !result.data) return { success: false, error: result.error };
return {
success: true,
data: {
order: transformApiToFrontend(result.data.order),
deletedCounts: {
workResults: result.data.deleted_counts.work_results,
workOrderItems: result.data.deleted_counts.work_order_items,
workOrders: result.data.deleted_counts.work_orders,
},
previousStatus: result.data.previous_status,
},
};
}
/**
@@ -1305,47 +1117,25 @@ export async function revertProductionOrder(orderId: string): Promise<{
*/
export async function revertOrderConfirmation(orderId: string): Promise<{
success: boolean;
data?: {
order: Order;
previousStatus: string;
};
data?: { order: Order; previousStatus: string };
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${orderId}/revert-confirmation`,
{ method: 'POST' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '수주확정 되돌리기에 실패했습니다.' };
}
const result: ApiResponse<{
order: ApiOrder;
previous_status: string;
}> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '수주확정 되돌리기에 실패했습니다.' };
}
return {
success: true,
data: {
order: transformApiToFrontend(result.data.order),
previousStatus: result.data.previous_status,
},
};
} catch (error) {
console.error('[revertOrderConfirmation] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
interface RevertConfirmResponse { order: ApiOrder; previous_status: string }
const result = await executeServerAction<RevertConfirmResponse>({
url: `${API_URL}/api/v1/orders/${orderId}/revert-confirmation`,
method: 'POST',
errorMessage: '수주확정 되돌리기에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
if (!result.success || !result.data) return { success: false, error: result.error };
return {
success: true,
data: {
order: transformApiToFrontend(result.data.order),
previousStatus: result.data.previous_status,
},
};
}
/**
@@ -1358,38 +1148,13 @@ export async function getQuoteByIdForSelect(id: string): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
// 품목 포함
searchParams.set('with_items', 'true');
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}?${searchParams.toString()}`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '견적 조회에 실패했습니다.' };
}
const result: ApiResponse<ApiQuoteForSelect> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '견적 조회에 실패했습니다.' };
}
return {
success: true,
data: transformQuoteForSelect(result.data),
};
} catch (error) {
console.error('[getQuoteByIdForSelect] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/${id}?with_items=true`,
transform: (data: ApiQuoteForSelect) => transformQuoteForSelect(data),
errorMessage: '견적 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
/**
@@ -1406,47 +1171,25 @@ export async function getQuotesForSelect(params?: {
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
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));
// 확정(finalized) 상태의 견적만 조회
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 { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes?${searchParams.toString()}`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '견적 목록 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<ApiQuoteForSelect>> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '견적 목록 조회에 실패했습니다.' };
}
return {
success: true,
data: {
items: result.data.data.map(transformQuoteForSelect),
total: result.data.total,
},
};
} catch (error) {
console.error('[getQuotesForSelect] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction<PaginatedResponse<ApiQuoteForSelect>>({
url: `${API_URL}/api/v1/quotes?${searchParams.toString()}`,
errorMessage: '견적 목록 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
if (!result.success || !result.data) return { success: false, error: result.error };
return {
success: true,
data: {
items: result.data.data.map(transformQuoteForSelect),
total: result.data.total,
},
};
}