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)
This commit is contained in:
2026-01-09 16:58:50 +09:00
parent 749f0ce3c3
commit 5fa20c837a
3 changed files with 492 additions and 273 deletions

View File

@@ -1,187 +1,250 @@
'use server';
import type { Item, ItemStats, ItemListParams, ItemListResponse, ItemDetail, ItemFormData, OrderItem } from './types';
import type { Item, ItemStats, ItemListParams, ItemListResponse, ItemDetail, ItemFormData, OrderItem, ItemType, Specification, OrderType as FrontOrderType, ItemStatus } from './types';
import { apiClient } from '@/lib/api';
// 목데이터
const mockItems: Item[] = [
{
id: '1',
itemNumber: '123123',
itemType: '제품',
categoryId: '1',
categoryName: '카테고리명',
itemName: '품목명',
specification: '인정',
unit: 'SET',
orderType: '외주발주',
status: '승인',
createdAt: '2026-01-01T10:00:00Z',
updatedAt: '2026-01-01T10:00:00Z',
},
{
id: '2',
itemNumber: '123124',
itemType: '부품',
categoryId: '2',
categoryName: '모터',
itemName: '소형 모터 A',
specification: '비인정',
unit: 'SET',
orderType: '외주발주',
status: '승인',
createdAt: '2026-01-02T11:00:00Z',
updatedAt: '2026-01-02T11:00:00Z',
},
{
id: '3',
itemNumber: '123125',
itemType: '소모품',
categoryId: '3',
categoryName: '공정자재',
itemName: '절연테이프',
specification: '인정',
unit: 'SET',
orderType: '외주발주',
status: '승인',
createdAt: '2026-01-03T09:00:00Z',
updatedAt: '2026-01-03T09:00:00Z',
},
{
id: '4',
itemNumber: '123126',
itemType: '공과',
categoryId: '4',
categoryName: '철물',
itemName: '볼트 세트',
specification: '비인정',
unit: 'EA',
orderType: '경품발주',
status: '작업',
createdAt: '2026-01-03T10:00:00Z',
updatedAt: '2026-01-03T10:00:00Z',
},
{
id: '5',
itemNumber: '123127',
itemType: '부품',
categoryId: '1',
categoryName: '슬라이드 OPEN 사이즈',
itemName: '슬라이드 레일',
specification: '인정',
unit: 'EA',
orderType: '원자재발주',
status: '작업',
createdAt: '2026-01-04T08:00:00Z',
updatedAt: '2026-01-04T08:00:00Z',
},
{
id: '6',
itemNumber: '123128',
itemType: '소모품',
categoryId: '3',
categoryName: '공정자재',
itemName: '윤활유',
specification: '비인정',
unit: 'L',
orderType: '외주발주',
status: '사용',
createdAt: '2026-01-04T09:00:00Z',
updatedAt: '2026-01-04T09:00:00Z',
},
{
id: '7',
itemNumber: '123129',
itemType: '소모품',
categoryId: '3',
categoryName: '공정자재',
itemName: '포장재',
specification: '인정',
unit: 'BOX',
orderType: '경품발주',
status: '중지',
createdAt: '2026-01-05T10:00:00Z',
updatedAt: '2026-01-05T10:00:00Z',
},
];
// ========================================
// 타입 변환 함수
// ========================================
// 품목 목록 조회
/**
* 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 {
// 시뮬레이션 딜레이
await new Promise((resolve) => setTimeout(resolve, 300));
const queryParams: Record<string, string> = {};
let filteredItems = [...mockItems];
// 페이지네이션
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') {
filteredItems = filteredItems.filter((item) => item.itemType === params.itemType);
queryParams.type = transformToBackendItemType(params.itemType as ItemType);
}
// 카테고리 필터
if (params.categoryId && params.categoryId !== 'all') {
filteredItems = filteredItems.filter((item) => item.categoryId === params.categoryId);
queryParams.category_id = params.categoryId;
}
// 규격 필터
if (params.specification && params.specification !== 'all') {
filteredItems = filteredItems.filter((item) => item.specification === params.specification);
}
// 구분 필터
if (params.orderType && params.orderType !== 'all') {
filteredItems = filteredItems.filter((item) => item.orderType === params.orderType);
}
// 상태 필터
// 활성 상태 필터
if (params.status && params.status !== 'all') {
filteredItems = filteredItems.filter((item) => item.status === params.status);
queryParams.active = params.status === '사용' || params.status === '승인' ? '1' : '0';
}
// 검색어 필터
if (params.search) {
const search = params.search.toLowerCase();
filteredItems = filteredItems.filter(
(item) =>
item.itemNumber.toLowerCase().includes(search) ||
item.itemName.toLowerCase().includes(search) ||
item.categoryName.toLowerCase().includes(search)
);
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);
filteredItems = filteredItems.filter((item) => new Date(item.createdAt) >= 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);
filteredItems = filteredItems.filter((item) => new Date(item.createdAt) <= endDate);
transformedItems = transformedItems.filter((item) => new Date(item.createdAt) <= endDate);
}
// 정렬
// 정렬 (Frontend)
if (params.sortBy === 'oldest') {
filteredItems.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
transformedItems.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
} else {
// 기본: 최신순
filteredItems.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
transformedItems.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
}
// 페이지네이션
const page = params.page || 1;
const size = params.size || 20;
const start = (page - 1) * size;
const paginatedItems = filteredItems.slice(start, start + size);
return {
success: true,
data: {
items: paginatedItems,
total: filteredItems.length,
page,
size,
items: transformedItems,
total: meta.total,
page: meta.current_page,
size: meta.per_page,
},
};
} catch (error) {
@@ -190,17 +253,20 @@ export async function getItemList(
}
}
// 품목 통계 조회
/**
* 품목 통계 조회
* GET /api/v1/items/stats
*/
export async function getItemStats(): Promise<{ success: boolean; data?: ItemStats; error?: string }> {
try {
await new Promise((resolve) => setTimeout(resolve, 100));
const total = mockItems.length;
const active = mockItems.filter((item) => item.status === '사용' || item.status === '승인').length;
const response = await apiClient.get<{ total: number; active: number }>('/items/stats');
return {
success: true,
data: { total, active },
data: {
total: response.total,
active: response.active,
},
};
} catch (error) {
console.error('품목 통계 조회 오류:', error);
@@ -208,17 +274,13 @@ export async function getItemStats(): Promise<{ success: boolean; data?: ItemSta
}
}
// 품목 삭제
/**
* 품목 삭제
* DELETE /api/v1/items/{id}
*/
export async function deleteItem(id: string): Promise<{ success: boolean; error?: string }> {
try {
await new Promise((resolve) => setTimeout(resolve, 300));
// 실제 구현에서는 API 호출
const index = mockItems.findIndex((item) => item.id === id);
if (index !== -1) {
mockItems.splice(index, 1);
}
await apiClient.delete(`/items/${id}`);
return { success: true };
} catch (error) {
console.error('품목 삭제 오류:', error);
@@ -226,40 +288,43 @@ export async function deleteItem(id: string): Promise<{ success: boolean; error?
}
}
// 품목 일괄 삭제
/**
* 품목 일괄 삭제
* DELETE /api/v1/items/batch
*/
export async function deleteItems(
ids: string[]
): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
try {
await new Promise((resolve) => setTimeout(resolve, 500));
let deletedCount = 0;
ids.forEach((id) => {
const index = mockItems.findIndex((item) => item.id === id);
if (index !== -1) {
mockItems.splice(index, 1);
deletedCount++;
}
const response = await apiClient.delete<{ deleted_count: number }>('/items/batch', {
data: { ids: ids.map((id) => parseInt(id, 10)) },
});
return { success: true, deletedCount };
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 {
await new Promise((resolve) => setTimeout(resolve, 100));
const response = await apiClient.get<{
data: { id: number; name: string }[];
}>('/categories', { params: { size: 100 } });
// 유니크한 카테고리 추출
const categories = [...new Map(mockItems.map((item) => [item.categoryId, { id: item.categoryId, name: item.categoryName }])).values()];
const categories = response.data.map((cat) => ({
id: String(cat.id),
name: cat.name,
}));
return { success: true, data: categories };
} catch (error) {
@@ -268,93 +333,49 @@ export async function getCategoryOptions(): Promise<{
}
}
// 발주 항목 목데이터
const mockOrderItems: Record<string, OrderItem[]> = {
'1': [
{ id: 'oi1', label: '무게', value: '400KG' },
{ id: 'oi2', label: '무게', value: '500KG' },
],
'2': [
{ id: 'oi3', label: '전압', value: '220V' },
],
'3': [],
'4': [
{ id: 'oi4', label: '규격', value: 'M10x20' },
],
'5': [],
'6': [],
'7': [],
};
// 품목 상세 조회
/**
* 품목 상세 조회
* GET /api/v1/items/{id}
*/
export async function getItem(id: string): Promise<{
success: boolean;
data?: ItemDetail;
error?: string;
}> {
try {
await new Promise((resolve) => setTimeout(resolve, 200));
const response = await apiClient.get<ApiItem>(`/items/${id}`);
const item = mockItems.find((i) => i.id === id);
if (!item) {
return { success: false, error: '품목을 찾을 수 없습니다.' };
}
const itemDetail: ItemDetail = {
...item,
note: '',
orderItems: mockOrderItems[id] || [],
};
return { success: true, data: itemDetail };
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 {
await new Promise((resolve) => setTimeout(resolve, 300));
const apiData = transformItemToApi(data);
const response = await apiClient.post<{ id: number }>('/items', apiData);
// 새 ID 생성
const newId = String(Math.max(...mockItems.map((i) => parseInt(i.id))) + 1);
// 카테고리명 찾기
const category = mockItems.find((i) => i.categoryId === data.categoryId);
const categoryName = category?.categoryName || '기본';
const newItem: Item = {
id: newId,
itemNumber: data.itemNumber,
itemType: data.itemType,
categoryId: data.categoryId,
categoryName,
itemName: data.itemName,
specification: data.specification,
unit: data.unit,
orderType: data.orderType,
status: data.status,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
mockItems.push(newItem);
mockOrderItems[newId] = data.orderItems;
return { success: true, data: { id: newId } };
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
@@ -363,32 +384,8 @@ export async function updateItem(
error?: string;
}> {
try {
await new Promise((resolve) => setTimeout(resolve, 300));
const index = mockItems.findIndex((i) => i.id === id);
if (index === -1) {
return { success: false, error: '품목을 찾을 수 없습니다.' };
}
// 카테고리명 찾기
const category = mockItems.find((i) => i.categoryId === data.categoryId);
const categoryName = category?.categoryName || mockItems[index].categoryName;
mockItems[index] = {
...mockItems[index],
itemNumber: data.itemNumber,
itemType: data.itemType,
categoryId: data.categoryId,
categoryName,
itemName: data.itemName,
specification: data.specification,
unit: data.unit,
orderType: data.orderType,
status: data.status,
updatedAt: new Date().toISOString(),
};
mockOrderItems[id] = data.orderItems;
const apiData = transformItemToApi(data);
await apiClient.put(`/items/${id}`, apiData);
return { success: true };
} catch (error) {