Files
sam-react-prod/src/components/business/construction/item-management/actions.ts
kent 5fa20c837a feat(item-management): Mock → API 연동 완료
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)
2026-01-09 16:58:50 +09:00

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: '품목 수정에 실패했습니다.' };
}
}