Files
sam-react-prod/src/components/business/construction/handover-report/actions.ts
권혁성 d12e2e0b4c fix: API 응답 래퍼 패턴 수정 및 기능 개선
- construction actions.ts 파일들 API 응답 래퍼 패턴 수정
  - handover-report, order-management, site-management, structure-review
  - apiClient 반환값 { success, data } 구조에 맞게 수정
- ShipmentManagement 기능 개선
- WorkerScreen 컴포넌트 수정
- .gitignore에 package-lock.json, tsconfig.tsbuildinfo 추가
2026-01-20 17:04:32 +09:00

644 lines
22 KiB
TypeScript

'use server';
import type {
HandoverReport,
HandoverReportDetail,
HandoverReportStats,
HandoverReportFormData,
ConstructionManager,
ContractItem,
ExternalEquipmentCost,
} from './types';
import { apiClient } from '@/lib/api';
/**
* 주일 기업 - 인수인계보고서관리 Server Actions
* 표준화된 apiClient 사용 버전
*/
// ========================================
// API 응답 타입
// ========================================
// 목록 조회 시: Contract 데이터 + handover_report 관계
interface ApiContractWithHandover {
id: number;
contract_code: string;
project_name: string;
partner_id: number | null;
partner_name: string | null;
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: string | number;
contract_start_date: string | null;
contract_end_date: string | null;
status: 'pending' | 'completed';
stage: string;
bidding_id: number | null;
bidding_code: string | null;
remarks: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
// 인수인계 보고서 관계 (없으면 null)
handover_report: ApiHandoverReportRelation | null;
}
// 인수인계 보고서 관계 데이터
interface ApiHandoverReportRelation {
id: number;
report_number: string;
status: 'pending' | 'completed';
completion_date: string | null;
}
// 상세 조회 시: HandoverReport 직접 반환
interface ApiHandoverReport {
id: number;
report_number: string;
partner_name: string | null;
site_name: string;
contract_manager_name: string | null;
construction_pm_name: string | null;
construction_pm_id: number | null;
total_sites: number;
contract_amount: number;
contract_date: string | null;
contract_start_date: string | null;
contract_end_date: string | null;
completion_date: string | null;
status: 'pending' | 'completed';
contract_id: number | null;
created_at: string;
updated_at: string;
// 상세 조회 시 포함
managers?: ApiManager[];
items?: ApiContractItem[];
has_secondary_piping?: boolean;
secondary_piping_amount?: number;
secondary_piping_note?: string | null;
has_coating?: boolean;
coating_amount?: number;
coating_note?: string | null;
external_equipment_cost?: ApiExternalEquipmentCost;
special_notes?: string | null;
}
interface ApiManager {
id: number;
name: string;
non_performance_reason: string | null;
signature: string | null;
}
interface ApiContractItem {
id: number;
item_no: number;
name: string;
product: string | null;
quantity: number;
remark: string | null;
}
interface ApiExternalEquipmentCost {
shipping_cost: number;
high_altitude_work: number;
public_expense: number;
}
// 통계 응답 (계약 완료건 기준)
interface ApiHandoverReportStats {
total_count: number;
handover_completed_count: number;
handover_pending_count: number;
total_amount?: number;
total_locations?: number;
}
// 상세 조회 시: Contract + handover_report 관계 (API show 응답)
interface ApiContractWithHandoverDetail extends ApiContractWithHandover {
contract_manager?: {
id: number;
name: string;
} | null;
construction_pm?: {
id: number;
name: string;
} | null;
handover_report: {
id: number;
report_number: string;
status: 'pending' | 'completed';
completion_date: string | null;
contract_date: string | null;
has_secondary_piping?: boolean;
secondary_piping_amount?: number;
secondary_piping_note?: string | null;
has_coating?: boolean;
coating_amount?: number;
coating_note?: string | null;
external_equipment_cost?: ApiExternalEquipmentCost;
special_notes?: string | null;
managers?: ApiManager[];
items?: ApiContractItem[];
} | null;
}
// ========================================
// 타입 변환 함수
// ========================================
/**
* Contract + handover_report 관계 → HandoverReport 타입 변환 (목록용)
* 계약 완료건 기준 목록 조회용
*/
function transformContractToHandoverReport(apiData: ApiContractWithHandover): HandoverReport {
// 인수인계 상태: 보고서가 있으면 completed, 없으면 pending
const handoverStatus = apiData.handover_report ? 'completed' : 'pending';
return {
id: String(apiData.id),
// 보고서 번호: 보고서가 있으면 보고서 번호, 없으면 계약 코드
reportNumber: apiData.handover_report?.report_number || apiData.contract_code || '',
partnerName: apiData.partner_name || '',
// 현장명 = 프로젝트명
siteName: apiData.project_name || '',
contractManagerName: apiData.contract_manager_name || '',
constructionPMName: apiData.construction_pm_name || null,
// 총 개소 = total_locations
totalSites: apiData.total_locations || 0,
contractAmount: Number(apiData.contract_amount) || 0,
contractStartDate: apiData.contract_start_date || null,
contractEndDate: apiData.contract_end_date || null,
// 인수인계 상태 (보고서 유무 기준)
status: handoverStatus,
contractId: String(apiData.id),
createdAt: apiData.created_at || '',
updatedAt: apiData.updated_at || '',
};
}
/**
* API 응답 → HandoverReport 타입 변환 (상세용 - 기존 유지)
*/
function transformHandoverReport(apiData: ApiHandoverReport): HandoverReport {
return {
id: String(apiData.id),
reportNumber: apiData.report_number || '',
partnerName: apiData.partner_name || '',
siteName: apiData.site_name || '',
contractManagerName: apiData.contract_manager_name || '',
constructionPMName: apiData.construction_pm_name || null,
totalSites: apiData.total_sites || 0,
contractAmount: apiData.contract_amount || 0,
contractStartDate: apiData.contract_start_date || null,
contractEndDate: apiData.contract_end_date || null,
status: apiData.status || 'pending',
contractId: apiData.contract_id ? String(apiData.contract_id) : '',
createdAt: apiData.created_at || '',
updatedAt: apiData.updated_at || '',
};
}
/**
* API 응답 → HandoverReportDetail 타입 변환 (상세용 - 기존 HandoverReport 직접 조회용)
*/
function transformHandoverReportDetail(apiData: ApiHandoverReport): HandoverReportDetail {
// 공사담당자 목록 변환
const constructionManagers: ConstructionManager[] = (apiData.managers || []).map((m) => ({
id: String(m.id),
name: m.name || '',
nonPerformanceReason: m.non_performance_reason || '',
signature: m.signature || null,
}));
// 계약 ITEM 목록 변환
const contractItems: ContractItem[] = (apiData.items || []).map((item) => ({
id: String(item.id),
no: item.item_no || 0,
name: item.name || '',
product: item.product || '',
quantity: item.quantity || 0,
remark: item.remark || '',
}));
// 장비 외 실행금액 변환
const externalCost = apiData.external_equipment_cost;
const externalEquipmentCost: ExternalEquipmentCost = externalCost
? {
shippingCost: externalCost.shipping_cost || 0,
highAltitudeWork: externalCost.high_altitude_work || 0,
publicExpense: externalCost.public_expense || 0,
}
: {
shippingCost: 0,
highAltitudeWork: 0,
publicExpense: 0,
};
return {
id: String(apiData.id),
reportNumber: apiData.report_number || '',
partnerName: apiData.partner_name || '',
siteName: apiData.site_name || '',
contractManagerName: apiData.contract_manager_name || '',
constructionPMName: apiData.construction_pm_name || null,
constructionPMId: apiData.construction_pm_id ? String(apiData.construction_pm_id) : null,
totalSites: apiData.total_sites || 0,
contractAmount: apiData.contract_amount || 0,
contractDate: apiData.contract_date || null,
contractStartDate: apiData.contract_start_date || null,
contractEndDate: apiData.contract_end_date || null,
completionDate: apiData.completion_date || null,
status: apiData.status || 'pending',
contractId: apiData.contract_id ? String(apiData.contract_id) : '',
createdAt: apiData.created_at || '',
updatedAt: apiData.updated_at || '',
constructionManagers,
contractItems,
hasSecondaryPiping: apiData.has_secondary_piping || false,
secondaryPipingAmount: apiData.secondary_piping_amount || 0,
secondaryPipingNote: apiData.secondary_piping_note || '',
hasCoating: apiData.has_coating || false,
coatingAmount: apiData.coating_amount || 0,
coatingNote: apiData.coating_note || '',
externalEquipmentCost,
specialNotes: apiData.special_notes || '',
};
}
/**
* Contract + handover_report → HandoverReportDetail 타입 변환 (상세용 - Contract 기반 조회)
*/
function transformContractToHandoverReportDetail(apiData: ApiContractWithHandoverDetail): HandoverReportDetail {
const report = apiData.handover_report;
// 공사담당자 목록 변환
const constructionManagers: ConstructionManager[] = (report?.managers || []).map((m) => ({
id: String(m.id),
name: m.name || '',
nonPerformanceReason: m.non_performance_reason || '',
signature: m.signature || null,
}));
// 계약 ITEM 목록 변환
const contractItems: ContractItem[] = (report?.items || []).map((item) => ({
id: String(item.id),
no: item.item_no || 0,
name: item.name || '',
product: item.product || '',
quantity: item.quantity || 0,
remark: item.remark || '',
}));
// 장비 외 실행금액 변환
const externalCost = report?.external_equipment_cost;
const externalEquipmentCost: ExternalEquipmentCost = externalCost
? {
shippingCost: externalCost.shipping_cost || 0,
highAltitudeWork: externalCost.high_altitude_work || 0,
publicExpense: externalCost.public_expense || 0,
}
: {
shippingCost: 0,
highAltitudeWork: 0,
publicExpense: 0,
};
// 인수인계 상태: 보고서가 있으면 completed, 없으면 pending
const handoverStatus = report ? (report.status || 'pending') : 'pending';
return {
// Contract 기반 ID (상세 조회에서는 contract_id를 사용)
id: String(apiData.id),
// 보고서 번호: 보고서가 있으면 보고서 번호, 없으면 계약 코드
reportNumber: report?.report_number || apiData.contract_code || '',
partnerName: apiData.partner_name || '',
// 현장명 = 프로젝트명
siteName: apiData.project_name || '',
contractManagerName: apiData.contract_manager_name || apiData.contract_manager?.name || '',
constructionPMName: apiData.construction_pm_name || apiData.construction_pm?.name || null,
constructionPMId: apiData.construction_pm_id ? String(apiData.construction_pm_id) : null,
// 총 개소 = total_locations
totalSites: apiData.total_locations || 0,
contractAmount: Number(apiData.contract_amount) || 0,
contractDate: report?.contract_date || null,
contractStartDate: apiData.contract_start_date || null,
contractEndDate: apiData.contract_end_date || null,
completionDate: report?.completion_date || null,
status: handoverStatus,
contractId: String(apiData.id),
createdAt: apiData.created_at || '',
updatedAt: apiData.updated_at || '',
constructionManagers,
contractItems,
hasSecondaryPiping: report?.has_secondary_piping || false,
secondaryPipingAmount: report?.secondary_piping_amount || 0,
secondaryPipingNote: report?.secondary_piping_note || '',
hasCoating: report?.has_coating || false,
coatingAmount: report?.coating_amount || 0,
coatingNote: report?.coating_note || '',
externalEquipmentCost,
specialNotes: report?.special_notes || '',
};
}
/**
* HandoverReportFormData → API 요청 데이터 변환
*/
function transformToApiRequest(data: Partial<HandoverReportFormData>): Record<string, unknown> {
const apiData: Record<string, unknown> = {};
if (data.reportNumber !== undefined) apiData.report_number = data.reportNumber;
if (data.partnerName !== undefined) apiData.partner_name = data.partnerName || null;
if (data.siteName !== undefined) apiData.site_name = data.siteName;
if (data.contractManagerName !== undefined) apiData.contract_manager_name = data.contractManagerName || null;
if (data.contractDate !== undefined) apiData.contract_date = data.contractDate || null;
if (data.totalSites !== undefined) apiData.total_sites = data.totalSites;
if (data.contractStartDate !== undefined) apiData.contract_start_date = data.contractStartDate || null;
if (data.contractEndDate !== undefined) apiData.contract_end_date = data.contractEndDate || null;
if (data.contractAmount !== undefined) apiData.contract_amount = data.contractAmount;
if (data.constructionPMId !== undefined) apiData.construction_pm_id = data.constructionPMId || null;
if (data.constructionPMName !== undefined) apiData.construction_pm_name = data.constructionPMName || null;
if (data.status !== undefined) apiData.status = data.status;
if (data.hasSecondaryPiping !== undefined) apiData.has_secondary_piping = data.hasSecondaryPiping;
if (data.secondaryPipingNote !== undefined) apiData.secondary_piping_note = data.secondaryPipingNote || null;
if (data.hasCoating !== undefined) apiData.has_coating = data.hasCoating;
if (data.coatingNote !== undefined) apiData.coating_note = data.coatingNote || null;
if (data.specialNotes !== undefined) apiData.special_notes = data.specialNotes || null;
// 장비 외 실행금액 변환
if (data.externalEquipmentCost !== undefined) {
apiData.external_equipment_cost = {
shipping_cost: data.externalEquipmentCost.shippingCost,
high_altitude_work: data.externalEquipmentCost.highAltitudeWork,
public_expense: data.externalEquipmentCost.publicExpense,
};
}
// 공사담당자 변환
if (data.constructionManagers !== undefined) {
apiData.managers = data.constructionManagers.map((m) => ({
name: m.name,
non_performance_reason: m.nonPerformanceReason || null,
signature: m.signature || null,
}));
}
// 계약 ITEM 변환
if (data.contractItems !== undefined) {
apiData.items = data.contractItems.map((item, index) => ({
item_no: item.no || index + 1,
name: item.name,
product: item.product || null,
quantity: item.quantity,
remark: item.remark || null,
}));
}
return apiData;
}
// ========================================
// API 함수
// ========================================
/**
* 인수인계보고서 목록 조회
* GET /api/v1/construction/handover-reports
*/
export async function getHandoverReportList(params?: {
size?: number;
page?: number;
startDate?: string;
endDate?: string;
search?: string;
status?: string;
partnerId?: string;
contractManagerId?: string;
constructionPMId?: string;
sortBy?: string;
}): Promise<{
success: boolean;
data?: {
items: HandoverReport[];
total: number;
page: number;
size: number;
totalPages: number;
};
error?: string;
}> {
try {
const queryParams: Record<string, string> = {};
// 페이지네이션
if (params?.page) queryParams.page = String(params.page);
if (params?.size) queryParams.per_page = String(params.size);
// 검색
if (params?.search) queryParams.search = params.search;
// 인수인계 상태 필터 (API는 handover_status 파라미터 사용)
if (params?.status && params.status !== 'all') queryParams.handover_status = params.status;
if (params?.partnerId && params.partnerId !== 'all') queryParams.partner_id = params.partnerId;
if (params?.contractManagerId && params.contractManagerId !== 'all') {
queryParams.contract_manager_id = params.contractManagerId;
}
if (params?.constructionPMId && params.constructionPMId !== 'all') {
queryParams.construction_pm_id = params.constructionPMId;
}
// 날짜 범위
if (params?.startDate) queryParams.start_date = params.startDate;
if (params?.endDate) queryParams.end_date = params.endDate;
// 정렬
if (params?.sortBy) {
const sortMap: Record<string, { field: string; dir: string }> = {
contractDateDesc: { field: 'contract_start_date', dir: 'desc' },
contractDateAsc: { field: 'contract_start_date', dir: 'asc' },
partnerNameAsc: { field: 'partner_name', dir: 'asc' },
partnerNameDesc: { field: 'partner_name', dir: 'desc' },
siteNameAsc: { field: 'site_name', dir: 'asc' },
siteNameDesc: { field: 'site_name', dir: 'desc' },
};
const sort = sortMap[params.sortBy];
if (sort) {
queryParams.sort_by = sort.field;
queryParams.sort_dir = sort.dir;
}
}
// API 응답: Contract 목록 + handover_report 관계
const response = await apiClient.get<{
success: boolean;
data: {
data: ApiContractWithHandover[];
current_page: number;
per_page: number;
total: number;
last_page: number;
};
}>('/construction/handover-reports', { params: queryParams });
// Contract → HandoverReport 변환
const paginatedData = response.data;
const items = (paginatedData.data || []).map(transformContractToHandoverReport);
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/handover-reports/stats
*/
export async function getHandoverReportStats(): Promise<{
success: boolean;
data?: HandoverReportStats;
error?: string;
}> {
try {
const response = await apiClient.get<{
success: boolean;
data: ApiHandoverReportStats;
}>('/construction/handover-reports/stats');
const statsData = response.data;
return {
success: true,
data: {
total: statsData.total_count || 0,
pending: statsData.handover_pending_count || 0,
completed: statsData.handover_completed_count || 0,
},
};
} catch (error) {
console.error('인수인계보고서 통계 조회 오류:', error);
return { success: false, error: '통계를 불러오는데 실패했습니다.' };
}
}
/**
* 인수인계보고서 삭제
* DELETE /api/v1/construction/handover-reports/{id}
*/
export async function deleteHandoverReport(id: string): Promise<{
success: boolean;
error?: string;
}> {
try {
await apiClient.delete(`/construction/handover-reports/${id}`);
return { success: true };
} catch (error) {
console.error('인수인계보고서 삭제 오류:', error);
return { success: false, error: '삭제에 실패했습니다.' };
}
}
/**
* 인수인계보고서 일괄 삭제
* DELETE /api/v1/construction/handover-reports/bulk
*/
export async function deleteHandoverReports(ids: string[]): Promise<{
success: boolean;
deletedCount?: number;
error?: string;
}> {
try {
await apiClient.delete('/construction/handover-reports/bulk', {
data: { ids: ids.map((id) => Number(id)) },
});
return { success: true, deletedCount: ids.length };
} catch (error) {
console.error('인수인계보고서 일괄 삭제 오류:', error);
return { success: false, error: '일괄 삭제에 실패했습니다.' };
}
}
/**
* 인수인계보고서 상세 조회 (계약 ID 기준)
* GET /api/v1/construction/handover-reports/{contractId}
* 백엔드는 Contract + handover_report 관계로 반환
*/
export async function getHandoverReportDetail(id: string): Promise<{
success: boolean;
data?: HandoverReportDetail;
error?: string;
}> {
try {
const response = await apiClient.get<{
success: boolean;
data: ApiContractWithHandoverDetail;
}>(`/construction/handover-reports/${id}`);
return { success: true, data: transformContractToHandoverReportDetail(response.data) };
} catch (error) {
console.error('인수인계보고서 상세 조회 오류:', error);
return { success: false, error: '인수인계보고서를 찾을 수 없습니다.' };
}
}
/**
* 인수인계보고서 수정
* PUT /api/v1/construction/handover-reports/{id}
*/
export async function updateHandoverReport(
id: string,
data: HandoverReportFormData
): Promise<{
success: boolean;
data?: HandoverReportDetail;
error?: string;
}> {
try {
const apiData = transformToApiRequest(data);
const response = await apiClient.put<{
success: boolean;
data: ApiHandoverReport;
}>(`/construction/handover-reports/${id}`, apiData);
return { success: true, data: transformHandoverReportDetail(response.data) };
} catch (error) {
console.error('인수인계보고서 수정 오류:', error);
return { success: false, error: '수정에 실패했습니다.' };
}
}
/**
* 인수인계보고서 등록
* POST /api/v1/construction/handover-reports
*/
export async function createHandoverReport(
data: HandoverReportFormData
): Promise<{
success: boolean;
data?: HandoverReportDetail;
error?: string;
}> {
try {
const apiData = transformToApiRequest(data);
const response = await apiClient.post<{
success: boolean;
data: ApiHandoverReport;
}>('/construction/handover-reports', apiData);
return { success: true, data: transformHandoverReportDetail(response.data) };
} catch (error) {
console.error('인수인계보고서 등록 오류:', error);
return { success: false, error: '등록에 실패했습니다.' };
}
}