feat(construction): Phase L 건설관리 3개 모듈 Mock → API 연동

- pricing-management: Mock → apiClient 표준화, types.ts 타입 추가
- estimates: Mock → apiClient 표준화 (복잡한 중첩 타입 처리)
- category-management: Mock → apiClient 표준화 (에러 타입 처리)
This commit is contained in:
2026-01-09 19:57:30 +09:00
parent 5db6e59bbc
commit d43433295d
5 changed files with 1010 additions and 617 deletions

View File

@@ -1,5 +1,46 @@
# SAM React 작업 현황 # 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 표준화 ## 2026-01-09 (목) - Phase 1.3-1.5 건설관리 apiClient 표준화
### 작업 목표 ### 작업 목표
@@ -58,7 +99,7 @@
✅ Next.js 빌드 성공 (349 페이지) ✅ Next.js 빌드 성공 (349 페이지)
### Git 커밋 ### Git 커밋
- 대기 중 - React: `5db6e59` refactor(construction): 건설관리 3개 모듈 apiClient 표준화
--- ---
@@ -776,16 +817,29 @@ useEffect(() => {
#### Phase K (✅ 완료) - 보고서 #### Phase K (✅ 완료) - 보고서
- [x] K-1 종합분석 (Reports) API 연동 ✅ - [x] K-1 종합분석 (Reports) API 연동 ✅
#### Phase L (🔄 진행중 ~30%) - 건설관리 #### Phase L (🔄 진행중 ~80%) - 건설관리
- [ ] bidding, category-management, contract, estimates (Mock 사용 중) **✅ apiClient 표준화 완료:**
- [ ] handover-report, pricing-management, site-briefings, structure-review (Mock 사용 중) - [x] handover-report (b7b8b90)
- [x] labor-management, order-management, partners, site-management (Custom fetch → 표준화 필요) - [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 > **점검일**: 2026-01-09
### 다음 작업 ### 다음 작업
- Phase L 건설관리 모듈 마이그레이션 완료 - Phase L 건설관리 모듈 마이그레이션 완료 (Backend API 개발 필요: bidding, site-briefings, structure-review, labor-management)
- ~~TODO-1: 결재선/참조 Select 변경 불가 문제~~ ✅ 2026-01-09 수정 완료 - ~~TODO-1: 결재선/참조 Select 변경 불가 문제~~ ✅ 2026-01-09 수정 완료
--- ---

View File

@@ -1,35 +1,79 @@
'use server'; 'use server';
import type { Category } from './types'; import type { Category } from './types';
import { apiClient } from '@/lib/api';
// ===== 목데이터 (추후 API 연동 시 교체) ===== /**
let mockCategories: Category[] = [ * 주일 기업 - 카테고리 관리 Server Actions
{ id: '1', name: '슬라이드 OPEN 사이즈', order: 1, isDefault: true }, * 표준화된 apiClient 사용 버전
{ id: '2', name: '모터', order: 2, isDefault: true }, */
{ id: '3', name: '공정자재', order: 3, isDefault: true },
{ id: '4', name: '철물', order: 4, isDefault: true },
];
// 다음 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<{ export async function getCategories(): Promise<{
success: boolean; success: boolean;
data?: Category[]; data?: Category[];
error?: string; error?: string;
}> { }> {
try { try {
// 목데이터 반환 (순서대로 정렬) const response = await apiClient.get<{
const sortedCategories = [...mockCategories].sort((a, b) => a.order - b.order); data: ApiCategory[];
return { success: true, data: sortedCategories }; }>('/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) { } catch (error) {
console.error('[getCategories] Error:', error); console.error('카테고리 목록 조회 오류:', error);
return { success: false, error: '서버 오류가 발생했습니다.' }; return { success: false, error: '카테고리 목록을 불러오는데 실패했습니다.' };
} }
} }
// ===== 카테고리 생성 ===== /**
* 카테고리 생성
* POST /api/v1/categories
*/
export async function createCategory(data: { export async function createCategory(data: {
name: string; name: string;
}): Promise<{ }): Promise<{
@@ -38,22 +82,20 @@ export async function createCategory(data: {
error?: string; error?: string;
}> { }> {
try { try {
const newCategory: Category = { const response = await apiClient.post<ApiCategory>('/categories', {
id: String(nextId++),
name: data.name, name: data.name,
order: mockCategories.length + 1, });
isDefault: false, return { success: true, data: transformCategory(response) };
};
mockCategories.push(newCategory);
return { success: true, data: newCategory };
} catch (error) { } catch (error) {
console.error('[createCategory] Error:', error); console.error('카테고리 생성 오류:', error);
return { success: false, error: '서버 오류가 발생했습니다.' }; return { success: false, error: '카테고리 생성에 실패했습니다.' };
} }
} }
// ===== 카테고리 수정 ===== /**
* 카테고리 수정
* PUT /api/v1/categories/{id}
*/
export async function updateCategory( export async function updateCategory(
id: string, id: string,
data: { name?: string } data: { name?: string }
@@ -63,65 +105,64 @@ export async function updateCategory(
error?: string; error?: string;
}> { }> {
try { try {
const index = mockCategories.findIndex(c => c.id === id); const response = await apiClient.put<ApiCategory>(`/categories/${id}`, {
if (index === -1) { name: data.name,
return { success: false, error: '카테고리를 찾을 수 없습니다.' }; });
} return { success: true, data: transformCategory(response) };
mockCategories[index] = {
...mockCategories[index],
...data,
};
return { success: true, data: mockCategories[index] };
} catch (error) { } catch (error) {
console.error('[updateCategory] Error:', error); console.error('카테고리 수정 오류:', error);
return { success: false, error: '서버 오류가 발생했습니다.' }; return { success: false, error: '카테고리 수정에 실패했습니다.' };
} }
} }
// ===== 카테고리 삭제 ===== /**
* 카테고리 삭제
* DELETE /api/v1/categories/{id}
*/
export async function deleteCategory(id: string): Promise<{ export async function deleteCategory(id: string): Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
errorType?: 'IN_USE' | 'DEFAULT' | 'GENERAL'; errorType?: 'IN_USE' | 'DEFAULT' | 'GENERAL';
}> { }> {
try { 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) { // API 에러 응답에서 errorType 추출
return { success: false, error: '카테고리를 찾을 수 없습니다.', errorType: 'GENERAL' }; 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 (errorType === 'IN_USE') {
if (category.isDefault) {
return { return {
success: false, success: false,
error: '기본 카테고리는 삭제가 불가합니다.', error: errorMessage || '해당 카테고리를 사용하고 있는 품목이 있습니다.',
errorType: 'DEFAULT' errorType: 'IN_USE',
}; };
} }
// TODO: 품목 사용 여부 체크 로직 (추후 API 연동 시) if (errorType === 'DEFAULT') {
// 현재는 목데이터이므로 사용 중인 품목이 없다고 가정 return {
// const itemsUsingCategory = await checkItemsUsingCategory(id); success: false,
// if (itemsUsingCategory.length > 0) { error: errorMessage || '기본 카테고리는 삭제가 불가합니다.',
// return { errorType: 'DEFAULT',
// success: false, };
// error: `"${category.name}"을(를) 사용하고 있는 품목이 있습니다. 모두 변경 후 삭제가 가능합니다.`, }
// errorType: 'IN_USE'
// };
// }
mockCategories = mockCategories.filter(c => c.id !== id); return {
return { success: true }; success: false,
} catch (error) { error: '카테고리 삭제에 실패했습니다.',
console.error('[deleteCategory] Error:', error); errorType: 'GENERAL',
return { success: false, error: '서버 오류가 발생했습니다.', errorType: 'GENERAL' }; };
} }
} }
// ===== 카테고리 순서 변경 ===== /**
* 카테고리 순서 변경
* PUT /api/v1/categories/reorder
*/
export async function reorderCategories( export async function reorderCategories(
items: { id: string; sort_order: number }[] items: { id: string; sort_order: number }[]
): Promise<{ ): Promise<{
@@ -129,17 +170,15 @@ export async function reorderCategories(
error?: string; error?: string;
}> { }> {
try { try {
// 순서 업데이트 await apiClient.put('/categories/reorder', {
items.forEach(item => { items: items.map((item) => ({
const category = mockCategories.find(c => c.id === item.id); id: Number(item.id),
if (category) { sort_order: item.sort_order,
category.order = item.sort_order; })),
}
}); });
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('[reorderCategories] Error:', error); console.error('카테고리 순서 변경 오류:', error);
return { success: false, error: '서버 오류가 발생했습니다.' }; return { success: false, error: '순서 변경에 실패했습니다.' };
} }
} }

View File

@@ -1,279 +1,551 @@
'use server'; '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 * 주일 기업 - 견적관리 Server Actions
* TODO: 실제 API 연동 시 구현 * 표준화된 apiClient 사용 버전
*/ */
// 목업 데이터 // ========================================
const mockEstimates: Estimate[] = [ // API 응답 타입
{ // ========================================
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: '김철수',
},
];
// 견적 목록 조회 interface ApiEstimate {
export async function getEstimateList( id: number;
filter?: EstimateFilter estimate_code: string;
): Promise<{ success: boolean; data?: EstimateListResponse; error?: 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<EstimateDetailFormData>): Record<string, unknown> {
const apiData: Record<string, unknown> = {};
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 { try {
let filtered = [...mockEstimates]; const queryParams: Record<string, string> = {};
// 검색 필터 // 검색
if (filter?.search) { if (filter?.search) queryParams.search = 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?.status && filter.status !== 'all') { if (filter?.status && filter.status !== 'all') queryParams.status = filter.status;
filtered = filtered.filter((e) => e.status === filter.status); if (filter?.partnerId) queryParams.partner_id = filter.partnerId;
} if (filter?.estimatorId) queryParams.estimator_id = filter.estimatorId;
// 거래처 필터 // 날짜 범위
if (filter?.partnerId) { if (filter?.startDate) queryParams.start_date = filter.startDate;
filtered = filtered.filter((e) => e.partnerId === filter.partnerId); if (filter?.endDate) queryParams.end_date = filter.endDate;
}
// 견적자 필터 // 페이지네이션
if (filter?.estimatorId) { if (filter?.page) queryParams.page = String(filter.page);
filtered = filtered.filter((e) => e.estimatorId === filter.estimatorId); if (filter?.size) queryParams.per_page = String(filter.size);
}
// 날짜 필터
if (filter?.startDate) {
filtered = filtered.filter((e) => e.createdAt >= filter.startDate!);
}
if (filter?.endDate) {
filtered = filtered.filter((e) => e.createdAt <= filter.endDate!);
}
// 정렬 // 정렬
if (filter?.sortBy) { if (filter?.sortBy) {
switch (filter.sortBy) { const sortMap: Record<string, { field: string; dir: string }> = {
case 'latest': latest: { field: 'created_at', dir: 'desc' },
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); oldest: { field: 'created_at', dir: 'asc' },
break; amountDesc: { field: 'estimate_amount', dir: 'desc' },
case 'oldest': amountAsc: { field: 'estimate_amount', dir: 'asc' },
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); bidDateDesc: { field: 'bid_date', dir: 'desc' },
break; partnerNameAsc: { field: 'partner_name', dir: 'asc' },
case 'amountDesc': partnerNameDesc: { field: 'partner_name', dir: 'desc' },
filtered.sort((a, b) => b.estimateAmount - a.estimateAmount); projectNameAsc: { field: 'project_name', dir: 'asc' },
break; projectNameDesc: { field: 'project_name', dir: 'desc' },
case 'amountAsc': };
filtered.sort((a, b) => a.estimateAmount - b.estimateAmount); const sort = sortMap[filter.sortBy];
break; if (sort) {
case 'bidDateDesc': queryParams.sort_by = sort.field;
filtered.sort((a, b) => { queryParams.sort_dir = sort.dir;
if (!a.bidDate) return 1;
if (!b.bidDate) return -1;
return new Date(b.bidDate).getTime() - new Date(a.bidDate).getTime();
});
break;
} }
} }
const page = filter?.page ?? 1; const response = await apiClient.get<{
const size = filter?.size ?? 20; data: ApiEstimate[];
const start = (page - 1) * size; current_page: number;
const paginatedItems = filtered.slice(start, start + size); per_page: number;
total: number;
last_page: number;
}>('/estimates', { params: queryParams });
const items = (response.data || []).map(transformEstimate);
return { return {
success: true, success: true,
data: { data: {
items: paginatedItems, items,
total: filtered.length, total: response.total || 0,
page, page: response.current_page || 1,
size, size: response.per_page || 20,
totalPages: Math.ceil(filtered.length / size), totalPages: response.last_page || 1,
}, },
}; };
} catch (error) { } catch (error) {
console.error('getEstimateList error:', error); console.error('견적 목록 조회 오류:', error);
return { success: false, error: '견적 목록 조회에 실패했습니다.' }; return { success: false, error: '견적 목록을 불러오는데 실패했습니다.' };
} }
} }
// 견적 상세 조회 /**
export async function getEstimate( * 견적 단건 조회
id: string * GET /api/v1/estimates/{id}
): Promise<{ success: boolean; data?: Estimate; error?: string }> { */
export async function getEstimate(id: string): Promise<{
success: boolean;
data?: Estimate;
error?: string;
}> {
try { try {
const estimate = mockEstimates.find((e) => e.id === id); const response = await apiClient.get<ApiEstimate>(`/estimates/${id}`);
return { success: true, data: transformEstimate(response) };
if (!estimate) {
return { success: false, error: '견적을 찾을 수 없습니다.' };
}
return { success: true, data: estimate };
} catch (error) { } catch (error) {
console.error('getEstimate error:', error); console.error('견적 조회 오류:', error);
return { success: false, 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 { try {
const total = mockEstimates.length; const response = await apiClient.get<ApiEstimateDetail>(`/estimates/${id}`);
const pending = mockEstimates.filter((e) => e.status === 'pending').length; return { success: true, data: transformEstimateDetail(response) };
const completed = mockEstimates.filter((e) => e.status === 'completed').length; } 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<ApiEstimateStats>('/estimates/stats');
return { return {
success: true, success: true,
data: { data: {
total, total: response.total_count || 0,
pending, pending: response.pending_count || 0,
completed, completed: response.completed_count || 0,
}, },
}; };
} catch (error) { } catch (error) {
console.error('getEstimateStats error:', error); console.error('견적 통계 조회 오류:', error);
return { success: false, 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 { try {
console.log('Delete estimate:', id); const apiData = transformToApiRequest(data);
const response = await apiClient.post<ApiEstimate>('/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<EstimateDetailFormData>
): Promise<{
success: boolean;
data?: Estimate;
error?: string;
}> {
try {
const apiData = transformToApiRequest(data);
const response = await apiClient.put<ApiEstimate>(`/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 }; return { success: true };
} catch (error) { } catch (error) {
console.error('deleteEstimate error:', error); console.error('견적 삭제 오류:', error);
return { success: false, 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 { try {
console.log('Delete estimates:', ids); await apiClient.delete('/estimates/bulk', {
data: { ids: ids.map((id) => Number(id)) },
});
return { success: true, deletedCount: ids.length }; return { success: true, deletedCount: ids.length };
} catch (error) { } catch (error) {
console.error('deleteEstimates error:', error); console.error('견적 일괄 삭제 오류:', error);
return { success: false, error: '일괄 삭제에 실패했습니다.' }; return { success: false, error: '일괄 삭제에 실패했습니다.' };
} }
} }

View File

@@ -1,388 +1,378 @@
'use server'; '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[] = [ * 주일 기업 - 단가관리 Server Actions
{ * 표준화된 apiClient 사용 버전
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',
},
];
// ===== 단가 목록 조회 ===== // ========================================
export async function getPricingList(params?: { // API 응답 타입
startDate?: string; // ========================================
endDate?: string;
itemType?: string; interface ApiPricing {
category?: string; id: number;
spec?: string; pricing_number: string;
division?: string; item_type: string | null;
status?: string; category: string | null;
sort?: string; item_name: string;
search?: string; spec: string | null;
}): Promise<{ 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<PricingFormData>): Record<string, unknown> {
const apiData: Record<string, unknown> = {};
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; success: boolean;
data?: { items: Pricing[]; total: number }; data?: PricingListResponse;
error?: string; error?: string;
}> { }> {
try { try {
let filtered = [...mockPricingList]; const queryParams: Record<string, string> = {};
// 품목유형 필터 // 검색
if (params?.itemType && params.itemType !== 'all') { if (filter?.search) queryParams.search = filter.search;
const typeMap: Record<string, string> = {
box: '박스',
parts: '부속',
consumables: '소모품',
utility: '공과',
};
filtered = filtered.filter(p => p.itemType === typeMap[params.itemType!]);
}
// 카테고리 필터 // 필터
if (params?.category && params.category !== 'all') { if (filter?.status && filter.status !== 'all') queryParams.status = filter.status;
const categoryMap: Record<string, string> = { if (filter?.itemType && filter.itemType !== 'all') queryParams.item_type = filter.itemType;
slide_open: '슬라이드 OPEN 사이즈', if (filter?.category && filter.category !== 'all') queryParams.category = filter.category;
motor: '모터', if (filter?.division && filter.division !== 'all') queryParams.division = filter.division;
process_material: '공정자재',
hardware: '철물',
};
filtered = filtered.filter(p => p.category === categoryMap[params.category!]);
}
// 구분 필터 // 페이지네이션
if (params?.division && params.division !== 'all') { if (filter?.page) queryParams.page = String(filter.page);
const divisionMap: Record<string, string> = { if (filter?.size) queryParams.per_page = String(filter.size);
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 (params?.sort === 'oldest') { if (filter?.sortBy) {
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); const sortMap: Record<string, { field: string; dir: string }> = {
} else if (params?.sort === 'price_high') { latest: { field: 'created_at', dir: 'desc' },
filtered.sort((a, b) => b.sellingPrice - a.sellingPrice); oldest: { field: 'created_at', dir: 'asc' },
} else if (params?.sort === 'price_low') { itemNameAsc: { field: 'item_name', dir: 'asc' },
filtered.sort((a, b) => a.sellingPrice - b.sellingPrice); itemNameDesc: { field: 'item_name', dir: 'desc' },
} else { priceAsc: { field: 'selling_price', dir: 'asc' },
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); 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 { return {
success: true, success: true,
data: { data: {
items: filtered, items,
total: filtered.length, total: response.total || 0,
page: response.current_page || 1,
size: response.per_page || 20,
totalPages: response.last_page || 1,
}, },
}; };
} catch (error) { } catch (error) {
console.error('[getPricingList] Error:', error); console.error('단가 목록 조회 오류:', error);
return { success: false, error: '서버 오류가 발생했습니다.' }; return { success: false, error: '단가 목록을 불러오는데 실패했습니다.' };
} }
} }
// ===== 통계 조회 ===== /**
* 단가 통계 조회
* GET /api/v1/pricing/stats
*/
export async function getPricingStats(): Promise<{ export async function getPricingStats(): Promise<{
success: boolean; success: boolean;
data?: PricingStats; data?: PricingStats;
error?: string; error?: string;
}> { }> {
try { try {
const stats: PricingStats = { const response = await apiClient.get<ApiPricingStats>('/pricing/stats');
total: mockPricingList.length,
inUse: mockPricingList.filter(p => p.status === 'in_use').length, return {
notRegistered: mockPricingList.filter(p => p.status === 'not_registered').length, success: true,
data: {
total: response.total || 0,
inUse: response.in_use || 0,
notRegistered: response.not_registered || 0,
},
}; };
return { success: true, data: stats };
} catch (error) { } catch (error) {
console.error('[getPricingStats] Error:', error); console.error('단가 통계 조회 오류:', error);
return { success: false, error: '서버 오류가 발생했습니다.' }; return { success: false, error: '통계를 불러오는데 실패했습니다.' };
} }
} }
// ===== 단일 삭제 ===== /**
export async function deletePricing(id: string): Promise<{ * 단가 상세 조회
success: boolean; * GET /api/v1/pricing/{id}
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: '서버 오류가 발생했습니다.' };
}
}
// ===== 단가 상세 조회 =====
export async function getPricingDetail(id: string): Promise<{ export async function getPricingDetail(id: string): Promise<{
success: boolean; success: boolean;
data?: Pricing; data?: Pricing;
error?: string; error?: string;
}> { }> {
try { try {
const pricing = mockPricingList.find(p => p.id === id); const response = await apiClient.get<ApiPricing>(`/pricing/${id}`);
if (!pricing) { return { success: true, data: transformPricing(response) };
return { success: false, error: '단가를 찾을 수 없습니다.' };
}
return { success: true, data: pricing };
} catch (error) { } catch (error) {
console.error('[getPricingDetail] Error:', error); console.error('단가 상세 조회 오류:', error);
return { success: false, error: '서버 오류가 발생했습니다.' }; return { success: false, error: '단가 정보를 찾을 수 없습니다.' };
} }
} }
// ===== 단가 생성 ===== /**
export async function createPricing(data: Omit<Pricing, 'id' | 'pricingNumber' | 'createdAt'>): Promise<{ * 단가 등록
* POST /api/v1/pricing
*/
export async function createPricing(data: PricingFormData): Promise<{
success: boolean; success: boolean;
data?: Pricing; data?: Pricing;
error?: string; error?: string;
}> { }> {
try { try {
const newId = String(mockPricingList.length + 1); const apiData = transformToApiRequest(data);
const newPricingNumber = `PRC-2026-${String(mockPricingList.length + 1).padStart(3, '0')}`; const response = await apiClient.post<ApiPricing>('/pricing', apiData);
return { success: true, data: transformPricing(response) };
const newPricing: Pricing = {
...data,
id: newId,
pricingNumber: newPricingNumber,
createdAt: new Date().toISOString().split('T')[0],
};
// 목데이터에는 추가하지 않음 (실제 API 연동 시 DB에 저장)
return { success: true, data: newPricing };
} catch (error) { } catch (error) {
console.error('[createPricing] Error:', error); console.error('단가 등록 오류:', error);
return { success: false, error: '서버 오류가 발생했습니다.' }; return { success: false, error: '단가 등록에 실패했습니다.' };
} }
} }
// ===== 단가 수정 ===== /**
export async function updatePricing(id: string, data: Partial<Pricing>): Promise<{ * 단가 수정
* PUT /api/v1/pricing/{id}
*/
export async function updatePricing(
id: string,
data: Partial<PricingFormData>
): Promise<{
success: boolean; success: boolean;
data?: Pricing; data?: Pricing;
error?: string; error?: string;
}> { }> {
try { try {
const index = mockPricingList.findIndex(p => p.id === id); const apiData = transformToApiRequest(data);
if (index === -1) { const response = await apiClient.put<ApiPricing>(`/pricing/${id}`, apiData);
return { success: false, error: '단가를 찾을 수 없습니다.' }; return { success: true, data: transformPricing(response) };
}
const updatedPricing: Pricing = {
...mockPricingList[index],
...data,
updatedAt: new Date().toISOString().split('T')[0],
};
// 목데이터에는 수정하지 않음 (실제 API 연동 시 DB에 업데이트)
return { success: true, data: updatedPricing };
} catch (error) { } catch (error) {
console.error('[updatePricing] Error:', error); console.error('단가 수정 오류:', error);
return { success: false, 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<{ export async function getVendorList(): Promise<{
success: boolean; success: boolean;
data?: { id: string; name: string }[]; data?: { id: string; name: string }[];
error?: string; error?: string;
}> { }> {
try { try {
// 목데이터에서 거래처 추출 const response = await apiClient.get<{
const vendors = [ data: ApiVendor[];
{ id: '1', name: '(주)슬라이드텍' }, }>('/clients', { params: { per_page: '100' } });
{ id: '2', name: '삼성전기' },
{ id: '3', name: '현대용접산업' }, const vendors = (response.data || []).map((v) => ({
{ id: '4', name: '철강볼트' }, id: String(v.id),
{ id: '5', name: '한국윤활유' }, name: v.name || '',
]; }));
return { success: true, data: vendors }; return { success: true, data: vendors };
} catch (error) { } catch (error) {
console.error('[getVendorList] Error:', error); console.error('거래처 목록 조회 오류:', error);
return { success: false, 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<ApiPricing>(`/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: '변경이력을 불러오는데 실패했습니다.' };
} }
} }

View File

@@ -39,6 +39,44 @@ export interface PricingStats {
notRegistered: number; // 미등록 단가 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;
}
// ===== 필터 옵션 ===== // ===== 필터 옵션 =====
// 품목유형 옵션 // 품목유형 옵션