diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 5bfe18df..dbfd9227 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -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 정리 완료 --- diff --git a/claudedocs/[IMPL-2026-01-09] item-management-api-integration.md b/claudedocs/[IMPL-2026-01-09] item-management-api-integration.md new file mode 100644 index 00000000..e9f459e0 --- /dev/null +++ b/claudedocs/[IMPL-2026-01-09] item-management-api-integration.md @@ -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 추가 고려 \ No newline at end of file diff --git a/src/components/business/construction/item-management/actions.ts b/src/components/business/construction/item-management/actions.ts index 2673a9e3..ebb6f124 100644 --- a/src/components/business/construction/item-management/actions.ts +++ b/src/components/business/construction/item-management/actions.ts @@ -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 = { + 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 = { + '제품': 'FG', + '부품': 'PT', + '소모품': 'CS', + '공과': 'RM', + }; + return typeMap[frontendType] || 'FG'; +} + +/** + * Backend options → Frontend specification 변환 + */ +function transformSpecification(options: Record | null | undefined): Specification { + const spec = options?.specification; + if (spec === '인정' || spec === '비인정') return spec; + return '인정'; // 기본값 +} + +/** + * Backend options → Frontend orderType 변환 + */ +function transformOrderType(options: Record | 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 | 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 | 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 | 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 { + 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 = {}; - 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 = { - '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(`/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) {