Files
sam-react-prod/src/components/business/construction/contract/actions.ts
byeongcheolryu d036ce4f42 feat(WEB): 견적서 V2 컴포넌트 개선 및 미리보기 모달 패턴 적용
- LocationDetailPanel: 6개 탭 구현 (본체, 가이드레일, 케이스, 하단마감재, 모터&제어기, 부자재)
- 각 탭별 다른 테이블 컬럼 구조 적용
- QuoteSummaryPanel: 개소별/상세별 합계 패널 개선
- QuotePreviewModal: EstimateDocumentModal 패턴 적용 (헤더+버튼 영역 분리)
- Input value → defaultValue 변경으로 React 경고 해결
- 팩스/카카오톡 버튼 제거

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 15:26:17 +09:00

539 lines
15 KiB
TypeScript

'use server';
import type {
Contract,
ContractDetail,
ContractStats,
ContractStageCount,
ContractListResponse,
ContractFilter,
ContractFormData,
} from './types';
// 목업 데이터
const MOCK_CONTRACTS: Contract[] = [
{
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',
},
];
// 계약 목록 조회
export async function getContractList(filter?: ContractFilter): Promise<{
success: boolean;
data?: ContractListResponse;
error?: string;
}> {
try {
await new Promise((resolve) => setTimeout(resolve, 300));
let filteredData = [...MOCK_CONTRACTS];
// 검색 필터
if (filter?.search) {
const search = filter.search.toLowerCase();
filteredData = filteredData.filter(
(item) =>
item.contractCode.toLowerCase().includes(search) ||
item.partnerName.toLowerCase().includes(search) ||
item.projectName.toLowerCase().includes(search)
);
}
// 상태 필터
if (filter?.status && filter.status !== 'all') {
filteredData = filteredData.filter((item) => item.status === filter.status);
}
// 단계 필터
if (filter?.stage && filter.stage !== 'all') {
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 {
success: true,
data: {
items: paginatedData,
total: filteredData.length,
page,
size,
totalPages: Math.ceil(filteredData.length / size),
},
};
} catch (error) {
console.error('getContractList error:', error);
return { success: false, error: '계약 목록을 불러오는데 실패했습니다.' };
}
}
// 계약 통계 조회
export async function getContractStats(): Promise<{
success: boolean;
data?: ContractStats;
error?: string;
}> {
try {
await new Promise((resolve) => setTimeout(resolve, 100));
const stats: ContractStats = {
total: MOCK_CONTRACTS.length,
pending: MOCK_CONTRACTS.filter((c) => c.status === 'pending').length,
completed: MOCK_CONTRACTS.filter((c) => c.status === 'completed').length,
};
return { success: true, data: stats };
} catch (error) {
console.error('getContractStats error:', error);
return { success: false, error: '통계를 불러오는데 실패했습니다.' };
}
}
// 단계별 건수 조회
export async function getContractStageCounts(): Promise<{
success: boolean;
data?: ContractStageCount;
error?: string;
}> {
try {
await new Promise((resolve) => setTimeout(resolve, 100));
const counts: ContractStageCount = {
estimateSelected: MOCK_CONTRACTS.filter((c) => c.stage === 'estimate_selected').length,
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,
inspection: MOCK_CONTRACTS.filter((c) => c.stage === 'inspection').length,
other: MOCK_CONTRACTS.filter((c) => c.stage === 'other').length,
};
return { success: true, data: counts };
} catch (error) {
console.error('getContractStageCounts error:', error);
return { success: false, error: '단계별 건수를 불러오는데 실패했습니다.' };
}
}
// 계약 단건 조회
export async function getContract(id: string): Promise<{
success: boolean;
data?: Contract;
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: '계약 정보를 찾을 수 없습니다.' };
}
return { success: true, data: contract };
} catch (error) {
console.error('getContract error:', error);
return { success: false, error: '계약 정보를 불러오는데 실패했습니다.' };
}
}
// 계약 삭제
export async function deleteContract(id: string): Promise<{
success: boolean;
error?: string;
}> {
try {
await new Promise((resolve) => setTimeout(resolve, 300));
const index = MOCK_CONTRACTS.findIndex((c) => c.id === id);
if (index === -1) {
return { success: false, error: '계약 정보를 찾을 수 없습니다.' };
}
return { success: true };
} catch (error) {
console.error('deleteContract error:', error);
return { success: false, error: '계약 삭제에 실패했습니다.' };
}
}
// 계약 일괄 삭제
export async function deleteContracts(ids: string[]): Promise<{
success: boolean;
deletedCount?: number;
error?: string;
}> {
try {
await new Promise((resolve) => setTimeout(resolve, 500));
return { success: true, deletedCount: ids.length };
} catch (error) {
console.error('deleteContracts error:', 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: '계약 수정에 실패했습니다.' };
}
}
// 계약 생성 (변경 계약서 생성 포함)
export async function createContract(
_data: ContractFormData
): Promise<{
success: boolean;
data?: { id: string };
error?: string;
}> {
try {
await new Promise((resolve) => setTimeout(resolve, 500));
// TODO: 실제 API 연동 시 데이터 생성 로직
// 새 계약 ID 생성 (목업)
const newId = String(MOCK_CONTRACTS.length + 1);
return { success: true, data: { id: newId } };
} catch (error) {
console.error('createContract error:', error);
return { success: false, error: '계약 생성에 실패했습니다.' };
}
}