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:
@@ -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 정리 완료
|
||||
|
||||
---
|
||||
|
||||
154
claudedocs/[IMPL-2026-01-09] item-management-api-integration.md
Normal file
154
claudedocs/[IMPL-2026-01-09] item-management-api-integration.md
Normal 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 추가 고려
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user