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,5 +1,63 @@
# SAM React 작업 현황
## 2026-01-09 (목) - Phase 2.3 자재관리(품목관리) API 연동
### 작업 목표
- 시공사 페이지 API 연동 계획 Phase 2.3: 자재관리
- `item-management/actions.ts` Mock 데이터 → 실제 API 연동
### 수정된 파일
| 파일명 | 설명 |
|--------|------|
| `src/components/business/construction/item-management/actions.ts` | Mock → API 완전 재작성 |
| `claudedocs/[IMPL-2026-01-09] item-management-api-integration.md` | 구현 문서 |
### 주요 변경 내용
#### 1. 타입 변환 함수 추가
- `transformItemType()` - Backend item_type → Frontend itemType
- `transformToBackendItemType()` - Frontend itemType → Backend item_type
- `transformSpecification()` - Backend options → Frontend specification
- `transformOrderType()` - Backend options → Frontend orderType
- `transformStatus()` - Backend is_active + options → Frontend status
- `transformOrderItems()` - Backend options → Frontend orderItems
- `transformItem()` - API 응답 → Item 타입
- `transformItemDetail()` - API 응답 → ItemDetail 타입
- `transformItemToApi()` - ItemFormData → API 요청 데이터
#### 2. 품목유형 매핑
| Frontend | Backend |
|----------|---------|
| 제품 | FG |
| 부품 | PT |
| 소모품 | CS |
| 공과 | RM |
#### 3. API 함수 구현 (8개)
- `getItemList()` - GET /api/v1/items
- `getItemStats()` - GET /api/v1/items/stats
- `getItem()` - GET /api/v1/items/{id}
- `createItem()` - POST /api/v1/items
- `updateItem()` - PUT /api/v1/items/{id}
- `deleteItem()` - DELETE /api/v1/items/{id}
- `deleteItems()` - DELETE /api/v1/items/batch
- `getCategoryOptions()` - GET /api/v1/categories
#### 4. Frontend 전용 필터링
Backend에서 미지원 필터는 Frontend에서 처리:
- 규격 (specification)
- 구분 (orderType)
- 날짜 범위 (startDate, endDate)
- 정렬 (sortBy)
### 관련 API 변경 (api 저장소)
- `routes/api.php` - `/items/stats` 라우트 추가
### 관련 문서
- 구현 문서: `claudedocs/[IMPL-2026-01-09] item-management-api-integration.md`
---
## 2025-01-09 (목) - 작업지시 process_type → process_id FK 변환
### 작업 목표
@@ -78,7 +136,7 @@
---
## 2026-01-02 (목) - 견적 등록 자동산출 기능 구현
## 2025-01-02 (목) - 견적 등록 자동산출 기능 구현
### 작업 목표
- 견적 등록 화면에서 BOM 기반 자동산출 기능 구현
@@ -121,7 +179,7 @@
---
## 2026-01-02 (목) - 채권현황 동적월 지원 및 버그 수정
## 2025-01-02 (목) - 채권현황 동적월 지원 및 버그 수정
### 작업 목표
- "최근 1년" 필터 선택 시 동적 월 기간(최근 12개월) 지원
@@ -322,14 +380,16 @@ React 컴포넌트에서 Mock 데이터를 실제 API 호출로 교체하는 작
- [x] A-5 알림 설정 API 연동
- [x] A-6 거래처 원장 (API 미존재로 스킵)
#### Phase B (진행 중)
#### Phase B (✅ 완료)
- [x] B-1 매출관리 (SalesManagement) API 연동 ✅
- [x] B-2 매입관리 (PurchaseManagement) API 연동 ✅
- [x] B-2.1 매입 세금계산서 토글 기능 수정 ✅
- [ ] B-3 세금계산서 API 연동
- [ ] B-4 금관리 API 연동
- [ ] B-5 출금관리 API 연동
- [ ] B-6 미수금현황 API 연동
- [x] B-3 입금관리 (DepositManagement) API 연동
- [x] B-4 금관리 (WithdrawalManagement) API 연동
- [x] B-5 거래처관리 (VendorManagement) API 연동
- [x] B-6 어음관리 (BillManagement) API 연동
> **참고**: 원본 계획 문서(`docs/plans/react-mock-to-api-migration-plan.md`)의 Phase B 정의와 일치하도록 수정함
---
@@ -466,8 +526,16 @@ useEffect(() => {
---
#### Phase C (✅ 완료)
- [x] C-1 직원관리 (EmployeeManagement) API 연동 ✅
- [x] C-2 근태관리 (AttendanceManagement) API 연동 ✅
- [x] C-3 휴가관리 (VacationManagement) API 연동 ✅
> **참고**: Phase C는 이전 세션에서 완료됨 (확인: 2025-01-09)
### 다음 작업
- B-3 세금계산서 API 연동
- B-4 ~ B-6 회계관리 나머지 컴포넌트
- Phase D~L 진행 (계획 문서 참조)
- TODO-1: 결재선/참조 Select 변경 불가 문제 해결 ← 2025-01-09 수정 완료
- 채권현황 console.log 제거 ← 2025-01-09 정리 완료
---

View File

@@ -0,0 +1,154 @@
# [IMPL-2026-01-09] 자재관리(품목관리) API 연동
## 작업 개요
- **작업자**: Claude Code
- **작업일**: 2026-01-09
- **Phase**: 2.3 자재관리 (시공사 페이지 API 연동 계획)
- **이전 Phase**: 2.2 거래처관리 완료
## 변경 사항 요약
### Backend (api/)
#### 1. 라우트 추가
**파일**: `routes/api.php`
```php
// Items (통합 품목 관리 - items 테이블)
Route::prefix('items')->group(function () {
Route::get('', [ItemsController::class, 'index'])->name('v1.items.index');
Route::get('/stats', [ItemsController::class, 'stats'])->name('v1.items.stats'); // 신규
Route::post('', [ItemsController::class, 'store'])->name('v1.items.store');
Route::get('/code/{code}', [ItemsController::class, 'showByCode'])->name('v1.items.show_by_code');
Route::get('/{id}', [ItemsController::class, 'show'])->name('v1.items.show');
Route::put('/{id}', [ItemsController::class, 'update'])->name('v1.items.update');
Route::delete('/batch', [ItemsController::class, 'batchDestroy'])->name('v1.items.batch_destroy');
Route::delete('/{id}', [ItemsController::class, 'destroy'])->name('v1.items.destroy');
});
```
**중요**: `/stats` 라우트는 `/{id}` 보다 먼저 정의하여 "stats"가 ID로 캡처되는 것을 방지
### Frontend (react/)
#### 1. actions.ts 완전 재작성
**파일**: `src/components/business/construction/item-management/actions.ts`
**변경 전**: Mock 데이터 기반 (mockItems, mockOrderItems 배열)
**변경 후**: 실제 API 연동
#### 주요 구현 내용
##### 타입 변환 함수
| 함수명 | 용도 |
|--------|------|
| `transformItemType()` | Backend item_type → Frontend itemType |
| `transformToBackendItemType()` | Frontend itemType → Backend item_type |
| `transformSpecification()` | Backend options → Frontend specification |
| `transformOrderType()` | Backend options → Frontend orderType |
| `transformStatus()` | Backend is_active + options → Frontend status |
| `transformOrderItems()` | Backend options → Frontend orderItems |
| `transformItem()` | API 응답 → Item 타입 |
| `transformItemDetail()` | API 응답 → ItemDetail 타입 |
| `transformItemToApi()` | ItemFormData → API 요청 데이터 |
##### 품목 유형 매핑
| Frontend (Korean) | Backend (Code) |
|-------------------|----------------|
| 제품 | FG |
| 부품 | PT |
| 소모품 | CS (또는 SM) |
| 공과 | RM |
##### API 함수
| 함수명 | API Endpoint | 설명 |
|--------|-------------|------|
| `getItemList()` | GET /api/v1/items | 품목 목록 조회 |
| `getItemStats()` | GET /api/v1/items/stats | 품목 통계 조회 |
| `getItem()` | GET /api/v1/items/{id} | 품목 상세 조회 |
| `createItem()` | POST /api/v1/items | 품목 등록 |
| `updateItem()` | PUT /api/v1/items/{id} | 품목 수정 |
| `deleteItem()` | DELETE /api/v1/items/{id} | 품목 삭제 |
| `deleteItems()` | DELETE /api/v1/items/batch | 품목 일괄 삭제 |
| `getCategoryOptions()` | GET /api/v1/categories | 카테고리 목록 조회 |
##### Frontend 전용 필터링
Backend에서 지원하지 않는 필터는 Frontend에서 처리:
- 규격 (specification) 필터
- 구분 (orderType) 필터
- 날짜 범위 (startDate, endDate) 필터
- 정렬 (sortBy: latest/oldest)
## 필드 매핑 상세
### Item 기본 필드
| Frontend | Backend | 변환 방식 |
|----------|---------|----------|
| id | id | String 변환 |
| itemNumber | code | 직접 매핑 |
| itemName | name | 직접 매핑 |
| itemType | item_type | transformItemType() |
| categoryId | category_id | String 변환 |
| categoryName | category.name | nested 접근 |
| unit | unit | 직접 매핑 (기본값: EA) |
| specification | options.specification | transformSpecification() |
| orderType | options.orderType | transformOrderType() |
| status | is_active + options.status | transformStatus() |
| createdAt | created_at | 직접 매핑 |
| updatedAt | updated_at | 직접 매핑 |
### ItemDetail 추가 필드
| Frontend | Backend | 변환 방식 |
|----------|---------|----------|
| note | description | 직접 매핑 |
| orderItems | options.orderItems | transformOrderItems() |
## 테스트 체크리스트
### API 연동 확인
- [ ] 품목 목록 조회 (GET /items)
- [ ] 품목 통계 조회 (GET /items/stats)
- [ ] 품목 상세 조회 (GET /items/{id})
- [ ] 품목 등록 (POST /items)
- [ ] 품목 수정 (PUT /items/{id})
- [ ] 품목 삭제 (DELETE /items/{id})
- [ ] 품목 일괄 삭제 (DELETE /items/batch)
- [ ] 카테고리 목록 조회 (GET /categories)
### 필터링 확인
- [ ] 검색 필터 (search → q)
- [ ] 품목유형 필터 (itemType → type)
- [ ] 카테고리 필터 (categoryId → category_id)
- [ ] 활성상태 필터 (status → active)
- [ ] 규격 필터 (Frontend only)
- [ ] 구분 필터 (Frontend only)
- [ ] 날짜 필터 (Frontend only)
### 데이터 변환 확인
- [ ] 품목유형 한글 ↔ 코드 변환
- [ ] 상태값 변환 (is_active ↔ status)
- [ ] options JSON 필드 파싱/생성
## 관련 파일
### 수정된 파일
1. `api/routes/api.php` - /items/stats 라우트 추가
2. `react/src/components/business/construction/item-management/actions.ts` - Mock → API 변환
### 참조 파일
- `api/app/Http/Controllers/Api/V1/ItemsController.php`
- `api/app/Services/ItemService.php`
- `react/src/components/business/construction/item-management/types.ts`
- `react/src/lib/api.ts`
## 다음 단계
### Phase 2.4 예정
- 자재관리 (품목관리) UI 컴포넌트 연동 테스트
- 에러 핸들링 개선
- 로딩 상태 처리
### 향후 개선 사항
- Backend에서 추가 필터 지원 시 Frontend 필터 제거
- options 필드 구조 표준화
- 품목 일괄 등록 API 추가 고려

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) {