- 입고관리: 상세/목록 UI 개선, actions 로직 강화 - 재고현황: 상세/목록 개선, StockAuditModal 신규 추가 - 영업주문관리: 페이지 구조 개선, OrderSalesDetailEdit 기능 강화 - 주문: OrderRegistration 개선, SalesOrderDocument 신규 추가 - 견적: QuoteTransactionModal 기능 개선 - 품질: InspectionModalV2, ImportInspectionDocument 대폭 개선 - UniversalListPage: 템플릿 기능 확장 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
970 lines
28 KiB
TypeScript
970 lines
28 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';
|
||
|
||
// ===== 목데이터 모드 플래그 =====
|
||
const USE_MOCK_DATA = true;
|
||
|
||
|
||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||
import type {
|
||
ReceivingItem,
|
||
ReceivingDetail,
|
||
ReceivingStats,
|
||
ReceivingStatus,
|
||
ReceivingProcessFormData,
|
||
} from './types';
|
||
|
||
// ===== 목데이터 =====
|
||
const MOCK_RECEIVING_LIST: ReceivingItem[] = [
|
||
{
|
||
id: '1',
|
||
lotNo: 'LOT-2026-001',
|
||
inspectionStatus: '적',
|
||
inspectionDate: '2026-01-25',
|
||
supplier: '(주)대한철강',
|
||
itemCode: 'STEEL-001',
|
||
itemName: 'SUS304 스테인리스 판재',
|
||
specification: '1000x2000x3T',
|
||
unit: 'EA',
|
||
receivingQty: 100,
|
||
receivingDate: '2026-01-26',
|
||
createdBy: '김철수',
|
||
status: 'completed',
|
||
},
|
||
{
|
||
id: '2',
|
||
lotNo: 'LOT-2026-002',
|
||
inspectionStatus: '적',
|
||
inspectionDate: '2026-01-26',
|
||
supplier: '삼성전자부품',
|
||
itemCode: 'ELEC-002',
|
||
itemName: 'MCU 컨트롤러 IC',
|
||
specification: 'STM32F103C8T6',
|
||
unit: 'EA',
|
||
receivingQty: 500,
|
||
receivingDate: '2026-01-27',
|
||
createdBy: '이영희',
|
||
status: 'completed',
|
||
},
|
||
{
|
||
id: '3',
|
||
lotNo: 'LOT-2026-003',
|
||
inspectionStatus: '-',
|
||
inspectionDate: undefined,
|
||
supplier: '한국플라스틱',
|
||
itemCode: 'PLAS-003',
|
||
itemName: 'ABS 사출 케이스',
|
||
specification: '150x100x50',
|
||
unit: 'SET',
|
||
receivingQty: undefined,
|
||
receivingDate: undefined,
|
||
createdBy: '박민수',
|
||
status: 'receiving_pending',
|
||
},
|
||
{
|
||
id: '4',
|
||
lotNo: 'LOT-2026-004',
|
||
inspectionStatus: '부적',
|
||
inspectionDate: '2026-01-27',
|
||
supplier: '(주)대한철강',
|
||
itemCode: 'STEEL-002',
|
||
itemName: '알루미늄 프로파일',
|
||
specification: '40x40x2000L',
|
||
unit: 'EA',
|
||
receivingQty: 50,
|
||
receivingDate: '2026-01-28',
|
||
createdBy: '김철수',
|
||
status: 'inspection_pending',
|
||
},
|
||
{
|
||
id: '5',
|
||
lotNo: 'LOT-2026-005',
|
||
inspectionStatus: '-',
|
||
inspectionDate: undefined,
|
||
supplier: '글로벌전자',
|
||
itemCode: 'ELEC-005',
|
||
itemName: 'DC 모터 24V',
|
||
specification: '24V 100RPM',
|
||
unit: 'EA',
|
||
receivingQty: undefined,
|
||
receivingDate: undefined,
|
||
createdBy: '최지훈',
|
||
status: 'receiving_pending',
|
||
},
|
||
{
|
||
id: '6',
|
||
lotNo: 'LOT-2026-006',
|
||
inspectionStatus: '적',
|
||
inspectionDate: '2026-01-24',
|
||
supplier: '동양화학',
|
||
itemCode: 'CHEM-001',
|
||
itemName: '에폭시 접착제',
|
||
specification: '500ml',
|
||
unit: 'EA',
|
||
receivingQty: 200,
|
||
receivingDate: '2026-01-25',
|
||
createdBy: '이영희',
|
||
status: 'completed',
|
||
},
|
||
{
|
||
id: '7',
|
||
lotNo: 'LOT-2026-007',
|
||
inspectionStatus: '적',
|
||
inspectionDate: '2026-01-28',
|
||
supplier: '삼성전자부품',
|
||
itemCode: 'ELEC-007',
|
||
itemName: '커패시터 100uF',
|
||
specification: '100uF 50V',
|
||
unit: 'EA',
|
||
receivingQty: 1000,
|
||
receivingDate: '2026-01-28',
|
||
createdBy: '박민수',
|
||
status: 'completed',
|
||
},
|
||
{
|
||
id: '8',
|
||
lotNo: 'LOT-2026-008',
|
||
inspectionStatus: '-',
|
||
inspectionDate: undefined,
|
||
supplier: '한국볼트',
|
||
itemCode: 'BOLT-001',
|
||
itemName: 'SUS 볼트 M8x30',
|
||
specification: 'M8x30 SUS304',
|
||
unit: 'EA',
|
||
receivingQty: undefined,
|
||
receivingDate: undefined,
|
||
createdBy: '김철수',
|
||
status: 'receiving_pending',
|
||
},
|
||
];
|
||
|
||
const MOCK_RECEIVING_STATS: ReceivingStats = {
|
||
receivingPendingCount: 3,
|
||
receivingCompletedCount: 4,
|
||
inspectionPendingCount: 1,
|
||
inspectionCompletedCount: 5,
|
||
};
|
||
|
||
// 기획서 2026-01-28 기준 상세 목데이터
|
||
const MOCK_RECEIVING_DETAIL: Record<string, ReceivingDetail> = {
|
||
'1': {
|
||
id: '1',
|
||
// 기본 정보
|
||
lotNo: 'LOT-2026-001',
|
||
itemCode: 'STEEL-001',
|
||
itemName: 'SUS304 스테인리스 판재',
|
||
specification: '1000x2000x3T',
|
||
unit: 'EA',
|
||
supplier: '(주)대한철강',
|
||
receivingQty: 100,
|
||
receivingDate: '2026-01-26',
|
||
createdBy: '김철수',
|
||
status: 'completed',
|
||
remark: '',
|
||
// 수입검사 정보
|
||
inspectionDate: '2026-01-25',
|
||
inspectionResult: '합격',
|
||
certificateFile: undefined,
|
||
// 하위 호환
|
||
orderNo: 'PO-2026-001',
|
||
orderUnit: 'EA',
|
||
},
|
||
'2': {
|
||
id: '2',
|
||
lotNo: 'LOT-2026-002',
|
||
itemCode: 'ELEC-002',
|
||
itemName: 'MCU 컨트롤러 IC',
|
||
specification: 'STM32F103C8T6',
|
||
unit: 'EA',
|
||
supplier: '삼성전자부품',
|
||
receivingQty: 500,
|
||
receivingDate: '2026-01-27',
|
||
createdBy: '이영희',
|
||
status: 'completed',
|
||
remark: '긴급 입고',
|
||
inspectionDate: '2026-01-26',
|
||
inspectionResult: '합격',
|
||
orderNo: 'PO-2026-002',
|
||
orderUnit: 'EA',
|
||
},
|
||
'3': {
|
||
id: '3',
|
||
lotNo: 'LOT-2026-003',
|
||
itemCode: 'PLAS-003',
|
||
itemName: 'ABS 사출 케이스',
|
||
specification: '150x100x50',
|
||
unit: 'SET',
|
||
supplier: '한국플라스틱',
|
||
receivingQty: undefined,
|
||
receivingDate: undefined,
|
||
createdBy: '박민수',
|
||
status: 'receiving_pending',
|
||
remark: '',
|
||
inspectionDate: undefined,
|
||
inspectionResult: undefined,
|
||
orderNo: 'PO-2026-003',
|
||
orderUnit: 'SET',
|
||
},
|
||
'4': {
|
||
id: '4',
|
||
lotNo: 'LOT-2026-004',
|
||
itemCode: 'STEEL-002',
|
||
itemName: '알루미늄 프로파일',
|
||
specification: '40x40x2000L',
|
||
unit: 'EA',
|
||
supplier: '(주)대한철강',
|
||
receivingQty: 50,
|
||
receivingDate: '2026-01-28',
|
||
createdBy: '김철수',
|
||
status: 'inspection_pending',
|
||
remark: '검사 진행 중',
|
||
inspectionDate: '2026-01-27',
|
||
inspectionResult: '불합격',
|
||
orderNo: 'PO-2026-004',
|
||
orderUnit: 'EA',
|
||
},
|
||
'5': {
|
||
id: '5',
|
||
lotNo: 'LOT-2026-005',
|
||
itemCode: 'ELEC-005',
|
||
itemName: 'DC 모터 24V',
|
||
specification: '24V 100RPM',
|
||
unit: 'EA',
|
||
supplier: '글로벌전자',
|
||
receivingQty: undefined,
|
||
receivingDate: undefined,
|
||
createdBy: '최지훈',
|
||
status: 'receiving_pending',
|
||
remark: '',
|
||
inspectionDate: undefined,
|
||
inspectionResult: undefined,
|
||
orderNo: 'PO-2026-005',
|
||
orderUnit: 'EA',
|
||
},
|
||
};
|
||
|
||
// ===== 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;
|
||
}> {
|
||
// ===== 목데이터 모드 =====
|
||
if (USE_MOCK_DATA) {
|
||
let filteredData = [...MOCK_RECEIVING_LIST];
|
||
|
||
// 상태 필터
|
||
if (params?.status && params.status !== 'all') {
|
||
filteredData = filteredData.filter(item => item.status === params.status);
|
||
}
|
||
|
||
// 검색 필터
|
||
if (params?.search) {
|
||
const search = params.search.toLowerCase();
|
||
filteredData = filteredData.filter(
|
||
item =>
|
||
item.lotNo?.toLowerCase().includes(search) ||
|
||
item.itemCode.toLowerCase().includes(search) ||
|
||
item.itemName.toLowerCase().includes(search) ||
|
||
item.supplier.toLowerCase().includes(search)
|
||
);
|
||
}
|
||
|
||
const page = params?.page || 1;
|
||
const perPage = params?.perPage || 20;
|
||
const total = filteredData.length;
|
||
const lastPage = Math.ceil(total / perPage);
|
||
const startIndex = (page - 1) * perPage;
|
||
const paginatedData = filteredData.slice(startIndex, startIndex + perPage);
|
||
|
||
return {
|
||
success: true,
|
||
data: paginatedData,
|
||
pagination: {
|
||
currentPage: page,
|
||
lastPage,
|
||
perPage,
|
||
total,
|
||
},
|
||
};
|
||
}
|
||
|
||
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;
|
||
}> {
|
||
// ===== 목데이터 모드 =====
|
||
if (USE_MOCK_DATA) {
|
||
return { success: true, data: MOCK_RECEIVING_STATS };
|
||
}
|
||
|
||
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;
|
||
}> {
|
||
// ===== 목데이터 모드 =====
|
||
if (USE_MOCK_DATA) {
|
||
const detail = MOCK_RECEIVING_DETAIL[id];
|
||
if (detail) {
|
||
return { success: true, data: detail };
|
||
}
|
||
return { success: false, error: '입고 정보를 찾을 수 없습니다.' };
|
||
}
|
||
|
||
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: '서버 오류가 발생했습니다.' };
|
||
}
|
||
}
|
||
|
||
// ===== 수입검사 템플릿 타입 (ImportInspectionDocument와 동일) =====
|
||
export interface InspectionTemplateResponse {
|
||
templateId: string;
|
||
templateName: string;
|
||
headerInfo: {
|
||
productName: string;
|
||
specification: string;
|
||
materialNo: string;
|
||
lotSize: number;
|
||
supplier: string;
|
||
lotNo: string;
|
||
inspectionDate: string;
|
||
inspector: string;
|
||
reportDate: string;
|
||
approvers: {
|
||
writer?: string;
|
||
reviewer?: string;
|
||
approver?: string;
|
||
};
|
||
};
|
||
inspectionItems: Array<{
|
||
id: string;
|
||
no: number;
|
||
name: string;
|
||
subName?: string;
|
||
parentId?: string;
|
||
standard: {
|
||
description?: string;
|
||
value?: string | number;
|
||
options?: Array<{
|
||
id: string;
|
||
label: string;
|
||
tolerance: string;
|
||
isSelected: boolean;
|
||
}>;
|
||
};
|
||
inspectionMethod: string;
|
||
inspectionCycle: string;
|
||
measurementType: 'okng' | 'numeric' | 'both';
|
||
measurementCount: number;
|
||
rowSpan?: number;
|
||
isSubRow?: boolean;
|
||
}>;
|
||
notes?: string[];
|
||
}
|
||
|
||
// ===== 수입검사 템플릿 조회 (품목명/규격 기반) =====
|
||
export async function getInspectionTemplate(params: {
|
||
itemName: string;
|
||
specification: string;
|
||
lotNo?: string;
|
||
supplier?: string;
|
||
}): Promise<{
|
||
success: boolean;
|
||
data?: InspectionTemplateResponse;
|
||
error?: string;
|
||
__authError?: boolean;
|
||
}> {
|
||
// ===== 목데이터 모드 - EGI 강판 템플릿 반환 =====
|
||
if (USE_MOCK_DATA) {
|
||
// 품목명/규격에 따라 다른 템플릿 반환 (추후 24종 확장)
|
||
const mockTemplate: InspectionTemplateResponse = {
|
||
templateId: 'EGI-001',
|
||
templateName: '전기 아연도금 강판',
|
||
headerInfo: {
|
||
productName: params.itemName || '전기 아연도금 강판 (KS D 3528, SECC) "EGI 평국판"',
|
||
specification: params.specification || '1.55 * 1218 × 480',
|
||
materialNo: 'PE02RB',
|
||
lotSize: 200,
|
||
supplier: params.supplier || '지오TNS (KG스틸)',
|
||
lotNo: params.lotNo || '250715-02',
|
||
inspectionDate: new Date().toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit' }).replace('. ', '/').replace('.', ''),
|
||
inspector: '노원호',
|
||
reportDate: new Date().toISOString().split('T')[0],
|
||
approvers: {
|
||
writer: '노원호',
|
||
reviewer: '',
|
||
approver: '',
|
||
},
|
||
},
|
||
inspectionItems: [
|
||
{
|
||
id: 'appearance',
|
||
no: 1,
|
||
name: '겉모양',
|
||
standard: { description: '사용상 해로운 결함이 없을 것' },
|
||
inspectionMethod: '육안검사',
|
||
inspectionCycle: '',
|
||
measurementType: 'okng',
|
||
measurementCount: 3,
|
||
},
|
||
{
|
||
id: 'thickness',
|
||
no: 2,
|
||
name: '치수',
|
||
subName: '두께',
|
||
standard: {
|
||
value: 1.55,
|
||
options: [
|
||
{ id: 't1', label: '0.8 이상 ~ 1.0 미만', tolerance: '± 0.07', isSelected: false },
|
||
{ id: 't2', label: '1.0 이상 ~ 1.25 미만', tolerance: '± 0.08', isSelected: false },
|
||
{ id: 't3', label: '1.25 이상 ~ 1.6 미만', tolerance: '± 0.10', isSelected: true },
|
||
{ id: 't4', label: '1.6 이상 ~ 2.0 미만', tolerance: '± 0.12', isSelected: false },
|
||
],
|
||
},
|
||
inspectionMethod: 'n = 3\nc = 0',
|
||
inspectionCycle: '체크검사',
|
||
measurementType: 'numeric',
|
||
measurementCount: 3,
|
||
rowSpan: 3,
|
||
},
|
||
{
|
||
id: 'width',
|
||
no: 2,
|
||
name: '치수',
|
||
subName: '너비',
|
||
parentId: 'thickness',
|
||
standard: {
|
||
value: 1219,
|
||
options: [{ id: 'w1', label: '1250 미만', tolerance: '+ 7\n- 0', isSelected: true }],
|
||
},
|
||
inspectionMethod: '',
|
||
inspectionCycle: '',
|
||
measurementType: 'numeric',
|
||
measurementCount: 3,
|
||
isSubRow: true,
|
||
},
|
||
{
|
||
id: 'length',
|
||
no: 2,
|
||
name: '치수',
|
||
subName: '길이',
|
||
parentId: 'thickness',
|
||
standard: {
|
||
value: 480,
|
||
options: [{ id: 'l1', label: '2000 이상 ~ 4000 미만', tolerance: '+ 15\n- 0', isSelected: true }],
|
||
},
|
||
inspectionMethod: '',
|
||
inspectionCycle: '',
|
||
measurementType: 'numeric',
|
||
measurementCount: 3,
|
||
isSubRow: true,
|
||
},
|
||
{
|
||
id: 'tensileStrength',
|
||
no: 3,
|
||
name: '인장강도 (N/㎟)',
|
||
standard: { description: '270 이상' },
|
||
inspectionMethod: '',
|
||
inspectionCycle: '',
|
||
measurementType: 'numeric',
|
||
measurementCount: 1,
|
||
},
|
||
{
|
||
id: 'elongation',
|
||
no: 4,
|
||
name: '연신율 %',
|
||
standard: {
|
||
options: [
|
||
{ id: 'e1', label: '두께 0.6 이상 ~ 1.0 미만', tolerance: '36 이상', isSelected: false },
|
||
{ id: 'e2', label: '두께 1.0 이상 ~ 1.6 미만', tolerance: '37 이상', isSelected: true },
|
||
{ id: 'e3', label: '두께 1.6 이상 ~ 2.3 미만', tolerance: '38 이상', isSelected: false },
|
||
],
|
||
},
|
||
inspectionMethod: '공급업체\n밀시트',
|
||
inspectionCycle: '입고시',
|
||
measurementType: 'numeric',
|
||
measurementCount: 1,
|
||
},
|
||
{
|
||
id: 'zincCoating',
|
||
no: 5,
|
||
name: '아연의 최소 부착량 (g/㎡)',
|
||
standard: { description: '편면 17 이상' },
|
||
inspectionMethod: '',
|
||
inspectionCycle: '',
|
||
measurementType: 'numeric',
|
||
measurementCount: 2,
|
||
},
|
||
],
|
||
notes: [
|
||
'※ 1.55mm의 경우 KS F 4510에 따른 MIN 1.5의 기준에 따름',
|
||
'※ 두께의 경우 너비 1000 이상 ~ 1250 미만 기준에 따름',
|
||
],
|
||
};
|
||
|
||
return { success: true, data: mockTemplate };
|
||
}
|
||
|
||
// ===== 실제 API 호출 =====
|
||
try {
|
||
const searchParams = new URLSearchParams();
|
||
searchParams.set('item_name', params.itemName);
|
||
searchParams.set('specification', params.specification);
|
||
if (params.lotNo) searchParams.set('lot_no', params.lotNo);
|
||
if (params.supplier) searchParams.set('supplier', params.supplier);
|
||
|
||
const { response, error } = await serverFetch(
|
||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspection-templates?${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 = await response.json();
|
||
|
||
if (!response.ok || !result.success || !result.data) {
|
||
return { success: false, error: result.message || '검사 템플릿 조회에 실패했습니다.' };
|
||
}
|
||
|
||
return { success: true, data: result.data };
|
||
} catch (error) {
|
||
if (isNextRedirectError(error)) throw error;
|
||
console.error('[ReceivingActions] getInspectionTemplate error:', error);
|
||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||
}
|
||
} |