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:
@@ -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 수정 완료
|
||||
|
||||
---
|
||||
|
||||
@@ -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<ApiCategory>('/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<ApiCategory>(`/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: '순서 변경에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -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<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 {
|
||||
let filtered = [...mockEstimates];
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
// 검색 필터
|
||||
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<string, { field: string; dir: string }> = {
|
||||
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<ApiEstimate>(`/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<ApiEstimateDetail>(`/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<ApiEstimateStats>('/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<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 };
|
||||
} 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: '일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -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<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;
|
||||
data?: { items: Pricing[]; total: number };
|
||||
data?: PricingListResponse;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
let filtered = [...mockPricingList];
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
// 품목유형 필터
|
||||
if (params?.itemType && params.itemType !== 'all') {
|
||||
const typeMap: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
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<string, string> = {
|
||||
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<string, { field: string; dir: string }> = {
|
||||
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<ApiPricingStats>('/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<ApiPricing>(`/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<Pricing, 'id' | 'pricingNumber' | 'createdAt'>): 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<ApiPricing>('/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<Pricing>): Promise<{
|
||||
/**
|
||||
* 단가 수정
|
||||
* PUT /api/v1/pricing/{id}
|
||||
*/
|
||||
export async function updatePricing(
|
||||
id: string,
|
||||
data: Partial<PricingFormData>
|
||||
): 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<ApiPricing>(`/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<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: '변경이력을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ===== 필터 옵션 =====
|
||||
|
||||
// 품목유형 옵션
|
||||
|
||||
Reference in New Issue
Block a user