Phase 2.3 자재관리 API 연동: - actions.ts Mock 데이터 제거, 실제 API 연동 - 8개 API 함수 구현 (getItemList, getItemStats, getItem, createItem, updateItem, deleteItem, deleteItems, getCategoryOptions) - 타입 변환 함수 구현 (Frontend ↔ Backend) - 품목유형 매핑 (제품↔FG, 부품↔PT, 소모품↔CS, 공과↔RM) - Frontend 전용 필터링 (specification, orderType, dateRange, sortBy)
395 lines
12 KiB
TypeScript
395 lines
12 KiB
TypeScript
'use server';
|
|
|
|
import type { Item, ItemStats, ItemListParams, ItemListResponse, ItemDetail, ItemFormData, OrderItem, ItemType, Specification, OrderType as FrontOrderType, ItemStatus } from './types';
|
|
import { apiClient } from '@/lib/api';
|
|
|
|
// ========================================
|
|
// 타입 변환 함수
|
|
// ========================================
|
|
|
|
/**
|
|
* Backend item_type → Frontend itemType 변환
|
|
* FG → 제품, PT → 부품, SM → 소모품, CS → 소모품, RM → 공과
|
|
*/
|
|
function transformItemType(backendType: string | null | undefined): ItemType {
|
|
const typeMap: Record<string, ItemType> = {
|
|
FG: '제품',
|
|
PT: '부품',
|
|
SM: '소모품',
|
|
CS: '소모품',
|
|
RM: '공과',
|
|
};
|
|
return typeMap[backendType?.toUpperCase() || ''] || '제품';
|
|
}
|
|
|
|
/**
|
|
* Frontend itemType → Backend item_type 변환
|
|
* 제품 → FG, 부품 → PT, 소모품 → CS, 공과 → RM
|
|
*/
|
|
function transformToBackendItemType(frontendType: ItemType): string {
|
|
const typeMap: Record<ItemType, string> = {
|
|
'제품': 'FG',
|
|
'부품': 'PT',
|
|
'소모품': 'CS',
|
|
'공과': 'RM',
|
|
};
|
|
return typeMap[frontendType] || 'FG';
|
|
}
|
|
|
|
/**
|
|
* Backend options → Frontend specification 변환
|
|
*/
|
|
function transformSpecification(options: Record<string, unknown> | null | undefined): Specification {
|
|
const spec = options?.specification;
|
|
if (spec === '인정' || spec === '비인정') return spec;
|
|
return '인정'; // 기본값
|
|
}
|
|
|
|
/**
|
|
* Backend options → Frontend orderType 변환
|
|
*/
|
|
function transformOrderType(options: Record<string, unknown> | null | undefined): FrontOrderType {
|
|
const orderType = options?.orderType as string | undefined;
|
|
const validTypes: FrontOrderType[] = ['외주발주', '경품발주', '원자재발주'];
|
|
if (orderType && validTypes.includes(orderType as FrontOrderType)) {
|
|
return orderType as FrontOrderType;
|
|
}
|
|
return '외주발주'; // 기본값
|
|
}
|
|
|
|
/**
|
|
* Backend is_active + options → Frontend status 변환
|
|
*/
|
|
function transformStatus(isActive: boolean | null | undefined, options: Record<string, unknown> | null | undefined): ItemStatus {
|
|
const status = options?.status as string | undefined;
|
|
if (status === '승인' || status === '작업' || status === '사용' || status === '중지') {
|
|
return status;
|
|
}
|
|
return isActive ? '사용' : '중지';
|
|
}
|
|
|
|
/**
|
|
* Backend options → Frontend orderItems 변환
|
|
*/
|
|
function transformOrderItems(options: Record<string, unknown> | null | undefined): OrderItem[] {
|
|
const orderItems = options?.orderItems;
|
|
if (Array.isArray(orderItems)) {
|
|
return orderItems.map((item: { id?: string; label?: string; value?: string }, index: number) => ({
|
|
id: item.id || `oi_${index}`,
|
|
label: item.label || '',
|
|
value: item.value || '',
|
|
}));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* API 응답 → Item 타입 변환
|
|
*/
|
|
interface ApiItem {
|
|
id: number;
|
|
code: string;
|
|
name: string;
|
|
item_type: string | null;
|
|
category_id: number | null;
|
|
category?: { name?: string } | null;
|
|
unit: string | null;
|
|
options: Record<string, unknown> | null;
|
|
is_active: boolean;
|
|
description: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
function transformItem(apiItem: ApiItem): Item {
|
|
return {
|
|
id: String(apiItem.id),
|
|
itemNumber: apiItem.code || '',
|
|
itemName: apiItem.name || '',
|
|
itemType: transformItemType(apiItem.item_type),
|
|
categoryId: apiItem.category_id ? String(apiItem.category_id) : '',
|
|
categoryName: apiItem.category?.name || '',
|
|
unit: apiItem.unit || 'EA',
|
|
specification: transformSpecification(apiItem.options),
|
|
orderType: transformOrderType(apiItem.options),
|
|
status: transformStatus(apiItem.is_active, apiItem.options),
|
|
createdAt: apiItem.created_at,
|
|
updatedAt: apiItem.updated_at,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* API 응답 → ItemDetail 타입 변환
|
|
*/
|
|
function transformItemDetail(apiItem: ApiItem): ItemDetail {
|
|
return {
|
|
...transformItem(apiItem),
|
|
note: apiItem.description || '',
|
|
orderItems: transformOrderItems(apiItem.options),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ItemFormData → API 요청 데이터 변환
|
|
*/
|
|
function transformItemToApi(data: ItemFormData): Record<string, unknown> {
|
|
return {
|
|
code: data.itemNumber,
|
|
name: data.itemName,
|
|
item_type: transformToBackendItemType(data.itemType),
|
|
category_id: data.categoryId ? parseInt(data.categoryId, 10) : null,
|
|
unit: data.unit,
|
|
is_active: data.status === '사용' || data.status === '승인',
|
|
description: data.note || null,
|
|
options: {
|
|
specification: data.specification,
|
|
orderType: data.orderType,
|
|
status: data.status,
|
|
orderItems: data.orderItems,
|
|
},
|
|
};
|
|
}
|
|
|
|
// ========================================
|
|
// API 함수
|
|
// ========================================
|
|
|
|
/**
|
|
* 품목 목록 조회
|
|
* GET /api/v1/items
|
|
*/
|
|
export async function getItemList(
|
|
params: ItemListParams = {}
|
|
): Promise<{ success: boolean; data?: ItemListResponse; error?: string }> {
|
|
try {
|
|
const queryParams: Record<string, string> = {};
|
|
|
|
// 페이지네이션
|
|
if (params.page) queryParams.page = String(params.page);
|
|
if (params.size) queryParams.size = String(params.size);
|
|
|
|
// 검색
|
|
if (params.search) queryParams.q = params.search;
|
|
|
|
// 품목유형 필터 (Frontend → Backend 변환)
|
|
if (params.itemType && params.itemType !== 'all') {
|
|
queryParams.type = transformToBackendItemType(params.itemType as ItemType);
|
|
}
|
|
|
|
// 카테고리 필터
|
|
if (params.categoryId && params.categoryId !== 'all') {
|
|
queryParams.category_id = params.categoryId;
|
|
}
|
|
|
|
// 활성 상태 필터
|
|
if (params.status && params.status !== 'all') {
|
|
queryParams.active = params.status === '사용' || params.status === '승인' ? '1' : '0';
|
|
}
|
|
|
|
const response = await apiClient.get<{
|
|
data: ApiItem[];
|
|
meta?: { total: number; current_page: number; per_page: number };
|
|
total?: number;
|
|
current_page?: number;
|
|
per_page?: number;
|
|
}>('/items', { params: queryParams });
|
|
|
|
// API 응답 구조 처리 (data 배열 또는 페이지네이션 객체)
|
|
const items = Array.isArray(response.data) ? response.data : (response.data as unknown as ApiItem[]);
|
|
const meta = response.meta || {
|
|
total: response.total || items.length,
|
|
current_page: response.current_page || params.page || 1,
|
|
per_page: response.per_page || params.size || 20,
|
|
};
|
|
|
|
// Frontend 필터링 (Backend에서 지원하지 않는 필터)
|
|
let transformedItems = items.map(transformItem);
|
|
|
|
// 규격 필터 (Frontend)
|
|
if (params.specification && params.specification !== 'all') {
|
|
transformedItems = transformedItems.filter((item) => item.specification === params.specification);
|
|
}
|
|
|
|
// 구분 필터 (Frontend)
|
|
if (params.orderType && params.orderType !== 'all') {
|
|
transformedItems = transformedItems.filter((item) => item.orderType === params.orderType);
|
|
}
|
|
|
|
// 상태 필터 (Frontend에서 추가 처리)
|
|
if (params.status && params.status !== 'all') {
|
|
transformedItems = transformedItems.filter((item) => item.status === params.status);
|
|
}
|
|
|
|
// 날짜 필터 (Frontend)
|
|
if (params.startDate) {
|
|
const startDate = new Date(params.startDate);
|
|
transformedItems = transformedItems.filter((item) => new Date(item.createdAt) >= startDate);
|
|
}
|
|
if (params.endDate) {
|
|
const endDate = new Date(params.endDate);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
transformedItems = transformedItems.filter((item) => new Date(item.createdAt) <= endDate);
|
|
}
|
|
|
|
// 정렬 (Frontend)
|
|
if (params.sortBy === 'oldest') {
|
|
transformedItems.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
} else {
|
|
transformedItems.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
items: transformedItems,
|
|
total: meta.total,
|
|
page: meta.current_page,
|
|
size: meta.per_page,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('품목 목록 조회 오류:', error);
|
|
return { success: false, error: '품목 목록을 불러오는데 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 품목 통계 조회
|
|
* GET /api/v1/items/stats
|
|
*/
|
|
export async function getItemStats(): Promise<{ success: boolean; data?: ItemStats; error?: string }> {
|
|
try {
|
|
const response = await apiClient.get<{ total: number; active: number }>('/items/stats');
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
total: response.total,
|
|
active: response.active,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('품목 통계 조회 오류:', error);
|
|
return { success: false, error: '품목 통계를 불러오는데 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 품목 삭제
|
|
* DELETE /api/v1/items/{id}
|
|
*/
|
|
export async function deleteItem(id: string): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
await apiClient.delete(`/items/${id}`);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('품목 삭제 오류:', error);
|
|
return { success: false, error: '품목 삭제에 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 품목 일괄 삭제
|
|
* DELETE /api/v1/items/batch
|
|
*/
|
|
export async function deleteItems(
|
|
ids: string[]
|
|
): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
|
|
try {
|
|
const response = await apiClient.delete<{ deleted_count: number }>('/items/batch', {
|
|
data: { ids: ids.map((id) => parseInt(id, 10)) },
|
|
});
|
|
|
|
return { success: true, deletedCount: response.deleted_count };
|
|
} catch (error) {
|
|
console.error('품목 일괄 삭제 오류:', error);
|
|
return { success: false, error: '품목 일괄 삭제에 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 카테고리 목록 조회 (필터용)
|
|
* GET /api/v1/categories
|
|
*/
|
|
export async function getCategoryOptions(): Promise<{
|
|
success: boolean;
|
|
data?: { id: string; name: string }[];
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const response = await apiClient.get<{
|
|
data: { id: number; name: string }[];
|
|
}>('/categories', { params: { size: 100 } });
|
|
|
|
const categories = response.data.map((cat) => ({
|
|
id: String(cat.id),
|
|
name: cat.name,
|
|
}));
|
|
|
|
return { success: true, data: categories };
|
|
} catch (error) {
|
|
console.error('카테고리 목록 조회 오류:', error);
|
|
return { success: false, error: '카테고리 목록을 불러오는데 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 품목 상세 조회
|
|
* GET /api/v1/items/{id}
|
|
*/
|
|
export async function getItem(id: string): Promise<{
|
|
success: boolean;
|
|
data?: ItemDetail;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const response = await apiClient.get<ApiItem>(`/items/${id}`);
|
|
|
|
return { success: true, data: transformItemDetail(response) };
|
|
} catch (error) {
|
|
console.error('품목 상세 조회 오류:', error);
|
|
return { success: false, error: '품목 정보를 불러오는데 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 품목 등록
|
|
* POST /api/v1/items
|
|
*/
|
|
export async function createItem(data: ItemFormData): Promise<{
|
|
success: boolean;
|
|
data?: { id: string };
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const apiData = transformItemToApi(data);
|
|
const response = await apiClient.post<{ id: number }>('/items', apiData);
|
|
|
|
return { success: true, data: { id: String(response.id) } };
|
|
} catch (error) {
|
|
console.error('품목 등록 오류:', error);
|
|
return { success: false, error: '품목 등록에 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 품목 수정
|
|
* PUT /api/v1/items/{id}
|
|
*/
|
|
export async function updateItem(
|
|
id: string,
|
|
data: ItemFormData
|
|
): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const apiData = transformItemToApi(data);
|
|
await apiClient.put(`/items/${id}`, apiData);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('품목 수정 오류:', error);
|
|
return { success: false, error: '품목 수정에 실패했습니다.' };
|
|
}
|
|
} |