- 전체 모듈 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>
460 lines
14 KiB
TypeScript
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: '서버 오류가 발생했습니다.' };
|
|
}
|
|
} |