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:
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user