feat: [시공관리] 계약관리 Frontend API 연동
- actions.ts Mock 데이터 → 실제 API 호출로 전환 - apiRequest 헬퍼 함수 구현 (인증, 에러 처리) - API 응답 snake_case → camelCase 변환 함수 추가 - CRUD 전체 기능 API 연동 완료
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import type {
|
import type {
|
||||||
Contract,
|
Contract,
|
||||||
ContractDetail,
|
ContractDetail,
|
||||||
@@ -10,327 +11,189 @@ import type {
|
|||||||
ContractFormData,
|
ContractFormData,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// 목업 데이터
|
/**
|
||||||
const MOCK_CONTRACTS: Contract[] = [
|
* 주일 기업 - 계약관리 Server Actions
|
||||||
{
|
* API 연동 버전
|
||||||
id: '1',
|
*/
|
||||||
contractCode: 'CT-2025-001',
|
|
||||||
partnerId: '1',
|
|
||||||
partnerName: '통신공사',
|
|
||||||
projectName: '강남역 통신시설 구축',
|
|
||||||
contractManagerId: 'hong',
|
|
||||||
contractManagerName: '홍길동',
|
|
||||||
constructionPMId: 'kim',
|
|
||||||
constructionPMName: '김PM',
|
|
||||||
totalLocations: 15,
|
|
||||||
contractAmount: 150000000,
|
|
||||||
contractStartDate: '2025-12-17',
|
|
||||||
contractEndDate: '2026-06-17',
|
|
||||||
status: 'pending',
|
|
||||||
stage: 'estimate_selected',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-01',
|
|
||||||
updatedAt: '2025-01-01',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '1',
|
|
||||||
biddingCode: 'BID-2025-001',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
contractCode: 'CT-2025-002',
|
|
||||||
partnerId: '2',
|
|
||||||
partnerName: '야사건설',
|
|
||||||
projectName: '판교 IT단지 배선공사',
|
|
||||||
contractManagerId: 'hong',
|
|
||||||
contractManagerName: '홍길동',
|
|
||||||
constructionPMId: 'lee',
|
|
||||||
constructionPMName: '이PM',
|
|
||||||
totalLocations: 28,
|
|
||||||
contractAmount: 280000000,
|
|
||||||
contractStartDate: '2025-11-01',
|
|
||||||
contractEndDate: '2026-03-31',
|
|
||||||
status: 'pending',
|
|
||||||
stage: 'estimate_progress',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-02',
|
|
||||||
updatedAt: '2025-01-02',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '2',
|
|
||||||
biddingCode: 'BID-2025-002',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
contractCode: 'CT-2025-003',
|
|
||||||
partnerId: '3',
|
|
||||||
partnerName: '여의건설',
|
|
||||||
projectName: '여의도 오피스빌딩 통신설비',
|
|
||||||
contractManagerId: 'kim',
|
|
||||||
contractManagerName: '김철수',
|
|
||||||
constructionPMId: 'park',
|
|
||||||
constructionPMName: '박PM',
|
|
||||||
totalLocations: 42,
|
|
||||||
contractAmount: 420000000,
|
|
||||||
contractStartDate: '2025-10-15',
|
|
||||||
contractEndDate: '2026-04-15',
|
|
||||||
status: 'pending',
|
|
||||||
stage: 'delivery',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-03',
|
|
||||||
updatedAt: '2025-01-03',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '3',
|
|
||||||
biddingCode: 'BID-2025-003',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
contractCode: 'CT-2025-004',
|
|
||||||
partnerId: '1',
|
|
||||||
partnerName: '통신공사',
|
|
||||||
projectName: '송파 데이터센터 증설',
|
|
||||||
contractManagerId: 'hong',
|
|
||||||
contractManagerName: '홍길동',
|
|
||||||
constructionPMId: 'kim',
|
|
||||||
constructionPMName: '김PM',
|
|
||||||
totalLocations: 58,
|
|
||||||
contractAmount: 580000000,
|
|
||||||
contractStartDate: '2025-09-01',
|
|
||||||
contractEndDate: '2026-02-28',
|
|
||||||
status: 'completed',
|
|
||||||
stage: 'inspection',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-04',
|
|
||||||
updatedAt: '2025-01-04',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '4',
|
|
||||||
biddingCode: 'BID-2025-004',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
contractCode: 'CT-2025-005',
|
|
||||||
partnerId: '2',
|
|
||||||
partnerName: '야사건설',
|
|
||||||
projectName: '분당 스마트빌딩 LAN공사',
|
|
||||||
contractManagerId: 'lee',
|
|
||||||
contractManagerName: '이영희',
|
|
||||||
constructionPMId: 'lee',
|
|
||||||
constructionPMName: '이PM',
|
|
||||||
totalLocations: 12,
|
|
||||||
contractAmount: 95000000,
|
|
||||||
contractStartDate: '2025-12-01',
|
|
||||||
contractEndDate: '2026-01-31',
|
|
||||||
status: 'pending',
|
|
||||||
stage: 'installation',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-05',
|
|
||||||
updatedAt: '2025-01-05',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '5',
|
|
||||||
biddingCode: 'BID-2025-005',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
contractCode: 'CT-2025-006',
|
|
||||||
partnerId: '3',
|
|
||||||
partnerName: '여의건설',
|
|
||||||
projectName: '마포 복합시설 CCTV설치',
|
|
||||||
contractManagerId: 'hong',
|
|
||||||
contractManagerName: '홍길동',
|
|
||||||
constructionPMId: 'park',
|
|
||||||
constructionPMName: '박PM',
|
|
||||||
totalLocations: 8,
|
|
||||||
contractAmount: 75000000,
|
|
||||||
contractStartDate: '2025-08-01',
|
|
||||||
contractEndDate: '2025-10-31',
|
|
||||||
status: 'completed',
|
|
||||||
stage: 'estimate_selected',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-06',
|
|
||||||
updatedAt: '2025-01-06',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '6',
|
|
||||||
biddingCode: 'BID-2025-006',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
contractCode: 'CT-2025-007',
|
|
||||||
partnerId: '1',
|
|
||||||
partnerName: '통신공사',
|
|
||||||
projectName: '용산 아파트 인터폰교체',
|
|
||||||
contractManagerId: 'kim',
|
|
||||||
contractManagerName: '김철수',
|
|
||||||
constructionPMId: 'kim',
|
|
||||||
constructionPMName: '김PM',
|
|
||||||
totalLocations: 120,
|
|
||||||
contractAmount: 45000000,
|
|
||||||
contractStartDate: '2025-07-15',
|
|
||||||
contractEndDate: '2025-09-15',
|
|
||||||
status: 'completed',
|
|
||||||
stage: 'estimate_progress',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-07',
|
|
||||||
updatedAt: '2025-01-07',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '7',
|
|
||||||
biddingCode: 'BID-2025-007',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
contractCode: 'CT-2025-008',
|
|
||||||
partnerId: '2',
|
|
||||||
partnerName: '야사건설',
|
|
||||||
projectName: '성수동 공장 방범설비',
|
|
||||||
contractManagerId: 'lee',
|
|
||||||
contractManagerName: '이영희',
|
|
||||||
constructionPMId: 'lee',
|
|
||||||
constructionPMName: '이PM',
|
|
||||||
totalLocations: 24,
|
|
||||||
contractAmount: 120000000,
|
|
||||||
contractStartDate: '2025-11-15',
|
|
||||||
contractEndDate: '2026-02-15',
|
|
||||||
status: 'pending',
|
|
||||||
stage: 'other',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-08',
|
|
||||||
updatedAt: '2025-01-08',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '8',
|
|
||||||
biddingCode: 'BID-2025-008',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '9',
|
|
||||||
contractCode: 'CT-2025-009',
|
|
||||||
partnerId: '3',
|
|
||||||
partnerName: '여의건설',
|
|
||||||
projectName: '강서 물류센터 네트워크',
|
|
||||||
contractManagerId: 'hong',
|
|
||||||
contractManagerName: '홍길동',
|
|
||||||
constructionPMId: 'park',
|
|
||||||
constructionPMName: '박PM',
|
|
||||||
totalLocations: 35,
|
|
||||||
contractAmount: 320000000,
|
|
||||||
contractStartDate: '2025-06-01',
|
|
||||||
contractEndDate: '2025-11-30',
|
|
||||||
status: 'completed',
|
|
||||||
stage: 'inspection',
|
|
||||||
remarks: '',
|
|
||||||
createdAt: '2025-01-09',
|
|
||||||
updatedAt: '2025-01-09',
|
|
||||||
createdBy: 'system',
|
|
||||||
biddingId: '9',
|
|
||||||
biddingCode: 'BID-2025-009',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 계약 목록 조회
|
// API 기본 URL
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://api.sam.kr';
|
||||||
|
const API_KEY = process.env.API_KEY || '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 요청 헬퍼 함수
|
||||||
|
*/
|
||||||
|
async function apiRequest<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<{ success: boolean; data?: T; error?: string; message?: string }> {
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const accessToken = cookieStore.get('access_token')?.value;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-KEY': API_KEY,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${API_BASE_URL}/api/v1${endpoint}`;
|
||||||
|
console.log('🔵 [Contract API]', options.method || 'GET', url);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('🔵 [Contract API] Response status:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: result.message || `API 오류: ${response.status}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.success ?? true,
|
||||||
|
data: result.data,
|
||||||
|
message: result.message,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API request error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 응답 → 프론트엔드 타입 변환
|
||||||
|
*/
|
||||||
|
function transformContract(apiData: Record<string, unknown>): Contract {
|
||||||
|
return {
|
||||||
|
id: String(apiData.id),
|
||||||
|
contractCode: String(apiData.contract_code || ''),
|
||||||
|
partnerId: String(apiData.partner_id || ''),
|
||||||
|
partnerName: String(apiData.partner_name || ''),
|
||||||
|
projectName: String(apiData.project_name || ''),
|
||||||
|
contractManagerId: String(apiData.contract_manager_id || ''),
|
||||||
|
contractManagerName: String(apiData.contract_manager_name || ''),
|
||||||
|
constructionPMId: String(apiData.construction_pm_id || ''),
|
||||||
|
constructionPMName: String(apiData.construction_pm_name || ''),
|
||||||
|
totalLocations: Number(apiData.total_locations || 0),
|
||||||
|
contractAmount: Number(apiData.contract_amount || 0),
|
||||||
|
contractStartDate: apiData.contract_start_date ? String(apiData.contract_start_date) : null,
|
||||||
|
contractEndDate: apiData.contract_end_date ? String(apiData.contract_end_date) : null,
|
||||||
|
status: (apiData.status as 'pending' | 'completed') || 'pending',
|
||||||
|
stage: (apiData.stage as Contract['stage']) || 'estimate_selected',
|
||||||
|
remarks: String(apiData.remarks || ''),
|
||||||
|
createdAt: String(apiData.created_at || ''),
|
||||||
|
updatedAt: String(apiData.updated_at || ''),
|
||||||
|
createdBy: String(apiData.created_by || ''),
|
||||||
|
biddingId: String(apiData.bidding_id || ''),
|
||||||
|
biddingCode: String(apiData.bidding_code || ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프론트엔드 → API 요청 타입 변환
|
||||||
|
*/
|
||||||
|
function transformToApiRequest(data: Partial<ContractFormData>): Record<string, unknown> {
|
||||||
|
const apiData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (data.contractCode !== undefined) apiData.contract_code = data.contractCode;
|
||||||
|
if (data.projectName !== undefined) apiData.project_name = data.projectName;
|
||||||
|
if (data.partnerId !== undefined) apiData.partner_id = data.partnerId || null;
|
||||||
|
if (data.partnerName !== undefined) apiData.partner_name = data.partnerName || null;
|
||||||
|
if (data.contractManagerId !== undefined) apiData.contract_manager_id = data.contractManagerId || null;
|
||||||
|
if (data.contractManagerName !== undefined) apiData.contract_manager_name = data.contractManagerName || null;
|
||||||
|
if (data.totalLocations !== undefined) apiData.total_locations = data.totalLocations;
|
||||||
|
if (data.contractAmount !== undefined) apiData.contract_amount = data.contractAmount;
|
||||||
|
if (data.contractStartDate !== undefined) apiData.contract_start_date = data.contractStartDate || null;
|
||||||
|
if (data.contractEndDate !== undefined) apiData.contract_end_date = data.contractEndDate || null;
|
||||||
|
if (data.status !== undefined) apiData.status = data.status;
|
||||||
|
if (data.remarks !== undefined) apiData.remarks = data.remarks || null;
|
||||||
|
|
||||||
|
return apiData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계약 목록 조회
|
||||||
|
*/
|
||||||
export async function getContractList(filter?: ContractFilter): Promise<{
|
export async function getContractList(filter?: ContractFilter): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: ContractListResponse;
|
data?: ContractListResponse;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
let filteredData = [...MOCK_CONTRACTS];
|
if (filter?.search) params.append('search', filter.search);
|
||||||
|
if (filter?.status && filter.status !== 'all') params.append('status', filter.status);
|
||||||
|
if (filter?.stage && filter.stage !== 'all') params.append('stage', filter.stage);
|
||||||
|
if (filter?.partnerId && filter.partnerId !== 'all') params.append('partner_id', filter.partnerId);
|
||||||
|
if (filter?.contractManagerId && filter.contractManagerId !== 'all') params.append('contract_manager_id', filter.contractManagerId);
|
||||||
|
if (filter?.constructionPMId && filter.constructionPMId !== 'all') params.append('construction_pm_id', filter.constructionPMId);
|
||||||
|
if (filter?.startDate) params.append('start_date', filter.startDate);
|
||||||
|
if (filter?.endDate) params.append('end_date', filter.endDate);
|
||||||
|
if (filter?.page) params.append('page', String(filter.page));
|
||||||
|
if (filter?.size) params.append('per_page', String(filter.size));
|
||||||
|
|
||||||
// 검색 필터
|
// 정렬 파라미터 변환
|
||||||
if (filter?.search) {
|
if (filter?.sortBy) {
|
||||||
const search = filter.search.toLowerCase();
|
const sortMap: Record<string, { field: string; dir: string }> = {
|
||||||
filteredData = filteredData.filter(
|
contractDateDesc: { field: 'created_at', dir: 'desc' },
|
||||||
(item) =>
|
contractDateAsc: { field: 'created_at', dir: 'asc' },
|
||||||
item.contractCode.toLowerCase().includes(search) ||
|
partnerNameAsc: { field: 'partner_name', dir: 'asc' },
|
||||||
item.partnerName.toLowerCase().includes(search) ||
|
partnerNameDesc: { field: 'partner_name', dir: 'desc' },
|
||||||
item.projectName.toLowerCase().includes(search)
|
projectNameAsc: { field: 'project_name', dir: 'asc' },
|
||||||
);
|
projectNameDesc: { field: 'project_name', dir: 'desc' },
|
||||||
|
amountDesc: { field: 'contract_amount', dir: 'desc' },
|
||||||
|
amountAsc: { field: 'contract_amount', dir: 'asc' },
|
||||||
|
};
|
||||||
|
const sort = sortMap[filter.sortBy];
|
||||||
|
if (sort) {
|
||||||
|
params.append('sort_by', sort.field);
|
||||||
|
params.append('sort_dir', sort.dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 상태 필터
|
const queryString = params.toString();
|
||||||
if (filter?.status && filter.status !== 'all') {
|
const endpoint = `/construction/contracts${queryString ? `?${queryString}` : ''}`;
|
||||||
filteredData = filteredData.filter((item) => item.status === filter.status);
|
|
||||||
|
const result = await apiRequest<{
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
current_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
last_page: number;
|
||||||
|
}>(endpoint);
|
||||||
|
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
return { success: false, error: result.error || '계약 목록 조회에 실패했습니다.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 단계 필터
|
const apiData = result.data;
|
||||||
if (filter?.stage && filter.stage !== 'all') {
|
const items = (apiData.data || []).map(transformContract);
|
||||||
filteredData = filteredData.filter((item) => item.stage === filter.stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 거래처 필터
|
|
||||||
if (filter?.partnerId && filter.partnerId !== 'all') {
|
|
||||||
filteredData = filteredData.filter((item) => item.partnerId === filter.partnerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 계약담당자 필터
|
|
||||||
if (filter?.contractManagerId && filter.contractManagerId !== 'all') {
|
|
||||||
filteredData = filteredData.filter((item) => item.contractManagerId === filter.contractManagerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 공사PM 필터
|
|
||||||
if (filter?.constructionPMId && filter.constructionPMId !== 'all') {
|
|
||||||
filteredData = filteredData.filter((item) => item.constructionPMId === filter.constructionPMId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 날짜 필터
|
|
||||||
if (filter?.startDate) {
|
|
||||||
filteredData = filteredData.filter(
|
|
||||||
(item) => item.contractStartDate && item.contractStartDate >= filter.startDate!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filter?.endDate) {
|
|
||||||
filteredData = filteredData.filter(
|
|
||||||
(item) => item.contractEndDate && item.contractEndDate <= filter.endDate!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 정렬
|
|
||||||
const sortBy = filter?.sortBy || 'contractDateDesc';
|
|
||||||
switch (sortBy) {
|
|
||||||
case 'contractDateDesc':
|
|
||||||
filteredData.sort((a, b) => {
|
|
||||||
if (!a.contractStartDate) return 1;
|
|
||||||
if (!b.contractStartDate) return -1;
|
|
||||||
return new Date(b.contractStartDate).getTime() - new Date(a.contractStartDate).getTime();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'contractDateAsc':
|
|
||||||
filteredData.sort((a, b) => {
|
|
||||||
if (!a.contractStartDate) return 1;
|
|
||||||
if (!b.contractStartDate) return -1;
|
|
||||||
return new Date(a.contractStartDate).getTime() - new Date(b.contractStartDate).getTime();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'partnerNameAsc':
|
|
||||||
filteredData.sort((a, b) => a.partnerName.localeCompare(b.partnerName, 'ko'));
|
|
||||||
break;
|
|
||||||
case 'partnerNameDesc':
|
|
||||||
filteredData.sort((a, b) => b.partnerName.localeCompare(a.partnerName, 'ko'));
|
|
||||||
break;
|
|
||||||
case 'projectNameAsc':
|
|
||||||
filteredData.sort((a, b) => a.projectName.localeCompare(b.projectName, 'ko'));
|
|
||||||
break;
|
|
||||||
case 'projectNameDesc':
|
|
||||||
filteredData.sort((a, b) => b.projectName.localeCompare(a.projectName, 'ko'));
|
|
||||||
break;
|
|
||||||
case 'amountDesc':
|
|
||||||
filteredData.sort((a, b) => b.contractAmount - a.contractAmount);
|
|
||||||
break;
|
|
||||||
case 'amountAsc':
|
|
||||||
filteredData.sort((a, b) => a.contractAmount - b.contractAmount);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 페이지네이션
|
|
||||||
const page = filter?.page || 1;
|
|
||||||
const size = filter?.size || 20;
|
|
||||||
const startIndex = (page - 1) * size;
|
|
||||||
const paginatedData = filteredData.slice(startIndex, startIndex + size);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
items: paginatedData,
|
items,
|
||||||
total: filteredData.length,
|
total: apiData.total || 0,
|
||||||
page,
|
page: apiData.current_page || 1,
|
||||||
size,
|
size: apiData.per_page || 20,
|
||||||
totalPages: Math.ceil(filteredData.length / size),
|
totalPages: apiData.last_page || 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -339,85 +202,214 @@ export async function getContractList(filter?: ContractFilter): Promise<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 계약 통계 조회
|
/**
|
||||||
|
* 계약 통계 조회
|
||||||
|
*/
|
||||||
export async function getContractStats(): Promise<{
|
export async function getContractStats(): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: ContractStats;
|
data?: ContractStats;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
const result = await apiRequest<{
|
||||||
|
total_count: number;
|
||||||
|
pending_count: number;
|
||||||
|
completed_count: number;
|
||||||
|
}>('/construction/contracts/stats');
|
||||||
|
|
||||||
const stats: ContractStats = {
|
if (!result.success || !result.data) {
|
||||||
total: MOCK_CONTRACTS.length,
|
return { success: false, error: result.error || '통계를 불러오는데 실패했습니다.' };
|
||||||
pending: MOCK_CONTRACTS.filter((c) => c.status === 'pending').length,
|
}
|
||||||
completed: MOCK_CONTRACTS.filter((c) => c.status === 'completed').length,
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
total: result.data.total_count || 0,
|
||||||
|
pending: result.data.pending_count || 0,
|
||||||
|
completed: result.data.completed_count || 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { success: true, data: stats };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('getContractStats error:', error);
|
console.error('getContractStats error:', error);
|
||||||
return { success: false, error: '통계를 불러오는데 실패했습니다.' };
|
return { success: false, error: '통계를 불러오는데 실패했습니다.' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 단계별 건수 조회
|
/**
|
||||||
|
* 단계별 건수 조회
|
||||||
|
*/
|
||||||
export async function getContractStageCounts(): Promise<{
|
export async function getContractStageCounts(): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: ContractStageCount;
|
data?: ContractStageCount;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
const result = await apiRequest<{
|
||||||
|
estimate_selected: number;
|
||||||
|
estimate_progress: number;
|
||||||
|
delivery: number;
|
||||||
|
installation: number;
|
||||||
|
inspection: number;
|
||||||
|
other: number;
|
||||||
|
}>('/construction/contracts/stage-counts');
|
||||||
|
|
||||||
const counts: ContractStageCount = {
|
if (!result.success || !result.data) {
|
||||||
estimateSelected: MOCK_CONTRACTS.filter((c) => c.stage === 'estimate_selected').length,
|
return { success: false, error: result.error || '단계별 건수를 불러오는데 실패했습니다.' };
|
||||||
estimateProgress: MOCK_CONTRACTS.filter((c) => c.stage === 'estimate_progress').length,
|
}
|
||||||
delivery: MOCK_CONTRACTS.filter((c) => c.stage === 'delivery').length,
|
|
||||||
installation: MOCK_CONTRACTS.filter((c) => c.stage === 'installation').length,
|
return {
|
||||||
inspection: MOCK_CONTRACTS.filter((c) => c.stage === 'inspection').length,
|
success: true,
|
||||||
other: MOCK_CONTRACTS.filter((c) => c.stage === 'other').length,
|
data: {
|
||||||
|
estimateSelected: result.data.estimate_selected || 0,
|
||||||
|
estimateProgress: result.data.estimate_progress || 0,
|
||||||
|
delivery: result.data.delivery || 0,
|
||||||
|
installation: result.data.installation || 0,
|
||||||
|
inspection: result.data.inspection || 0,
|
||||||
|
other: result.data.other || 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { success: true, data: counts };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('getContractStageCounts error:', error);
|
console.error('getContractStageCounts error:', error);
|
||||||
return { success: false, error: '단계별 건수를 불러오는데 실패했습니다.' };
|
return { success: false, error: '단계별 건수를 불러오는데 실패했습니다.' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 계약 단건 조회
|
/**
|
||||||
|
* 계약 단건 조회
|
||||||
|
*/
|
||||||
export async function getContract(id: string): Promise<{
|
export async function getContract(id: string): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: Contract;
|
data?: Contract;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
const result = await apiRequest<Record<string, unknown>>(`/construction/contracts/${id}`);
|
||||||
|
|
||||||
const contract = MOCK_CONTRACTS.find((c) => c.id === id);
|
if (!result.success || !result.data) {
|
||||||
if (!contract) {
|
return { success: false, error: result.error || '계약 정보를 찾을 수 없습니다.' };
|
||||||
return { success: false, error: '계약 정보를 찾을 수 없습니다.' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, data: contract };
|
return { success: true, data: transformContract(result.data) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('getContract error:', error);
|
console.error('getContract error:', error);
|
||||||
return { success: false, error: '계약 정보를 불러오는데 실패했습니다.' };
|
return { success: false, error: '계약 정보를 불러오는데 실패했습니다.' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 계약 삭제
|
/**
|
||||||
|
* 계약 상세 조회 (첨부파일 포함)
|
||||||
|
*/
|
||||||
|
export async function getContractDetail(id: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: ContractDetail;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const result = await apiRequest<Record<string, unknown>>(`/construction/contracts/${id}`);
|
||||||
|
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
return { success: false, error: result.error || '계약 정보를 찾을 수 없습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const contract = transformContract(result.data);
|
||||||
|
|
||||||
|
// 첨부파일 정보 변환 (API에서 반환하는 경우)
|
||||||
|
const contractFile = result.data.contract_file as Record<string, unknown> | null;
|
||||||
|
const attachmentsData = result.data.attachments as Record<string, unknown>[] | undefined;
|
||||||
|
|
||||||
|
const detail: ContractDetail = {
|
||||||
|
...contract,
|
||||||
|
contractFile: contractFile ? {
|
||||||
|
id: String(contractFile.id || ''),
|
||||||
|
fileName: String(contractFile.file_name || contractFile.fileName || ''),
|
||||||
|
fileUrl: String(contractFile.file_url || contractFile.fileUrl || ''),
|
||||||
|
uploadedAt: String(contractFile.uploaded_at || contractFile.uploadedAt || ''),
|
||||||
|
} : null,
|
||||||
|
attachments: (attachmentsData || []).map((att) => ({
|
||||||
|
id: String(att.id || ''),
|
||||||
|
fileName: String(att.file_name || att.fileName || ''),
|
||||||
|
fileSize: Number(att.file_size || att.fileSize || 0),
|
||||||
|
fileUrl: String(att.file_url || att.fileUrl || ''),
|
||||||
|
uploadedAt: String(att.uploaded_at || att.uploadedAt || ''),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { success: true, data: detail };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getContractDetail error:', error);
|
||||||
|
return { success: false, error: '계약 상세 정보를 불러오는데 실패했습니다.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계약 등록
|
||||||
|
*/
|
||||||
|
export async function createContract(
|
||||||
|
data: ContractFormData
|
||||||
|
): Promise<{ success: boolean; data?: Contract; error?: string }> {
|
||||||
|
try {
|
||||||
|
const apiData = transformToApiRequest(data);
|
||||||
|
|
||||||
|
const result = await apiRequest<Record<string, unknown>>('/construction/contracts', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(apiData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
return { success: false, error: result.error || '계약 등록에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: transformContract(result.data) };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('createContract error:', error);
|
||||||
|
return { success: false, error: '계약 등록에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계약 수정
|
||||||
|
*/
|
||||||
|
export async function updateContract(
|
||||||
|
id: string,
|
||||||
|
data: Partial<ContractFormData>
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: Contract;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const apiData = transformToApiRequest(data);
|
||||||
|
|
||||||
|
const result = await apiRequest<Record<string, unknown>>(`/construction/contracts/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(apiData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
return { success: false, error: result.error || '계약 수정에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: transformContract(result.data) };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('updateContract error:', error);
|
||||||
|
return { success: false, error: '계약 수정에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계약 삭제
|
||||||
|
*/
|
||||||
export async function deleteContract(id: string): Promise<{
|
export async function deleteContract(id: string): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
const result = await apiRequest(`/construction/contracts/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
const index = MOCK_CONTRACTS.findIndex((c) => c.id === id);
|
if (!result.success) {
|
||||||
if (index === -1) {
|
return { success: false, error: result.error || '계약 삭제에 실패했습니다.' };
|
||||||
return { success: false, error: '계약 정보를 찾을 수 없습니다.' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -427,91 +419,27 @@ export async function deleteContract(id: string): Promise<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 계약 일괄 삭제
|
/**
|
||||||
|
* 계약 일괄 삭제
|
||||||
|
*/
|
||||||
export async function deleteContracts(ids: string[]): Promise<{
|
export async function deleteContracts(ids: string[]): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
deletedCount?: number;
|
deletedCount?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
const result = await apiRequest('/construction/contracts/bulk', {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: JSON.stringify({ ids: ids.map(id => Number(id)) }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, error: result.error || '일괄 삭제에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, deletedCount: ids.length };
|
return { success: true, deletedCount: ids.length };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('deleteContracts error:', error);
|
console.error('deleteContracts error:', error);
|
||||||
return { success: false, error: '일괄 삭제에 실패했습니다.' };
|
return { success: false, error: '일괄 삭제에 실패했습니다.' };
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 계약 상세 조회 (첨부파일 포함)
|
|
||||||
export async function getContractDetail(id: string): Promise<{
|
|
||||||
success: boolean;
|
|
||||||
data?: ContractDetail;
|
|
||||||
error?: string;
|
|
||||||
}> {
|
|
||||||
try {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
||||||
|
|
||||||
const contract = MOCK_CONTRACTS.find((c) => c.id === id);
|
|
||||||
if (!contract) {
|
|
||||||
return { success: false, error: '계약 정보를 찾을 수 없습니다.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContractDetail로 변환 (첨부파일 목데이터 포함)
|
|
||||||
const contractDetail: ContractDetail = {
|
|
||||||
...contract,
|
|
||||||
// 계약서 파일 목업 데이터
|
|
||||||
contractFile: {
|
|
||||||
id: '100',
|
|
||||||
fileName: '계약서_CT-2025-001.pdf',
|
|
||||||
fileUrl: '/files/contract.pdf',
|
|
||||||
uploadedAt: contract.createdAt,
|
|
||||||
},
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
id: 'att-1',
|
|
||||||
fileName: '견적서.pdf',
|
|
||||||
fileSize: 1024000,
|
|
||||||
fileUrl: '/files/estimate.pdf',
|
|
||||||
uploadedAt: contract.createdAt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'att-2',
|
|
||||||
fileName: '시방서.pdf',
|
|
||||||
fileSize: 2048000,
|
|
||||||
fileUrl: '/files/spec.pdf',
|
|
||||||
uploadedAt: contract.createdAt,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return { success: true, data: contractDetail };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('getContractDetail error:', error);
|
|
||||||
return { success: false, error: '계약 상세 정보를 불러오는데 실패했습니다.' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 계약 수정
|
|
||||||
export async function updateContract(
|
|
||||||
id: string,
|
|
||||||
_data: Partial<ContractFormData>
|
|
||||||
): Promise<{
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
}> {
|
|
||||||
try {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
const index = MOCK_CONTRACTS.findIndex((c) => c.id === id);
|
|
||||||
if (index === -1) {
|
|
||||||
return { success: false, error: '계약 정보를 찾을 수 없습니다.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 실제 API 연동 시 데이터 업데이트 로직
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('updateContract error:', error);
|
|
||||||
return { success: false, error: '계약 수정에 실패했습니다.' };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user