- Laravel ApiResponse 래퍼 구조 반영 ({ success, data })
- getContractList: response.data.data로 페이지네이션 데이터 접근
- getContractStats: response.data로 통계 데이터 접근
- getContractStageCounts: response.data로 단계별 건수 접근
- getContract/getContractDetail: response.data로 단건 데이터 접근
- createContract/updateContract: response.data로 생성/수정 결과 접근
433 lines
13 KiB
TypeScript
433 lines
13 KiB
TypeScript
'use server';
|
|
|
|
import type {
|
|
Contract,
|
|
ContractDetail,
|
|
ContractStats,
|
|
ContractStageCount,
|
|
ContractListResponse,
|
|
ContractFilter,
|
|
ContractFormData,
|
|
} from './types';
|
|
import { apiClient } from '@/lib/api';
|
|
|
|
/**
|
|
* 주일 기업 - 계약관리 Server Actions
|
|
* 표준화된 apiClient 사용 버전
|
|
*/
|
|
|
|
// ========================================
|
|
// API 응답 타입
|
|
// ========================================
|
|
|
|
interface ApiContract {
|
|
id: number;
|
|
contract_code: string;
|
|
partner_id: number | null;
|
|
partner_name: string | null;
|
|
project_name: string;
|
|
contract_manager_id: number | null;
|
|
contract_manager_name: string | null;
|
|
construction_pm_id: number | null;
|
|
construction_pm_name: string | null;
|
|
total_locations: number;
|
|
contract_amount: number;
|
|
contract_start_date: string | null;
|
|
contract_end_date: string | null;
|
|
status: 'pending' | 'completed';
|
|
stage: string;
|
|
remarks: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
created_by: string | null;
|
|
bidding_id: number | null;
|
|
bidding_code: string | null;
|
|
contract_file?: ApiContractFile | null;
|
|
attachments?: ApiAttachment[];
|
|
}
|
|
|
|
interface ApiContractFile {
|
|
id: number;
|
|
file_name: string;
|
|
file_url: string;
|
|
uploaded_at: string;
|
|
}
|
|
|
|
interface ApiAttachment {
|
|
id: number;
|
|
file_name: string;
|
|
file_size: number;
|
|
file_url: string;
|
|
uploaded_at: string;
|
|
}
|
|
|
|
interface ApiContractStats {
|
|
total_count: number;
|
|
pending_count: number;
|
|
completed_count: number;
|
|
}
|
|
|
|
interface ApiContractStageCount {
|
|
estimate_selected: number;
|
|
estimate_progress: number;
|
|
delivery: number;
|
|
installation: number;
|
|
inspection: number;
|
|
other: number;
|
|
}
|
|
|
|
// ========================================
|
|
// 타입 변환 함수
|
|
// ========================================
|
|
|
|
/**
|
|
* API 응답 → Contract 타입 변환
|
|
*/
|
|
function transformContract(apiData: ApiContract): Contract {
|
|
return {
|
|
id: String(apiData.id),
|
|
contractCode: apiData.contract_code || '',
|
|
partnerId: apiData.partner_id ? String(apiData.partner_id) : '',
|
|
partnerName: apiData.partner_name || '',
|
|
projectName: apiData.project_name || '',
|
|
contractManagerId: apiData.contract_manager_id ? String(apiData.contract_manager_id) : '',
|
|
contractManagerName: apiData.contract_manager_name || '',
|
|
constructionPMId: apiData.construction_pm_id ? String(apiData.construction_pm_id) : '',
|
|
constructionPMName: apiData.construction_pm_name || '',
|
|
totalLocations: apiData.total_locations || 0,
|
|
contractAmount: apiData.contract_amount || 0,
|
|
contractStartDate: apiData.contract_start_date || null,
|
|
contractEndDate: apiData.contract_end_date || null,
|
|
status: apiData.status || 'pending',
|
|
stage: (apiData.stage as Contract['stage']) || 'estimate_selected',
|
|
remarks: apiData.remarks || '',
|
|
createdAt: apiData.created_at || '',
|
|
updatedAt: apiData.updated_at || '',
|
|
createdBy: apiData.created_by || '',
|
|
biddingId: apiData.bidding_id ? String(apiData.bidding_id) : '',
|
|
biddingCode: apiData.bidding_code || '',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* API 응답 → ContractDetail 타입 변환
|
|
*/
|
|
function transformContractDetail(apiData: ApiContract): ContractDetail {
|
|
const contract = transformContract(apiData);
|
|
|
|
return {
|
|
...contract,
|
|
contractFile: apiData.contract_file
|
|
? {
|
|
id: String(apiData.contract_file.id),
|
|
fileName: apiData.contract_file.file_name || '',
|
|
fileUrl: apiData.contract_file.file_url || '',
|
|
uploadedAt: apiData.contract_file.uploaded_at || '',
|
|
}
|
|
: null,
|
|
attachments: (apiData.attachments || []).map((att) => ({
|
|
id: String(att.id),
|
|
fileName: att.file_name || '',
|
|
fileSize: att.file_size || 0,
|
|
fileUrl: att.file_url || '',
|
|
uploadedAt: att.uploaded_at || '',
|
|
})),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ContractFormData → 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;
|
|
}
|
|
|
|
// ========================================
|
|
// API 함수
|
|
// ========================================
|
|
|
|
/**
|
|
* 계약 목록 조회
|
|
* GET /api/v1/construction/contracts
|
|
*/
|
|
export async function getContractList(filter?: ContractFilter): Promise<{
|
|
success: boolean;
|
|
data?: ContractListResponse;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const queryParams: Record<string, string> = {};
|
|
|
|
// 검색
|
|
if (filter?.search) queryParams.search = filter.search;
|
|
|
|
// 필터
|
|
if (filter?.status && filter.status !== 'all') queryParams.status = filter.status;
|
|
if (filter?.stage && filter.stage !== 'all') queryParams.stage = filter.stage;
|
|
if (filter?.partnerId && filter.partnerId !== 'all') queryParams.partner_id = filter.partnerId;
|
|
if (filter?.contractManagerId && filter.contractManagerId !== 'all') {
|
|
queryParams.contract_manager_id = filter.contractManagerId;
|
|
}
|
|
if (filter?.constructionPMId && filter.constructionPMId !== 'all') {
|
|
queryParams.construction_pm_id = filter.constructionPMId;
|
|
}
|
|
|
|
// 날짜 범위
|
|
if (filter?.startDate) queryParams.start_date = filter.startDate;
|
|
if (filter?.endDate) queryParams.end_date = filter.endDate;
|
|
|
|
// 페이지네이션
|
|
if (filter?.page) queryParams.page = String(filter.page);
|
|
if (filter?.size) queryParams.per_page = String(filter.size);
|
|
|
|
// 정렬
|
|
if (filter?.sortBy) {
|
|
const sortMap: Record<string, { field: string; dir: string }> = {
|
|
contractDateDesc: { field: 'created_at', dir: 'desc' },
|
|
contractDateAsc: { field: 'created_at', dir: 'asc' },
|
|
partnerNameAsc: { field: 'partner_name', dir: 'asc' },
|
|
partnerNameDesc: { field: 'partner_name', dir: 'desc' },
|
|
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) {
|
|
queryParams.sort_by = sort.field;
|
|
queryParams.sort_dir = sort.dir;
|
|
}
|
|
}
|
|
|
|
const response = await apiClient.get<{
|
|
success: boolean;
|
|
data: {
|
|
data: ApiContract[];
|
|
current_page: number;
|
|
per_page: number;
|
|
total: number;
|
|
last_page: number;
|
|
};
|
|
}>('/construction/contracts', { params: queryParams });
|
|
|
|
const paginatedData = response.data;
|
|
const items = (paginatedData.data || []).map(transformContract);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
items,
|
|
total: paginatedData.total || 0,
|
|
page: paginatedData.current_page || 1,
|
|
size: paginatedData.per_page || 20,
|
|
totalPages: paginatedData.last_page || 1,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('계약 목록 조회 오류:', error);
|
|
return { success: false, error: '계약 목록을 불러오는데 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계약 통계 조회
|
|
* GET /api/v1/construction/contracts/stats
|
|
*/
|
|
export async function getContractStats(): Promise<{
|
|
success: boolean;
|
|
data?: ContractStats;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const response = await apiClient.get<{
|
|
success: boolean;
|
|
data: ApiContractStats;
|
|
}>('/construction/contracts/stats');
|
|
|
|
const statsData = response.data;
|
|
return {
|
|
success: true,
|
|
data: {
|
|
total: statsData.total_count || 0,
|
|
pending: statsData.pending_count || 0,
|
|
completed: statsData.completed_count || 0,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('계약 통계 조회 오류:', error);
|
|
return { success: false, error: '통계를 불러오는데 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단계별 건수 조회
|
|
* GET /api/v1/construction/contracts/stage-counts
|
|
*/
|
|
export async function getContractStageCounts(): Promise<{
|
|
success: boolean;
|
|
data?: ContractStageCount;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const response = await apiClient.get<{
|
|
success: boolean;
|
|
data: ApiContractStageCount;
|
|
}>('/construction/contracts/stage-counts');
|
|
|
|
const stageData = response.data;
|
|
return {
|
|
success: true,
|
|
data: {
|
|
estimateSelected: stageData.estimate_selected || 0,
|
|
estimateProgress: stageData.estimate_progress || 0,
|
|
delivery: stageData.delivery || 0,
|
|
installation: stageData.installation || 0,
|
|
inspection: stageData.inspection || 0,
|
|
other: stageData.other || 0,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('단계별 건수 조회 오류:', error);
|
|
return { success: false, error: '단계별 건수를 불러오는데 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계약 단건 조회
|
|
* GET /api/v1/construction/contracts/{id}
|
|
*/
|
|
export async function getContract(id: string): Promise<{
|
|
success: boolean;
|
|
data?: Contract;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const response = await apiClient.get<{ success: boolean; data: ApiContract }>(
|
|
`/construction/contracts/${id}`
|
|
);
|
|
return { success: true, data: transformContract(response.data) };
|
|
} catch (error) {
|
|
console.error('계약 조회 오류:', error);
|
|
return { success: false, error: '계약 정보를 찾을 수 없습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계약 상세 조회 (첨부파일 포함)
|
|
* GET /api/v1/construction/contracts/{id}
|
|
*/
|
|
export async function getContractDetail(id: string): Promise<{
|
|
success: boolean;
|
|
data?: ContractDetail;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const response = await apiClient.get<{ success: boolean; data: ApiContract }>(
|
|
`/construction/contracts/${id}`
|
|
);
|
|
return { success: true, data: transformContractDetail(response.data) };
|
|
} catch (error) {
|
|
console.error('계약 상세 조회 오류:', error);
|
|
return { success: false, error: '계약 상세 정보를 불러오는데 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계약 등록
|
|
* POST /api/v1/construction/contracts
|
|
*/
|
|
export async function createContract(data: ContractFormData): Promise<{
|
|
success: boolean;
|
|
data?: Contract;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const apiData = transformToApiRequest(data);
|
|
const response = await apiClient.post<{ success: boolean; data: ApiContract }>(
|
|
'/construction/contracts',
|
|
apiData
|
|
);
|
|
return { success: true, data: transformContract(response.data) };
|
|
} catch (error) {
|
|
console.error('계약 등록 오류:', error);
|
|
return { success: false, error: '계약 등록에 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계약 수정
|
|
* PUT /api/v1/construction/contracts/{id}
|
|
*/
|
|
export async function updateContract(
|
|
id: string,
|
|
data: Partial<ContractFormData>
|
|
): Promise<{
|
|
success: boolean;
|
|
data?: Contract;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const apiData = transformToApiRequest(data);
|
|
const response = await apiClient.put<{ success: boolean; data: ApiContract }>(
|
|
`/construction/contracts/${id}`,
|
|
apiData
|
|
);
|
|
return { success: true, data: transformContract(response.data) };
|
|
} catch (error) {
|
|
console.error('계약 수정 오류:', error);
|
|
return { success: false, error: '계약 수정에 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계약 삭제
|
|
* DELETE /api/v1/construction/contracts/{id}
|
|
*/
|
|
export async function deleteContract(id: string): Promise<{
|
|
success: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
await apiClient.delete(`/construction/contracts/${id}`);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('계약 삭제 오류:', error);
|
|
return { success: false, error: '계약 삭제에 실패했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계약 일괄 삭제
|
|
* DELETE /api/v1/construction/contracts/bulk
|
|
*/
|
|
export async function deleteContracts(ids: string[]): Promise<{
|
|
success: boolean;
|
|
deletedCount?: number;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
await apiClient.delete('/construction/contracts/bulk', {
|
|
data: { ids: ids.map((id) => Number(id)) },
|
|
});
|
|
return { success: true, deletedCount: ids.length };
|
|
} catch (error) {
|
|
console.error('계약 일괄 삭제 오류:', error);
|
|
return { success: false, error: '일괄 삭제에 실패했습니다.' };
|
|
}
|
|
}
|