Files
sam-react-prod/src/components/material/ReceivingManagement/actions.ts
byeongcheolryu 0d539628f3 chore(WEB): actions.ts 에러 핸들링 및 CEO 대시보드 개선
- 전체 모듈 actions.ts redirect 에러 핸들링 추가
- CEODashboard DetailModal 추가
- MonthlyExpenseSection 개선
- fetch-wrapper redirect 에러 처리
- redirect-error 유틸 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 18:41:15 +09:00

460 lines
14 KiB
TypeScript

/**
* 입고 관리 서버 액션
*
* API Endpoints:
* - GET /api/v1/receivings - 목록 조회
* - GET /api/v1/receivings/stats - 통계 조회
* - GET /api/v1/receivings/{id} - 상세 조회
* - POST /api/v1/receivings - 등록
* - PUT /api/v1/receivings/{id} - 수정
* - DELETE /api/v1/receivings/{id} - 삭제
* - POST /api/v1/receivings/{id}/process - 입고처리
*/
'use server';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
ReceivingItem,
ReceivingDetail,
ReceivingStats,
ReceivingStatus,
ReceivingProcessFormData,
} from './types';
// ===== API 데이터 타입 =====
interface ReceivingApiData {
id: number;
receiving_number: string;
order_no?: string;
order_date?: string;
item_id?: number;
item_code: string;
item_name: string;
specification?: string;
supplier: string;
order_qty: string | number;
order_unit: string;
due_date?: string;
receiving_qty?: string | number;
receiving_date?: string;
lot_no?: string;
supplier_lot?: string;
receiving_location?: string;
receiving_manager?: string;
status: ReceivingStatus;
remark?: string;
creator?: { id: number; name: string };
created_at?: string;
updated_at?: string;
}
interface ReceivingApiPaginatedResponse {
data: ReceivingApiData[];
current_page: number;
last_page: number;
per_page: number;
total: number;
}
interface ReceivingApiStatsResponse {
receiving_pending_count: number;
shipping_count: number;
inspection_pending_count: number;
today_receiving_count: number;
}
// ===== API → Frontend 변환 (목록용) =====
function transformApiToListItem(data: ReceivingApiData): ReceivingItem {
return {
id: String(data.id),
orderNo: data.order_no || data.receiving_number,
itemCode: data.item_code,
itemName: data.item_name,
supplier: data.supplier,
orderQty: parseFloat(String(data.order_qty)) || 0,
orderUnit: data.order_unit || 'EA',
receivingQty: data.receiving_qty ? parseFloat(String(data.receiving_qty)) : undefined,
lotNo: data.lot_no,
status: data.status,
};
}
// ===== API → Frontend 변환 (상세용) =====
function transformApiToDetail(data: ReceivingApiData): ReceivingDetail {
return {
id: String(data.id),
orderNo: data.order_no || data.receiving_number,
orderDate: data.order_date,
supplier: data.supplier,
itemCode: data.item_code,
itemName: data.item_name,
specification: data.specification,
orderQty: parseFloat(String(data.order_qty)) || 0,
orderUnit: data.order_unit || 'EA',
dueDate: data.due_date,
status: data.status,
receivingDate: data.receiving_date,
receivingQty: data.receiving_qty ? parseFloat(String(data.receiving_qty)) : undefined,
receivingLot: data.lot_no,
supplierLot: data.supplier_lot,
receivingLocation: data.receiving_location,
receivingManager: data.receiving_manager,
};
}
// ===== API → Frontend 변환 (통계용) =====
function transformApiToStats(data: ReceivingApiStatsResponse): ReceivingStats {
return {
receivingPendingCount: data.receiving_pending_count,
shippingCount: data.shipping_count,
inspectionPendingCount: data.inspection_pending_count,
todayReceivingCount: data.today_receiving_count,
};
}
// ===== Frontend → API 변환 (등록/수정용) =====
function transformFrontendToApi(
data: Partial<ReceivingDetail>
): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (data.orderNo !== undefined) result.order_no = data.orderNo;
if (data.orderDate !== undefined) result.order_date = data.orderDate;
if (data.itemCode !== undefined) result.item_code = data.itemCode;
if (data.itemName !== undefined) result.item_name = data.itemName;
if (data.specification !== undefined) result.specification = data.specification;
if (data.supplier !== undefined) result.supplier = data.supplier;
if (data.orderQty !== undefined) result.order_qty = data.orderQty;
if (data.orderUnit !== undefined) result.order_unit = data.orderUnit;
if (data.dueDate !== undefined) result.due_date = data.dueDate;
if (data.status !== undefined) result.status = data.status;
return result;
}
// ===== Frontend → API 변환 (입고처리용) =====
function transformProcessDataToApi(
data: ReceivingProcessFormData
): Record<string, unknown> {
return {
receiving_qty: data.receivingQty,
lot_no: data.receivingLot,
supplier_lot: data.supplierLot,
receiving_location: data.receivingLocation,
remark: data.remark,
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
currentPage: number;
lastPage: number;
perPage: number;
total: number;
}
// ===== 입고 목록 조회 =====
export async function getReceivings(params?: {
page?: number;
perPage?: number;
startDate?: string;
endDate?: string;
status?: string;
search?: string;
}): Promise<{
success: boolean;
data: ReceivingItem[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
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?.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 url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings${queryString ? `?${queryString}` : ''}`;
const { response, error } = await serverFetch(url, {
method: 'GET',
cache: 'no-store',
});
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '입고 목록 조회에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '입고 목록 조회에 실패했습니다.',
};
}
const paginatedData: ReceivingApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
per_page: 20,
total: 0,
};
const receivings = (paginatedData.data || []).map(transformApiToListItem);
return {
success: true,
data: receivings,
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] getReceivings error:', error);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '서버 오류가 발생했습니다.',
};
}
}
// ===== 입고 통계 조회 =====
export async function getReceivingStats(): Promise<{
success: boolean;
data?: ReceivingStats;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/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 = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '입고 통계 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToStats(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] getReceivingStats error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 입고 상세 조회 =====
export async function getReceivingById(id: string): Promise<{
success: boolean;
data?: ReceivingDetail;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${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 = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '입고 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] getReceivingById error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 입고 등록 =====
export async function createReceiving(
data: Partial<ReceivingDetail>
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings`,
{ 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 = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] createReceiving error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 입고 수정 =====
export async function updateReceiving(
id: string,
data: Partial<ReceivingDetail>
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${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 = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] updateReceiving error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 입고 삭제 =====
export async function deleteReceiving(
id: string
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}`,
{ method: 'DELETE' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '입고 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고 삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] deleteReceiving error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 입고처리 =====
export async function processReceiving(
id: string,
data: ReceivingProcessFormData
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const apiData = transformProcessDataToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}/process`,
{ 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 = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고처리에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] processReceiving error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}