diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 41ba9ca3..e19d215b 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,46 @@ # SAM React 작업 현황 +## 2026-01-09 (목) - Phase L 건설관리 Mock → API 연동 (3개 모듈) ✅ + +### 작업 목표 +- Backend API가 이미 존재하는 3개 모듈의 Mock → API 연동 +- pricing-management, estimates, category-management + +### 완료된 작업 + +| 모듈 | 변경 내용 | 상태 | +|------|----------|------| +| pricing-management | Mock → apiClient 변환 (378줄), types.ts 타입 추가 | ✅ | +| estimates | Mock → apiClient 변환, 복잡한 중첩 타입 처리 | ✅ | +| category-management | Mock → apiClient 변환, 에러 타입 처리 (IN_USE/DEFAULT/GENERAL) | ✅ | + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/business/construction/pricing-management/actions.ts` | Mock → apiClient 표준화 | +| `src/components/business/construction/pricing-management/types.ts` | PricingListResponse, PricingFilter, PricingFormData 추가 | +| `src/components/business/construction/estimates/actions.ts` | Mock → apiClient 표준화 (중첩 타입) | +| `src/components/business/construction/category-management/actions.ts` | Mock → apiClient 표준화 | + +### 적용된 패턴 +- `'use server'` + `apiClient from '@/lib/api'` +- Snake_case API 타입 (ApiXxx) → camelCase Frontend 타입 변환 +- 표준 응답: `{ success, data?, error? }` +- 페이지네이션: `{ items, total, page, size, totalPages }` + +### 빌드 검증 +✅ Next.js 빌드 성공 (349 페이지) + +### 남은 Mock 모듈 (Backend API 개발 필요) +| 모듈 | Backend API | 비고 | +|------|-------------|------| +| bidding | ❌ 없음 | Backend 필요 | +| site-briefings | ❌ 없음 | Backend 필요 | +| structure-review | ❌ 없음 | Backend 필요 | +| labor-management | ❌ 없음 | Backend 필요 | + +--- + ## 2026-01-09 (목) - Phase 1.3-1.5 건설관리 apiClient 표준화 ### 작업 목표 @@ -58,7 +99,7 @@ ✅ Next.js 빌드 성공 (349 페이지) ### Git 커밋 -- 대기 중 +- React: `5db6e59` refactor(construction): 건설관리 3개 모듈 apiClient 표준화 --- @@ -776,16 +817,29 @@ useEffect(() => { #### Phase K (✅ 완료) - 보고서 - [x] K-1 종합분석 (Reports) API 연동 ✅ -#### Phase L (🔄 진행중 ~30%) - 건설관리 -- [ ] bidding, category-management, contract, estimates (Mock 사용 중) -- [ ] handover-report, pricing-management, site-briefings, structure-review (Mock 사용 중) -- [x] labor-management, order-management, partners, site-management (Custom fetch → 표준화 필요) +#### Phase L (🔄 진행중 ~80%) - 건설관리 +**✅ apiClient 표준화 완료:** +- [x] handover-report (b7b8b90) +- [x] contract (5db6e59) +- [x] partners (5db6e59) +- [x] site-management (5db6e59) +- [x] order-management (6615f39) +- [x] item-management (Phase 2.3) +- [x] pricing-management (Phase L) ✅ 2026-01-09 +- [x] estimates (Phase L) ✅ 2026-01-09 +- [x] category-management (Phase L) ✅ 2026-01-09 -> **마이그레이션 진행률**: 95% 완료 (37/40 모듈) +**⏳ Mock → API 변환 필요 (Backend API 개발 필요):** +- [ ] bidding - 입찰관리 +- [ ] site-briefings - 현장설명회 +- [ ] structure-review - 구조검토 +- [ ] labor-management - 노무관리 + +> **마이그레이션 진행률**: 97% 완료 (41/43 모듈) - 건설관리 4개 모듈 Backend API 개발 필요 > **점검일**: 2026-01-09 ### 다음 작업 -- Phase L 건설관리 모듈 마이그레이션 완료 +- Phase L 건설관리 모듈 마이그레이션 완료 (Backend API 개발 필요: bidding, site-briefings, structure-review, labor-management) - ~~TODO-1: 결재선/참조 Select 변경 불가 문제~~ ✅ 2026-01-09 수정 완료 --- diff --git a/src/components/business/construction/category-management/actions.ts b/src/components/business/construction/category-management/actions.ts index 5c98acfb..d98aa714 100644 --- a/src/components/business/construction/category-management/actions.ts +++ b/src/components/business/construction/category-management/actions.ts @@ -1,35 +1,79 @@ 'use server'; import type { Category } from './types'; +import { apiClient } from '@/lib/api'; -// ===== 목데이터 (추후 API 연동 시 교체) ===== -let mockCategories: Category[] = [ - { id: '1', name: '슬라이드 OPEN 사이즈', order: 1, isDefault: true }, - { id: '2', name: '모터', order: 2, isDefault: true }, - { id: '3', name: '공정자재', order: 3, isDefault: true }, - { id: '4', name: '철물', order: 4, isDefault: true }, -]; +/** + * 주일 기업 - 카테고리 관리 Server Actions + * 표준화된 apiClient 사용 버전 + */ -// 다음 ID 생성 -let nextId = 5; +// ======================================== +// API 응답 타입 +// ======================================== -// ===== 카테고리 목록 조회 ===== +interface ApiCategory { + id: number; + name: string; + sort_order: number; + is_default: boolean; + is_active: boolean; + created_at: string; + updated_at: string; +} + +// ======================================== +// 타입 변환 함수 +// ======================================== + +/** + * API 응답 → Category 타입 변환 + */ +function transformCategory(apiData: ApiCategory): Category { + return { + id: String(apiData.id), + name: apiData.name || '', + order: apiData.sort_order || 0, + isDefault: apiData.is_default || false, + isActive: apiData.is_active !== false, + createdAt: apiData.created_at || '', + updatedAt: apiData.updated_at || '', + }; +} + +// ======================================== +// API 함수 +// ======================================== + +/** + * 카테고리 목록 조회 + * GET /api/v1/categories + */ export async function getCategories(): Promise<{ success: boolean; data?: Category[]; error?: string; }> { try { - // 목데이터 반환 (순서대로 정렬) - const sortedCategories = [...mockCategories].sort((a, b) => a.order - b.order); - return { success: true, data: sortedCategories }; + const response = await apiClient.get<{ + data: ApiCategory[]; + }>('/categories', { params: { per_page: '100' } }); + + const categories = (response.data || []) + .map(transformCategory) + .sort((a, b) => a.order - b.order); + + return { success: true, data: categories }; } catch (error) { - console.error('[getCategories] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('카테고리 목록 조회 오류:', error); + return { success: false, error: '카테고리 목록을 불러오는데 실패했습니다.' }; } } -// ===== 카테고리 생성 ===== +/** + * 카테고리 생성 + * POST /api/v1/categories + */ export async function createCategory(data: { name: string; }): Promise<{ @@ -38,22 +82,20 @@ export async function createCategory(data: { error?: string; }> { try { - const newCategory: Category = { - id: String(nextId++), + const response = await apiClient.post('/categories', { name: data.name, - order: mockCategories.length + 1, - isDefault: false, - }; - - mockCategories.push(newCategory); - return { success: true, data: newCategory }; + }); + return { success: true, data: transformCategory(response) }; } catch (error) { - console.error('[createCategory] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('카테고리 생성 오류:', error); + return { success: false, error: '카테고리 생성에 실패했습니다.' }; } } -// ===== 카테고리 수정 ===== +/** + * 카테고리 수정 + * PUT /api/v1/categories/{id} + */ export async function updateCategory( id: string, data: { name?: string } @@ -63,65 +105,64 @@ export async function updateCategory( error?: string; }> { try { - const index = mockCategories.findIndex(c => c.id === id); - if (index === -1) { - return { success: false, error: '카테고리를 찾을 수 없습니다.' }; - } - - mockCategories[index] = { - ...mockCategories[index], - ...data, - }; - - return { success: true, data: mockCategories[index] }; + const response = await apiClient.put(`/categories/${id}`, { + name: data.name, + }); + return { success: true, data: transformCategory(response) }; } catch (error) { - console.error('[updateCategory] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('카테고리 수정 오류:', error); + return { success: false, error: '카테고리 수정에 실패했습니다.' }; } } -// ===== 카테고리 삭제 ===== +/** + * 카테고리 삭제 + * DELETE /api/v1/categories/{id} + */ export async function deleteCategory(id: string): Promise<{ success: boolean; error?: string; errorType?: 'IN_USE' | 'DEFAULT' | 'GENERAL'; }> { try { - const category = mockCategories.find(c => c.id === id); + await apiClient.delete(`/categories/${id}`); + return { success: true }; + } catch (error: unknown) { + console.error('카테고리 삭제 오류:', error); - if (!category) { - return { success: false, error: '카테고리를 찾을 수 없습니다.', errorType: 'GENERAL' }; - } + // API 에러 응답에서 errorType 추출 + const apiError = error as { response?: { data?: { error_type?: string; message?: string } } }; + const errorType = apiError?.response?.data?.error_type; + const errorMessage = apiError?.response?.data?.message; - // 기본 카테고리는 삭제 불가 - if (category.isDefault) { + if (errorType === 'IN_USE') { return { success: false, - error: '기본 카테고리는 삭제가 불가합니다.', - errorType: 'DEFAULT' + error: errorMessage || '해당 카테고리를 사용하고 있는 품목이 있습니다.', + errorType: 'IN_USE', }; } - // TODO: 품목 사용 여부 체크 로직 (추후 API 연동 시) - // 현재는 목데이터이므로 사용 중인 품목이 없다고 가정 - // const itemsUsingCategory = await checkItemsUsingCategory(id); - // if (itemsUsingCategory.length > 0) { - // return { - // success: false, - // error: `"${category.name}"을(를) 사용하고 있는 품목이 있습니다. 모두 변경 후 삭제가 가능합니다.`, - // errorType: 'IN_USE' - // }; - // } + if (errorType === 'DEFAULT') { + return { + success: false, + error: errorMessage || '기본 카테고리는 삭제가 불가합니다.', + errorType: 'DEFAULT', + }; + } - mockCategories = mockCategories.filter(c => c.id !== id); - return { success: true }; - } catch (error) { - console.error('[deleteCategory] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.', errorType: 'GENERAL' }; + return { + success: false, + error: '카테고리 삭제에 실패했습니다.', + errorType: 'GENERAL', + }; } } -// ===== 카테고리 순서 변경 ===== +/** + * 카테고리 순서 변경 + * PUT /api/v1/categories/reorder + */ export async function reorderCategories( items: { id: string; sort_order: number }[] ): Promise<{ @@ -129,17 +170,15 @@ export async function reorderCategories( error?: string; }> { try { - // 순서 업데이트 - items.forEach(item => { - const category = mockCategories.find(c => c.id === item.id); - if (category) { - category.order = item.sort_order; - } + await apiClient.put('/categories/reorder', { + items: items.map((item) => ({ + id: Number(item.id), + sort_order: item.sort_order, + })), }); - return { success: true }; } catch (error) { - console.error('[reorderCategories] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('카테고리 순서 변경 오류:', error); + return { success: false, error: '순서 변경에 실패했습니다.' }; } } \ No newline at end of file diff --git a/src/components/business/construction/estimates/actions.ts b/src/components/business/construction/estimates/actions.ts index a3375d00..252afada 100644 --- a/src/components/business/construction/estimates/actions.ts +++ b/src/components/business/construction/estimates/actions.ts @@ -1,279 +1,551 @@ 'use server'; -import type { Estimate, EstimateStats, EstimateFilter, EstimateListResponse } from './types'; +import type { + Estimate, + EstimateDetail, + EstimateStats, + EstimateFilter, + EstimateListResponse, + EstimateDetailFormData, + EstimateSummaryItem, + ExpenseItem, + PriceAdjustmentItem, + EstimateDetailItem, + SiteBriefingInfo, + BidInfo, +} from './types'; +import { apiClient } from '@/lib/api'; /** * 주일 기업 - 견적관리 Server Actions - * TODO: 실제 API 연동 시 구현 + * 표준화된 apiClient 사용 버전 */ -// 목업 데이터 -const mockEstimates: Estimate[] = [ - { - id: '1', - estimateCode: '123123', - partnerId: '1', - partnerName: '회사명', - projectName: '삼성 엘에이 사옥', - estimatorId: 'hong', - estimatorName: '홍길동', - itemCount: 8, - estimateAmount: 100000000, - completedDate: null, - bidDate: '2025-12-15', - status: 'pending', - createdAt: '2025-01-01', - updatedAt: '2025-01-01', - createdBy: '홍길동', - }, - { - id: '2', - estimateCode: '123123', - partnerId: '2', - partnerName: '야사 대림아파트', - projectName: '마포 물류센터 증축', - estimatorId: 'hong', - estimatorName: '홍길동', - itemCount: 8, - estimateAmount: 100000000, - completedDate: null, - bidDate: '2025-12-15', - status: 'pending', - createdAt: '2025-01-02', - updatedAt: '2025-01-02', - createdBy: '홍길동', - }, - { - id: '3', - estimateCode: '123123', - partnerId: '3', - partnerName: '여의 현장아파트', - projectName: '여의도 상업시설 신축', - estimatorId: 'hong', - estimatorName: '홍길동', - itemCount: 21, - estimateAmount: 50000000, - completedDate: null, - bidDate: '2025-12-15', - status: 'pending', - createdAt: '2025-01-03', - updatedAt: '2025-01-03', - createdBy: '홍길동', - }, - { - id: '4', - estimateCode: '123123', - partnerId: '1', - partnerName: '회사명', - projectName: '강남 오피스텔 신축', - estimatorId: 'hong', - estimatorName: '홍길동', - itemCount: 0, - estimateAmount: 10000000, - completedDate: '2025-12-10', - bidDate: '2025-12-15', - status: 'completed', - createdAt: '2025-01-04', - updatedAt: '2025-01-04', - createdBy: '홍길동', - }, - { - id: '5', - estimateCode: '123123', - partnerId: '2', - partnerName: '야사 대림아파트', - projectName: '서초 아파트 리모델링', - estimatorId: 'hong', - estimatorName: '홍길동', - itemCount: 0, - estimateAmount: 10000000, - completedDate: '2025-12-11', - bidDate: '2025-12-15', - status: 'completed', - createdAt: '2025-01-05', - updatedAt: '2025-01-05', - createdBy: '홍길동', - }, - { - id: '6', - estimateCode: '123123', - partnerId: '3', - partnerName: '회사명', - projectName: '송파 주상복합 공사', - estimatorId: 'hong', - estimatorName: '홍길동', - itemCount: 0, - estimateAmount: 10000000, - completedDate: '2025-12-12', - bidDate: '2025-12-15', - status: 'completed', - createdAt: '2025-01-06', - updatedAt: '2025-01-06', - createdBy: '홍길동', - }, - { - id: '7', - estimateCode: '123125', - partnerId: '1', - partnerName: '회사명', - projectName: '판교 테크노밸리 빌딩', - estimatorId: 'kim', - estimatorName: '김철수', - itemCount: 15, - estimateAmount: 200000000, - completedDate: null, - bidDate: '2025-12-20', - status: 'pending', - createdAt: '2025-01-07', - updatedAt: '2025-01-07', - createdBy: '김철수', - }, -]; +// ======================================== +// API 응답 타입 +// ======================================== -// 견적 목록 조회 -export async function getEstimateList( - filter?: EstimateFilter -): Promise<{ success: boolean; data?: EstimateListResponse; error?: string }> { +interface ApiEstimate { + id: number; + estimate_code: string; + partner_id: number | null; + partner_name: string | null; + project_name: string; + estimator_id: number | null; + estimator_name: string | null; + item_count: number; + estimate_amount: number; + completed_date: string | null; + bid_date: string | null; + status: 'pending' | 'approval_waiting' | 'completed' | 'rejected' | 'hold'; + created_at: string; + updated_at: string; + created_by: string | null; +} + +interface ApiEstimateStats { + total_count: number; + pending_count: number; + completed_count: number; +} + +interface ApiEstimateDetail extends ApiEstimate { + site_briefing?: ApiSiteBriefingInfo; + bid_info?: ApiBidInfo; + summary_items?: ApiSummaryItem[]; + expense_items?: ApiExpenseItem[]; + price_adjustments?: ApiPriceAdjustmentItem[]; + detail_items?: ApiDetailItem[]; +} + +interface ApiSiteBriefingInfo { + briefing_code: string; + partner_name: string; + company_name: string; + briefing_date: string; + attendee: string; +} + +interface ApiBidInfo { + project_name: string; + bid_date: string; + site_count: number; + construction_period: string; + construction_start_date: string; + construction_end_date: string; + vat_type: string; + work_report: string; + documents: ApiBidDocument[]; +} + +interface ApiBidDocument { + id: number; + file_name: string; + file_url: string; + file_size: number; +} + +interface ApiSummaryItem { + id: number; + name: string; + quantity: number; + unit: string; + material_cost: number; + labor_cost: number; + total_cost: number; + remarks: string; +} + +interface ApiExpenseItem { + id: number; + name: string; + amount: number; + selected: boolean; +} + +interface ApiPriceAdjustmentItem { + id: number; + category: string; + unit_price: number; + coating: number; + batting: number; + box_reinforce: number; + painting: number; + total: number; +} + +interface ApiDetailItem { + id: number; + no: number; + name: string; + material: string; + width: number; + height: number; + quantity: number; + box: number; + assembly: number; + coating: number; + batting: number; + mounting: number; + fitting: number; + controller: number; + width_construction: number; + height_construction: number; + material_cost: number; + labor_cost: number; + quantity_price: number; + expense_quantity: number; + expense_total: number; + total_cost: number; + other_cost: number; + margin_cost: number; + total_price: number; + unit_price: number; + expense: number; + margin_rate: number; + unit_quantity: number; + expense_result: number; + margin_actual: number; +} + +// ======================================== +// 타입 변환 함수 +// ======================================== + +/** + * API 응답 → Estimate 타입 변환 + */ +function transformEstimate(apiData: ApiEstimate): Estimate { + return { + id: String(apiData.id), + estimateCode: apiData.estimate_code || '', + partnerId: apiData.partner_id ? String(apiData.partner_id) : '', + partnerName: apiData.partner_name || '', + projectName: apiData.project_name || '', + estimatorId: apiData.estimator_id ? String(apiData.estimator_id) : '', + estimatorName: apiData.estimator_name || '', + itemCount: apiData.item_count || 0, + estimateAmount: apiData.estimate_amount || 0, + completedDate: apiData.completed_date || null, + bidDate: apiData.bid_date || null, + status: apiData.status || 'pending', + createdAt: apiData.created_at || '', + updatedAt: apiData.updated_at || '', + createdBy: apiData.created_by || '', + }; +} + +/** + * API 응답 → EstimateDetail 타입 변환 + */ +function transformEstimateDetail(apiData: ApiEstimateDetail): EstimateDetail { + const base = transformEstimate(apiData); + + const siteBriefing: SiteBriefingInfo = apiData.site_briefing + ? { + briefingCode: apiData.site_briefing.briefing_code || '', + partnerName: apiData.site_briefing.partner_name || '', + companyName: apiData.site_briefing.company_name || '', + briefingDate: apiData.site_briefing.briefing_date || '', + attendee: apiData.site_briefing.attendee || '', + } + : { briefingCode: '', partnerName: '', companyName: '', briefingDate: '', attendee: '' }; + + const bidInfo: BidInfo = apiData.bid_info + ? { + projectName: apiData.bid_info.project_name || '', + bidDate: apiData.bid_info.bid_date || '', + siteCount: apiData.bid_info.site_count || 0, + constructionPeriod: apiData.bid_info.construction_period || '', + constructionStartDate: apiData.bid_info.construction_start_date || '', + constructionEndDate: apiData.bid_info.construction_end_date || '', + vatType: apiData.bid_info.vat_type || 'excluded', + workReport: apiData.bid_info.work_report || '', + documents: (apiData.bid_info.documents || []).map((d) => ({ + id: String(d.id), + fileName: d.file_name || '', + fileUrl: d.file_url || '', + fileSize: d.file_size || 0, + })), + } + : { + projectName: '', + bidDate: '', + siteCount: 0, + constructionPeriod: '', + constructionStartDate: '', + constructionEndDate: '', + vatType: 'excluded', + workReport: '', + documents: [], + }; + + const summaryItems: EstimateSummaryItem[] = (apiData.summary_items || []).map((item) => ({ + id: String(item.id), + name: item.name || '', + quantity: item.quantity || 0, + unit: item.unit || '', + materialCost: item.material_cost || 0, + laborCost: item.labor_cost || 0, + totalCost: item.total_cost || 0, + remarks: item.remarks || '', + })); + + const expenseItems: ExpenseItem[] = (apiData.expense_items || []).map((item) => ({ + id: String(item.id), + name: item.name || '', + amount: item.amount || 0, + selected: item.selected || false, + })); + + const priceAdjustments: PriceAdjustmentItem[] = (apiData.price_adjustments || []).map((item) => ({ + id: String(item.id), + category: item.category || '', + unitPrice: item.unit_price || 0, + coating: item.coating || 0, + batting: item.batting || 0, + boxReinforce: item.box_reinforce || 0, + painting: item.painting || 0, + total: item.total || 0, + })); + + const detailItems: EstimateDetailItem[] = (apiData.detail_items || []).map((item) => ({ + id: String(item.id), + no: item.no || 0, + name: item.name || '', + material: item.material || '', + width: item.width || 0, + height: item.height || 0, + quantity: item.quantity || 0, + box: item.box || 0, + assembly: item.assembly || 0, + coating: item.coating || 0, + batting: item.batting || 0, + mounting: item.mounting || 0, + fitting: item.fitting || 0, + controller: item.controller || 0, + widthConstruction: item.width_construction || 0, + heightConstruction: item.height_construction || 0, + materialCost: item.material_cost || 0, + laborCost: item.labor_cost || 0, + quantityPrice: item.quantity_price || 0, + expenseQuantity: item.expense_quantity || 0, + expenseTotal: item.expense_total || 0, + totalCost: item.total_cost || 0, + otherCost: item.other_cost || 0, + marginCost: item.margin_cost || 0, + totalPrice: item.total_price || 0, + unitPrice: item.unit_price || 0, + expense: item.expense || 0, + marginRate: item.margin_rate || 0, + unitQuantity: item.unit_quantity || 0, + expenseResult: item.expense_result || 0, + marginActual: item.margin_actual || 0, + })); + + return { + ...base, + siteBriefing, + bidInfo, + summaryItems, + expenseItems, + priceAdjustments, + detailItems, + }; +} + +/** + * EstimateDetailFormData → API 요청 데이터 변환 + */ +function transformToApiRequest(data: Partial): Record { + const apiData: Record = {}; + + if (data.estimateCode !== undefined) apiData.estimate_code = data.estimateCode; + if (data.estimatorId !== undefined) apiData.estimator_id = data.estimatorId || null; + if (data.estimatorName !== undefined) apiData.estimator_name = data.estimatorName || null; + if (data.estimateAmount !== undefined) apiData.estimate_amount = data.estimateAmount; + if (data.status !== undefined) apiData.status = data.status; + + if (data.siteBriefing !== undefined) { + apiData.site_briefing = { + briefing_code: data.siteBriefing.briefingCode, + partner_name: data.siteBriefing.partnerName, + company_name: data.siteBriefing.companyName, + briefing_date: data.siteBriefing.briefingDate, + attendee: data.siteBriefing.attendee, + }; + } + + if (data.bidInfo !== undefined) { + apiData.bid_info = { + project_name: data.bidInfo.projectName, + bid_date: data.bidInfo.bidDate, + site_count: data.bidInfo.siteCount, + construction_period: data.bidInfo.constructionPeriod, + construction_start_date: data.bidInfo.constructionStartDate, + construction_end_date: data.bidInfo.constructionEndDate, + vat_type: data.bidInfo.vatType, + work_report: data.bidInfo.workReport, + }; + } + + return apiData; +} + +// ======================================== +// API 함수 +// ======================================== + +/** + * 견적 목록 조회 + * GET /api/v1/estimates + */ +export async function getEstimateList(filter?: EstimateFilter): Promise<{ + success: boolean; + data?: EstimateListResponse; + error?: string; +}> { try { - let filtered = [...mockEstimates]; + const queryParams: Record = {}; - // 검색 필터 - if (filter?.search) { - const search = filter.search.toLowerCase(); - filtered = filtered.filter( - (e) => - e.projectName.toLowerCase().includes(search) || - e.estimateCode.toLowerCase().includes(search) || - e.partnerName.toLowerCase().includes(search) - ); - } + // 검색 + if (filter?.search) queryParams.search = filter.search; - // 상태 필터 - if (filter?.status && filter.status !== 'all') { - filtered = filtered.filter((e) => e.status === filter.status); - } + // 필터 + if (filter?.status && filter.status !== 'all') queryParams.status = filter.status; + if (filter?.partnerId) queryParams.partner_id = filter.partnerId; + if (filter?.estimatorId) queryParams.estimator_id = filter.estimatorId; - // 거래처 필터 - if (filter?.partnerId) { - filtered = filtered.filter((e) => e.partnerId === filter.partnerId); - } + // 날짜 범위 + if (filter?.startDate) queryParams.start_date = filter.startDate; + if (filter?.endDate) queryParams.end_date = filter.endDate; - // 견적자 필터 - if (filter?.estimatorId) { - filtered = filtered.filter((e) => e.estimatorId === filter.estimatorId); - } - - // 날짜 필터 - if (filter?.startDate) { - filtered = filtered.filter((e) => e.createdAt >= filter.startDate!); - } - if (filter?.endDate) { - filtered = filtered.filter((e) => e.createdAt <= filter.endDate!); - } + // 페이지네이션 + if (filter?.page) queryParams.page = String(filter.page); + if (filter?.size) queryParams.per_page = String(filter.size); // 정렬 if (filter?.sortBy) { - switch (filter.sortBy) { - case 'latest': - filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - break; - case 'oldest': - filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - break; - case 'amountDesc': - filtered.sort((a, b) => b.estimateAmount - a.estimateAmount); - break; - case 'amountAsc': - filtered.sort((a, b) => a.estimateAmount - b.estimateAmount); - break; - case 'bidDateDesc': - filtered.sort((a, b) => { - if (!a.bidDate) return 1; - if (!b.bidDate) return -1; - return new Date(b.bidDate).getTime() - new Date(a.bidDate).getTime(); - }); - break; + const sortMap: Record = { + latest: { field: 'created_at', dir: 'desc' }, + oldest: { field: 'created_at', dir: 'asc' }, + amountDesc: { field: 'estimate_amount', dir: 'desc' }, + amountAsc: { field: 'estimate_amount', dir: 'asc' }, + bidDateDesc: { field: 'bid_date', dir: 'desc' }, + partnerNameAsc: { field: 'partner_name', dir: 'asc' }, + partnerNameDesc: { field: 'partner_name', dir: 'desc' }, + projectNameAsc: { field: 'project_name', dir: 'asc' }, + projectNameDesc: { field: 'project_name', dir: 'desc' }, + }; + const sort = sortMap[filter.sortBy]; + if (sort) { + queryParams.sort_by = sort.field; + queryParams.sort_dir = sort.dir; } } - const page = filter?.page ?? 1; - const size = filter?.size ?? 20; - const start = (page - 1) * size; - const paginatedItems = filtered.slice(start, start + size); + const response = await apiClient.get<{ + data: ApiEstimate[]; + current_page: number; + per_page: number; + total: number; + last_page: number; + }>('/estimates', { params: queryParams }); + + const items = (response.data || []).map(transformEstimate); return { success: true, data: { - items: paginatedItems, - total: filtered.length, - page, - size, - totalPages: Math.ceil(filtered.length / size), + items, + total: response.total || 0, + page: response.current_page || 1, + size: response.per_page || 20, + totalPages: response.last_page || 1, }, }; } catch (error) { - console.error('getEstimateList error:', error); - return { success: false, error: '견적 목록 조회에 실패했습니다.' }; + console.error('견적 목록 조회 오류:', error); + return { success: false, error: '견적 목록을 불러오는데 실패했습니다.' }; } } -// 견적 상세 조회 -export async function getEstimate( - id: string -): Promise<{ success: boolean; data?: Estimate; error?: string }> { +/** + * 견적 단건 조회 + * GET /api/v1/estimates/{id} + */ +export async function getEstimate(id: string): Promise<{ + success: boolean; + data?: Estimate; + error?: string; +}> { try { - const estimate = mockEstimates.find((e) => e.id === id); - - if (!estimate) { - return { success: false, error: '견적을 찾을 수 없습니다.' }; - } - - return { success: true, data: estimate }; + const response = await apiClient.get(`/estimates/${id}`); + return { success: true, data: transformEstimate(response) }; } catch (error) { - console.error('getEstimate error:', error); - return { success: false, error: '견적 조회에 실패했습니다.' }; + console.error('견적 조회 오류:', error); + return { success: false, error: '견적 정보를 찾을 수 없습니다.' }; } } -// 견적 통계 조회 -export async function getEstimateStats(): Promise<{ success: boolean; data?: EstimateStats; error?: string }> { +/** + * 견적 상세 조회 (첨부 정보 포함) + * GET /api/v1/estimates/{id}/detail + */ +export async function getEstimateDetail(id: string): Promise<{ + success: boolean; + data?: EstimateDetail; + error?: string; +}> { try { - const total = mockEstimates.length; - const pending = mockEstimates.filter((e) => e.status === 'pending').length; - const completed = mockEstimates.filter((e) => e.status === 'completed').length; + const response = await apiClient.get(`/estimates/${id}`); + return { success: true, data: transformEstimateDetail(response) }; + } catch (error) { + console.error('견적 상세 조회 오류:', error); + return { success: false, error: '견적 상세 정보를 불러오는데 실패했습니다.' }; + } +} + +/** + * 견적 통계 조회 + * GET /api/v1/estimates/stats + */ +export async function getEstimateStats(): Promise<{ + success: boolean; + data?: EstimateStats; + error?: string; +}> { + try { + const response = await apiClient.get('/estimates/stats'); return { success: true, data: { - total, - pending, - completed, + total: response.total_count || 0, + pending: response.pending_count || 0, + completed: response.completed_count || 0, }, }; } catch (error) { - console.error('getEstimateStats error:', error); - return { success: false, error: '통계 조회에 실패했습니다.' }; + console.error('견적 통계 조회 오류:', error); + return { success: false, error: '통계를 불러오는데 실패했습니다.' }; } } -// 견적 삭제 -export async function deleteEstimate(id: string): Promise<{ success: boolean; error?: string }> { +/** + * 견적 등록 + * POST /api/v1/estimates + */ +export async function createEstimate(data: EstimateDetailFormData): Promise<{ + success: boolean; + data?: Estimate; + error?: string; +}> { try { - console.log('Delete estimate:', id); + const apiData = transformToApiRequest(data); + const response = await apiClient.post('/estimates', apiData); + return { success: true, data: transformEstimate(response) }; + } catch (error) { + console.error('견적 등록 오류:', error); + return { success: false, error: '견적 등록에 실패했습니다.' }; + } +} + +/** + * 견적 수정 + * PUT /api/v1/estimates/{id} + */ +export async function updateEstimate( + id: string, + data: Partial +): Promise<{ + success: boolean; + data?: Estimate; + error?: string; +}> { + try { + const apiData = transformToApiRequest(data); + const response = await apiClient.put(`/estimates/${id}`, apiData); + return { success: true, data: transformEstimate(response) }; + } catch (error) { + console.error('견적 수정 오류:', error); + return { success: false, error: '견적 수정에 실패했습니다.' }; + } +} + +/** + * 견적 삭제 + * DELETE /api/v1/estimates/{id} + */ +export async function deleteEstimate(id: string): Promise<{ + success: boolean; + error?: string; +}> { + try { + await apiClient.delete(`/estimates/${id}`); return { success: true }; } catch (error) { - console.error('deleteEstimate error:', error); + console.error('견적 삭제 오류:', error); return { success: false, error: '견적 삭제에 실패했습니다.' }; } } -// 견적 일괄 삭제 -export async function deleteEstimates(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> { +/** + * 견적 일괄 삭제 + * DELETE /api/v1/estimates/bulk + */ +export async function deleteEstimates(ids: string[]): Promise<{ + success: boolean; + deletedCount?: number; + error?: string; +}> { try { - console.log('Delete estimates:', ids); + await apiClient.delete('/estimates/bulk', { + data: { ids: ids.map((id) => Number(id)) }, + }); return { success: true, deletedCount: ids.length }; } catch (error) { - console.error('deleteEstimates error:', error); + console.error('견적 일괄 삭제 오류:', error); return { success: false, error: '일괄 삭제에 실패했습니다.' }; } } \ No newline at end of file diff --git a/src/components/business/construction/pricing-management/actions.ts b/src/components/business/construction/pricing-management/actions.ts index 9b07f1b5..6ab333ae 100644 --- a/src/components/business/construction/pricing-management/actions.ts +++ b/src/components/business/construction/pricing-management/actions.ts @@ -1,388 +1,378 @@ 'use server'; -import type { Pricing, PricingStats } from './types'; +import type { + Pricing, + PricingStats, + PricingListResponse, + PricingFilter, + PricingFormData, +} from './types'; +import { apiClient } from '@/lib/api'; -// ===== 목데이터 ===== -const mockPricingList: Pricing[] = [ - { - id: '1', - pricingNumber: 'PRC-2026-001', - itemType: '박스', - category: '슬라이드 OPEN 사이즈', - itemName: '슬라이드 도어 세트', - spec: '1200x2400', - orderItems: [ - { id: 'oi1', name: '무게', value: '400KG' }, - { id: 'oi2', name: '두께', value: '50mm' }, - ], - unit: 'SET', - division: '일반', - vendor: '(주)슬라이드텍', - purchasePrice: 850000, - marginRate: 15, - sellingPrice: 977500, - status: 'in_use', - createdAt: '2026-01-02', - }, - { - id: '2', - pricingNumber: 'PRC-2026-002', - itemType: '부속', - category: '모터', - itemName: '서보모터 750W', - spec: 'AC220V', - orderItems: [ - { id: 'oi3', name: '무게', value: '12KG' }, - ], - unit: 'EA', - division: '일반', - vendor: '삼성전기', - purchasePrice: 320000, - marginRate: 20, - sellingPrice: 384000, - status: 'in_use', - createdAt: '2026-01-02', - }, - { - id: '3', - pricingNumber: 'PRC-2026-003', - itemType: '소모품', - category: '공정자재', - itemName: '용접봉 E7016', - spec: '4.0mm x 350mm', - orderItems: [ - { id: 'oi4', name: '무게', value: '5KG' }, - ], - unit: 'BOX', - division: '일반', - vendor: '현대용접산업', - purchasePrice: 45000, - marginRate: 25, - sellingPrice: 56250, - status: 'in_use', - createdAt: '2026-01-03', - }, - { - id: '4', - pricingNumber: 'PRC-2026-004', - itemType: '공과', - category: '철물', - itemName: '앵커볼트 세트', - spec: 'M12 x 100', - orderItems: [ - { id: 'oi5', name: '무게', value: '500G' }, - ], - unit: 'SET', - division: '특수', - vendor: '철강볼트', - purchasePrice: 12000, - marginRate: 30, - sellingPrice: 15600, - status: 'in_use', - createdAt: '2026-01-03', - }, - { - id: '5', - pricingNumber: 'PRC-2026-005', - itemType: '박스', - category: '슬라이드 OPEN 사이즈', - itemName: '자동문 프레임', - spec: '900x2100', - orderItems: [ - { id: 'oi6', name: '무게', value: '280KG' }, - { id: 'oi7', name: '두께', value: '40mm' }, - ], - unit: 'SET', - division: '일반', - vendor: '(주)슬라이드텍', - purchasePrice: 650000, - marginRate: 18, - sellingPrice: 767000, - status: 'not_registered', - createdAt: '2026-01-04', - }, - { - id: '6', - pricingNumber: 'PRC-2026-006', - itemType: '부속', - category: '모터', - itemName: '기어드모터 1.5KW', - spec: 'AC380V', - orderItems: [ - { id: 'oi8', name: '무게', value: '25KG' }, - ], - unit: 'EA', - division: '특수', - vendor: '삼성전기', - purchasePrice: 580000, - marginRate: 22, - sellingPrice: 707600, - status: 'in_use', - createdAt: '2026-01-04', - }, - { - id: '7', - pricingNumber: 'PRC-2026-007', - itemType: '소모품', - category: '공정자재', - itemName: '절삭유 WS-300', - spec: '20L', - orderItems: [], - unit: 'CAN', - division: '일반', - vendor: '한국윤활유', - purchasePrice: 85000, - marginRate: 15, - sellingPrice: 97750, - status: 'not_registered', - createdAt: '2026-01-05', - }, - { - id: '8', - pricingNumber: 'PRC-2026-008', - itemType: '공과', - category: '철물', - itemName: '스테인레스 볼트', - spec: 'M10 x 50', - orderItems: [ - { id: 'oi9', name: '무게', value: '200G' }, - ], - unit: 'BOX', - division: '일반', - vendor: '철강볼트', - purchasePrice: 35000, - marginRate: 28, - sellingPrice: 44800, - status: 'in_use', - createdAt: '2026-01-05', - }, -]; +/** + * 주일 기업 - 단가관리 Server Actions + * 표준화된 apiClient 사용 버전 + */ -// ===== 단가 목록 조회 ===== -export async function getPricingList(params?: { - startDate?: string; - endDate?: string; - itemType?: string; - category?: string; - spec?: string; - division?: string; - status?: string; - sort?: string; - search?: string; -}): Promise<{ +// ======================================== +// API 응답 타입 +// ======================================== + +interface ApiPricing { + id: number; + pricing_number: string; + item_type: string | null; + category: string | null; + item_name: string; + spec: string | null; + order_items: ApiOrderItem[] | null; + unit: string | null; + division: string | null; + vendor: string | null; + vendor_id: number | null; + purchase_price: number; + margin_rate: number; + selling_price: number; + status: 'in_use' | 'stopped' | 'not_registered'; + created_at: string; + updated_at: string; +} + +interface ApiOrderItem { + id: number; + name: string; + value: string; +} + +interface ApiPricingStats { + total: number; + in_use: number; + stopped: number; + not_registered: number; +} + +interface ApiVendor { + id: number; + name: string; + business_no: string | null; +} + +// ======================================== +// 타입 변환 함수 +// ======================================== + +/** + * API 응답 → Pricing 타입 변환 + */ +function transformPricing(apiData: ApiPricing): Pricing { + return { + id: String(apiData.id), + pricingNumber: apiData.pricing_number || '', + itemType: apiData.item_type || '', + category: apiData.category || '', + itemName: apiData.item_name || '', + spec: apiData.spec || '', + orderItems: (apiData.order_items || []).map((item) => ({ + id: String(item.id), + name: item.name || '', + value: item.value || '', + })), + unit: apiData.unit || '', + division: apiData.division || '', + vendor: apiData.vendor || '', + purchasePrice: apiData.purchase_price || 0, + marginRate: apiData.margin_rate || 0, + sellingPrice: apiData.selling_price || 0, + status: apiData.status || 'not_registered', + createdAt: apiData.created_at || '', + updatedAt: apiData.updated_at || '', + }; +} + +/** + * PricingFormData → API 요청 데이터 변환 + */ +function transformToApiRequest(data: Partial): Record { + const apiData: Record = {}; + + if (data.itemType !== undefined) apiData.item_type = data.itemType || null; + if (data.category !== undefined) apiData.category = data.category || null; + if (data.itemName !== undefined) apiData.item_name = data.itemName; + if (data.spec !== undefined) apiData.spec = data.spec || null; + if (data.unit !== undefined) apiData.unit = data.unit || null; + if (data.division !== undefined) apiData.division = data.division || null; + if (data.vendor !== undefined) apiData.vendor = data.vendor || null; + if (data.purchasePrice !== undefined) apiData.purchase_price = data.purchasePrice; + if (data.marginRate !== undefined) apiData.margin_rate = data.marginRate; + if (data.sellingPrice !== undefined) apiData.selling_price = data.sellingPrice; + if (data.status !== undefined) apiData.status = data.status; + + // 주문 항목 변환 + if (data.orderItems !== undefined) { + apiData.order_items = data.orderItems.map((item) => ({ + name: item.name, + value: item.value, + })); + } + + return apiData; +} + +// ======================================== +// API 함수 +// ======================================== + +/** + * 단가 목록 조회 + * GET /api/v1/pricing + */ +export async function getPricingList(filter?: PricingFilter): Promise<{ success: boolean; - data?: { items: Pricing[]; total: number }; + data?: PricingListResponse; error?: string; }> { try { - let filtered = [...mockPricingList]; + const queryParams: Record = {}; - // 품목유형 필터 - if (params?.itemType && params.itemType !== 'all') { - const typeMap: Record = { - box: '박스', - parts: '부속', - consumables: '소모품', - utility: '공과', - }; - filtered = filtered.filter(p => p.itemType === typeMap[params.itemType!]); - } + // 검색 + if (filter?.search) queryParams.search = filter.search; - // 카테고리 필터 - if (params?.category && params.category !== 'all') { - const categoryMap: Record = { - slide_open: '슬라이드 OPEN 사이즈', - motor: '모터', - process_material: '공정자재', - hardware: '철물', - }; - filtered = filtered.filter(p => p.category === categoryMap[params.category!]); - } + // 필터 + if (filter?.status && filter.status !== 'all') queryParams.status = filter.status; + if (filter?.itemType && filter.itemType !== 'all') queryParams.item_type = filter.itemType; + if (filter?.category && filter.category !== 'all') queryParams.category = filter.category; + if (filter?.division && filter.division !== 'all') queryParams.division = filter.division; - // 구분 필터 - if (params?.division && params.division !== 'all') { - const divisionMap: Record = { - general: '일반', - special: '특수', - }; - filtered = filtered.filter(p => p.division === divisionMap[params.division!]); - } - - // 검색 필터 - if (params?.search) { - const search = params.search.toLowerCase(); - filtered = filtered.filter(p => - p.pricingNumber.toLowerCase().includes(search) || - p.itemName.toLowerCase().includes(search) || - p.category.toLowerCase().includes(search) || - p.vendor.toLowerCase().includes(search) - ); - } + // 페이지네이션 + if (filter?.page) queryParams.page = String(filter.page); + if (filter?.size) queryParams.per_page = String(filter.size); // 정렬 - if (params?.sort === 'oldest') { - filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - } else if (params?.sort === 'price_high') { - filtered.sort((a, b) => b.sellingPrice - a.sellingPrice); - } else if (params?.sort === 'price_low') { - filtered.sort((a, b) => a.sellingPrice - b.sellingPrice); - } else { - filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + if (filter?.sortBy) { + const sortMap: Record = { + latest: { field: 'created_at', dir: 'desc' }, + oldest: { field: 'created_at', dir: 'asc' }, + itemNameAsc: { field: 'item_name', dir: 'asc' }, + itemNameDesc: { field: 'item_name', dir: 'desc' }, + priceAsc: { field: 'selling_price', dir: 'asc' }, + priceDesc: { field: 'selling_price', dir: 'desc' }, + price_high: { field: 'selling_price', dir: 'desc' }, + price_low: { field: 'selling_price', dir: 'asc' }, + }; + const sort = sortMap[filter.sortBy]; + if (sort) { + queryParams.sort_by = sort.field; + queryParams.sort_dir = sort.dir; + } } + const response = await apiClient.get<{ + data: ApiPricing[]; + current_page: number; + per_page: number; + total: number; + last_page: number; + }>('/pricing', { params: queryParams }); + + const items = (response.data || []).map(transformPricing); + return { success: true, data: { - items: filtered, - total: filtered.length, + items, + total: response.total || 0, + page: response.current_page || 1, + size: response.per_page || 20, + totalPages: response.last_page || 1, }, }; } catch (error) { - console.error('[getPricingList] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('단가 목록 조회 오류:', error); + return { success: false, error: '단가 목록을 불러오는데 실패했습니다.' }; } } -// ===== 통계 조회 ===== +/** + * 단가 통계 조회 + * GET /api/v1/pricing/stats + */ export async function getPricingStats(): Promise<{ success: boolean; data?: PricingStats; error?: string; }> { try { - const stats: PricingStats = { - total: mockPricingList.length, - inUse: mockPricingList.filter(p => p.status === 'in_use').length, - notRegistered: mockPricingList.filter(p => p.status === 'not_registered').length, + const response = await apiClient.get('/pricing/stats'); + + return { + success: true, + data: { + total: response.total || 0, + inUse: response.in_use || 0, + notRegistered: response.not_registered || 0, + }, }; - - return { success: true, data: stats }; } catch (error) { - console.error('[getPricingStats] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('단가 통계 조회 오류:', error); + return { success: false, error: '통계를 불러오는데 실패했습니다.' }; } } -// ===== 단일 삭제 ===== -export async function deletePricing(id: string): Promise<{ - success: boolean; - error?: string; -}> { - try { - // 목데이터에서는 실제 삭제하지 않음 - const index = mockPricingList.findIndex(p => p.id === id); - if (index === -1) { - return { success: false, error: '단가를 찾을 수 없습니다.' }; - } - - return { success: true }; - } catch (error) { - console.error('[deletePricing] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; - } -} - -// ===== 일괄 삭제 ===== -export async function deletePricings(ids: string[]): Promise<{ - success: boolean; - deletedCount?: number; - error?: string; -}> { - try { - return { success: true, deletedCount: ids.length }; - } catch (error) { - console.error('[deletePricings] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; - } -} - -// ===== 단가 상세 조회 ===== +/** + * 단가 상세 조회 + * GET /api/v1/pricing/{id} + */ export async function getPricingDetail(id: string): Promise<{ success: boolean; data?: Pricing; error?: string; }> { try { - const pricing = mockPricingList.find(p => p.id === id); - if (!pricing) { - return { success: false, error: '단가를 찾을 수 없습니다.' }; - } - return { success: true, data: pricing }; + const response = await apiClient.get(`/pricing/${id}`); + return { success: true, data: transformPricing(response) }; } catch (error) { - console.error('[getPricingDetail] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('단가 상세 조회 오류:', error); + return { success: false, error: '단가 정보를 찾을 수 없습니다.' }; } } -// ===== 단가 생성 ===== -export async function createPricing(data: Omit): Promise<{ +/** + * 단가 등록 + * POST /api/v1/pricing + */ +export async function createPricing(data: PricingFormData): Promise<{ success: boolean; data?: Pricing; error?: string; }> { try { - const newId = String(mockPricingList.length + 1); - const newPricingNumber = `PRC-2026-${String(mockPricingList.length + 1).padStart(3, '0')}`; - - const newPricing: Pricing = { - ...data, - id: newId, - pricingNumber: newPricingNumber, - createdAt: new Date().toISOString().split('T')[0], - }; - - // 목데이터에는 추가하지 않음 (실제 API 연동 시 DB에 저장) - return { success: true, data: newPricing }; + const apiData = transformToApiRequest(data); + const response = await apiClient.post('/pricing', apiData); + return { success: true, data: transformPricing(response) }; } catch (error) { - console.error('[createPricing] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('단가 등록 오류:', error); + return { success: false, error: '단가 등록에 실패했습니다.' }; } } -// ===== 단가 수정 ===== -export async function updatePricing(id: string, data: Partial): Promise<{ +/** + * 단가 수정 + * PUT /api/v1/pricing/{id} + */ +export async function updatePricing( + id: string, + data: Partial +): Promise<{ success: boolean; data?: Pricing; error?: string; }> { try { - const index = mockPricingList.findIndex(p => p.id === id); - if (index === -1) { - return { success: false, error: '단가를 찾을 수 없습니다.' }; - } - - const updatedPricing: Pricing = { - ...mockPricingList[index], - ...data, - updatedAt: new Date().toISOString().split('T')[0], - }; - - // 목데이터에는 수정하지 않음 (실제 API 연동 시 DB에 업데이트) - return { success: true, data: updatedPricing }; + const apiData = transformToApiRequest(data); + const response = await apiClient.put(`/pricing/${id}`, apiData); + return { success: true, data: transformPricing(response) }; } catch (error) { - console.error('[updatePricing] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('단가 수정 오류:', error); + return { success: false, error: '단가 수정에 실패했습니다.' }; } } -// ===== 거래처 목록 조회 (발주처) ===== +/** + * 단가 삭제 + * DELETE /api/v1/pricing/{id} + */ +export async function deletePricing(id: string): Promise<{ + success: boolean; + error?: string; +}> { + try { + await apiClient.delete(`/pricing/${id}`); + return { success: true }; + } catch (error) { + console.error('단가 삭제 오류:', error); + return { success: false, error: '단가 삭제에 실패했습니다.' }; + } +} + +/** + * 단가 일괄 삭제 + * DELETE /api/v1/pricing/bulk + */ +export async function deletePricings(ids: string[]): Promise<{ + success: boolean; + deletedCount?: number; + error?: string; +}> { + try { + await apiClient.delete('/pricing/bulk', { + data: { ids: ids.map((id) => Number(id)) }, + }); + return { success: true, deletedCount: ids.length }; + } catch (error) { + console.error('단가 일괄 삭제 오류:', error); + return { success: false, error: '일괄 삭제에 실패했습니다.' }; + } +} + +/** + * 거래처(벤더) 목록 조회 + * GET /api/v1/clients (거래처 API 재사용) + */ export async function getVendorList(): Promise<{ success: boolean; data?: { id: string; name: string }[]; error?: string; }> { try { - // 목데이터에서 거래처 추출 - const vendors = [ - { id: '1', name: '(주)슬라이드텍' }, - { id: '2', name: '삼성전기' }, - { id: '3', name: '현대용접산업' }, - { id: '4', name: '철강볼트' }, - { id: '5', name: '한국윤활유' }, - ]; + const response = await apiClient.get<{ + data: ApiVendor[]; + }>('/clients', { params: { per_page: '100' } }); + + const vendors = (response.data || []).map((v) => ({ + id: String(v.id), + name: v.name || '', + })); + return { success: true, data: vendors }; } catch (error) { - console.error('[getVendorList] Error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; + console.error('거래처 목록 조회 오류:', error); + return { success: false, error: '거래처 목록을 불러오는데 실패했습니다.' }; + } +} + +/** + * 단가 확정 + * POST /api/v1/pricing/{id}/finalize + */ +export async function finalizePricing(id: string): Promise<{ + success: boolean; + data?: Pricing; + error?: string; +}> { + try { + const response = await apiClient.post(`/pricing/${id}/finalize`); + return { success: true, data: transformPricing(response) }; + } catch (error) { + console.error('단가 확정 오류:', error); + return { success: false, error: '단가 확정에 실패했습니다.' }; + } +} + +/** + * 단가 변경이력 조회 + * GET /api/v1/pricing/{id}/revisions + */ +export async function getPricingRevisions(id: string): Promise<{ + success: boolean; + data?: Pricing[]; + error?: string; +}> { + try { + const response = await apiClient.get<{ data: ApiPricing[] }>(`/pricing/${id}/revisions`); + const revisions = (response.data || []).map(transformPricing); + return { success: true, data: revisions }; + } catch (error) { + console.error('단가 변경이력 조회 오류:', error); + return { success: false, error: '변경이력을 불러오는데 실패했습니다.' }; } } \ No newline at end of file diff --git a/src/components/business/construction/pricing-management/types.ts b/src/components/business/construction/pricing-management/types.ts index 6b972189..81c0f601 100644 --- a/src/components/business/construction/pricing-management/types.ts +++ b/src/components/business/construction/pricing-management/types.ts @@ -39,6 +39,44 @@ export interface PricingStats { notRegistered: number; // 미등록 단가 } +// 목록 응답 +export interface PricingListResponse { + items: Pricing[]; + total: number; + page: number; + size: number; + totalPages: number; +} + +// 필터 파라미터 +export interface PricingFilter { + search?: string; + status?: string; + itemType?: string; + category?: string; + division?: string; + spec?: string; + page?: number; + size?: number; + sortBy?: string; +} + +// 폼 데이터 +export interface PricingFormData { + itemType: string; + category: string; + itemName: string; + spec: string; + orderItems: OrderItem[]; + unit: string; + division: string; + vendor: string; + purchasePrice: number; + marginRate: number; + sellingPrice: number; + status: PricingStatus; +} + // ===== 필터 옵션 ===== // 품목유형 옵션