From b7b8b9039833204550a114a7623e969e081b5713 Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 9 Jan 2026 19:08:28 +0900 Subject: [PATCH] =?UTF-8?q?refactor(handover-report):=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20fetch=20=E2=86=92=20apiClient=20=ED=91=9C=EC=A4=80?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 커스텀 apiRequest 함수 제거 (52줄) - cookies() 직접 사용 제거 - @/lib/api의 apiClient 사용으로 통일 - 명시적 API 타입 정의 추가 - ApiHandoverReport, ApiManager, ApiContractItem - ApiExternalEquipmentCost, ApiHandoverReportStats - 코드량 499줄 → 452줄 (47줄 감소) --- CURRENT_WORKS.md | 36 ++ .../construction/handover-report/actions.ts | 495 ++++++++---------- 2 files changed, 260 insertions(+), 271 deletions(-) diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 4a7ff58b..dd82e882 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,41 @@ # SAM React 작업 현황 +## 2026-01-09 (목) - Phase 1.2 인수인계보고서 API 표준화 + +### 작업 목표 +- `handover-report/actions.ts` 커스텀 fetch → 표준 apiClient 변환 +- 기존 API 연동 코드를 프로젝트 표준 패턴으로 통일 + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/business/construction/handover-report/actions.ts` | 커스텀 apiRequest → apiClient 표준화 | + +### 주요 변경 내용 + +#### 1. 제거된 코드 +- 커스텀 `apiRequest()` 함수 (52줄) +- `cookies()` 직접 import +- `API_BASE_URL`, `API_KEY` 직접 정의 + +#### 2. 추가된 코드 +- `import { apiClient } from '@/lib/api'` +- 명시적 API 타입 정의: `ApiHandoverReport`, `ApiManager`, `ApiContractItem`, `ApiExternalEquipmentCost` + +#### 3. API 엔드포인트 (변경 없음) +- `GET /construction/handover-reports` - 목록 +- `GET /construction/handover-reports/stats` - 통계 +- `GET /construction/handover-reports/{id}` - 상세 +- `POST /construction/handover-reports` - 등록 +- `PUT /construction/handover-reports/{id}` - 수정 +- `DELETE /construction/handover-reports/{id}` - 삭제 +- `DELETE /construction/handover-reports/bulk` - 일괄 삭제 + +### 빌드 검증 +✅ Next.js 빌드 성공 (349 페이지) + +--- + ## 2026-01-09 (목) - Phase 2.4 수주관리 API 연동 ### 작업 목표 diff --git a/src/components/business/construction/handover-report/actions.ts b/src/components/business/construction/handover-report/actions.ts index 938f1ac0..92a0022f 100644 --- a/src/components/business/construction/handover-report/actions.ts +++ b/src/components/business/construction/handover-report/actions.ts @@ -1,6 +1,5 @@ 'use server'; -import { cookies } from 'next/headers'; import type { HandoverReport, HandoverReportDetail, @@ -10,125 +9,133 @@ import type { ContractItem, ExternalEquipmentCost, } from './types'; +import { apiClient } from '@/lib/api'; /** * 주일 기업 - 인수인계보고서관리 Server Actions - * API 연동 버전 + * 표준화된 apiClient 사용 버전 */ -// API 기본 URL -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://api.sam.kr'; -const API_KEY = process.env.API_KEY || ''; +// ======================================== +// API 응답 타입 +// ======================================== -/** - * API 요청 헬퍼 함수 - */ -async function apiRequest( - 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 = { - '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('🔵 [HandoverReport API]', options.method || 'GET', url); - - const response = await fetch(url, { - ...options, - headers: { - ...headers, - ...options.headers, - }, - }); - - const result = await response.json(); - console.log('🔵 [HandoverReport 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 : '알 수 없는 오류가 발생했습니다.', - }; - } +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; + pending_count: number; + completed_count: number; + total_amount?: number; + total_sites?: number; +} + +// ======================================== +// 타입 변환 함수 +// ======================================== + /** - * API 응답 → 프론트엔드 타입 변환 (목록용) + * API 응답 → HandoverReport 타입 변환 (목록용) */ -function transformHandoverReport(apiData: Record): HandoverReport { +function transformHandoverReport(apiData: ApiHandoverReport): HandoverReport { return { id: String(apiData.id), - reportNumber: String(apiData.report_number || ''), - partnerName: String(apiData.partner_name || ''), - siteName: String(apiData.site_name || ''), - contractManagerName: String(apiData.contract_manager_name || ''), - constructionPMName: apiData.construction_pm_name ? String(apiData.construction_pm_name) : null, - totalSites: Number(apiData.total_sites || 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', - contractId: String(apiData.contract_id || ''), - createdAt: String(apiData.created_at || ''), - updatedAt: String(apiData.updated_at || ''), + 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 응답 → 프론트엔드 타입 변환 (상세용) + * API 응답 → HandoverReportDetail 타입 변환 (상세용) */ -function transformHandoverReportDetail(apiData: Record): HandoverReportDetail { +function transformHandoverReportDetail(apiData: ApiHandoverReport): HandoverReportDetail { // 공사담당자 목록 변환 - const managersData = apiData.managers as Record[] | undefined; - const constructionManagers: ConstructionManager[] = (managersData || []).map((m) => ({ - id: String(m.id || ''), - name: String(m.name || ''), - nonPerformanceReason: String(m.non_performance_reason || ''), - signature: m.signature ? String(m.signature) : null, + 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 itemsData = apiData.items as Record[] | undefined; - const contractItems: ContractItem[] = (itemsData || []).map((item) => ({ - id: String(item.id || ''), - no: Number(item.item_no || item.no || 0), - name: String(item.name || ''), - product: String(item.product || ''), - quantity: Number(item.quantity || 0), - remark: String(item.remark || ''), + 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 externalCostData = apiData.external_equipment_cost as Record | undefined; - const externalEquipmentCost: ExternalEquipmentCost = externalCostData + const externalCost = apiData.external_equipment_cost; + const externalEquipmentCost: ExternalEquipmentCost = externalCost ? { - shippingCost: Number(externalCostData.shipping_cost || externalCostData.shippingCost || 0), - highAltitudeWork: Number(externalCostData.high_altitude_work || externalCostData.highAltitudeWork || 0), - publicExpense: Number(externalCostData.public_expense || externalCostData.publicExpense || 0), + shippingCost: externalCost.shipping_cost || 0, + highAltitudeWork: externalCost.high_altitude_work || 0, + publicExpense: externalCost.public_expense || 0, } : { shippingCost: 0, @@ -138,37 +145,37 @@ function transformHandoverReportDetail(apiData: Record): Handov return { id: String(apiData.id), - reportNumber: String(apiData.report_number || ''), - partnerName: String(apiData.partner_name || ''), - siteName: String(apiData.site_name || ''), - contractManagerName: String(apiData.contract_manager_name || ''), - constructionPMName: apiData.construction_pm_name ? String(apiData.construction_pm_name) : null, + 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: Number(apiData.total_sites || 0), - contractAmount: Number(apiData.contract_amount || 0), - contractDate: apiData.contract_date ? String(apiData.contract_date) : null, - contractStartDate: apiData.contract_start_date ? String(apiData.contract_start_date) : null, - contractEndDate: apiData.contract_end_date ? String(apiData.contract_end_date) : null, - completionDate: apiData.completion_date ? String(apiData.completion_date) : null, - status: (apiData.status as 'pending' | 'completed') || 'pending', - contractId: String(apiData.contract_id || ''), - createdAt: String(apiData.created_at || ''), - updatedAt: String(apiData.updated_at || ''), + 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: Boolean(apiData.has_secondary_piping), - secondaryPipingAmount: Number(apiData.secondary_piping_amount || 0), - secondaryPipingNote: String(apiData.secondary_piping_note || ''), - hasCoating: Boolean(apiData.has_coating), - coatingAmount: Number(apiData.coating_amount || 0), - coatingNote: String(apiData.coating_note || ''), + 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: String(apiData.special_notes || ''), + specialNotes: apiData.special_notes || '', }; } /** - * 프론트엔드 → API 요청 타입 변환 + * HandoverReportFormData → API 요청 데이터 변환 */ function transformToApiRequest(data: Partial): Record { const apiData: Record = {}; @@ -223,11 +230,15 @@ function transformToApiRequest(data: Partial): Record { +}> { try { - const queryParams = new URLSearchParams(); + const queryParams: Record = {}; - if (params.search) queryParams.append('search', params.search); - if (params.status && params.status !== 'all') queryParams.append('status', params.status); - if (params.partnerId && params.partnerId !== 'all') queryParams.append('partner_id', params.partnerId); - if (params.contractManagerId && params.contractManagerId !== 'all') queryParams.append('contract_manager_id', params.contractManagerId); - if (params.constructionPMId && params.constructionPMId !== 'all') queryParams.append('construction_pm_id', params.constructionPMId); - if (params.startDate) queryParams.append('start_date', params.startDate); - if (params.endDate) queryParams.append('end_date', params.endDate); - if (params.page) queryParams.append('page', String(params.page)); - if (params.size) queryParams.append('per_page', String(params.size)); + // 페이지네이션 + if (params?.page) queryParams.page = String(params.page); + if (params?.size) queryParams.per_page = String(params.size); - // 정렬 파라미터 변환 - if (params.sortBy) { + // 검색 + if (params?.search) queryParams.search = params.search; + + // 필터 + if (params?.status && params.status !== 'all') queryParams.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 = { contractDateDesc: { field: 'contract_start_date', dir: 'desc' }, contractDateAsc: { field: 'contract_start_date', dir: 'asc' }, @@ -283,217 +296,157 @@ export async function getHandoverReportList( }; const sort = sortMap[params.sortBy]; if (sort) { - queryParams.append('sort_by', sort.field); - queryParams.append('sort_dir', sort.dir); + queryParams.sort_by = sort.field; + queryParams.sort_dir = sort.dir; } } - const queryString = queryParams.toString(); - const endpoint = `/construction/handover-reports${queryString ? `?${queryString}` : ''}`; - - const result = await apiRequest<{ - data: Record[]; + const response = await apiClient.get<{ + data: ApiHandoverReport[]; current_page: number; per_page: number; total: number; last_page: number; - }>(endpoint); + }>('/construction/handover-reports', { params: queryParams }); - if (!result.success || !result.data) { - return { success: false, error: result.error || '인수인계보고서 목록 조회에 실패했습니다.' }; - } - - const apiData = result.data; - const items = (apiData.data || []).map(transformHandoverReport); + const items = (response.data || []).map(transformHandoverReport); return { success: true, data: { items, - total: apiData.total || 0, - page: apiData.current_page || 1, - size: apiData.per_page || 20, - totalPages: apiData.last_page || 1, + total: response.total || 0, + page: response.current_page || 1, + size: response.per_page || 20, + totalPages: response.last_page || 1, }, }; } catch (error) { - console.error('getHandoverReportList error:', error); + console.error('인수인계보고서 목록 조회 오류:', error); return { success: false, error: '인수인계보고서 목록을 불러오는데 실패했습니다.' }; } } -interface GetHandoverReportStatsResult { +/** + * 인수인계보고서 통계 조회 + * GET /api/v1/construction/handover-reports/stats + */ +export async function getHandoverReportStats(): Promise<{ success: boolean; data?: HandoverReportStats; error?: string; -} - -/** - * 인수인계보고서 통계 조회 - */ -export async function getHandoverReportStats(): Promise { +}> { try { - const result = await apiRequest<{ - total_count: number; - pending_count: number; - completed_count: number; - total_amount?: number; - total_sites?: number; - }>('/construction/handover-reports/stats'); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '통계를 불러오는데 실패했습니다.' }; - } + const response = await apiClient.get('/construction/handover-reports/stats'); return { success: true, data: { - total: result.data.total_count || 0, - pending: result.data.pending_count || 0, - completed: result.data.completed_count || 0, + total: response.total_count || 0, + pending: response.pending_count || 0, + completed: response.completed_count || 0, }, }; } catch (error) { - console.error('getHandoverReportStats error:', error); + console.error('인수인계보고서 통계 조회 오류:', error); return { success: false, error: '통계를 불러오는데 실패했습니다.' }; } } -interface DeleteHandoverReportResult { - success: boolean; - error?: string; -} - /** * 인수인계보고서 삭제 + * DELETE /api/v1/construction/handover-reports/{id} */ -export async function deleteHandoverReport(id: string): Promise { +export async function deleteHandoverReport(id: string): Promise<{ + success: boolean; + error?: string; +}> { try { - const result = await apiRequest(`/construction/handover-reports/${id}`, { - method: 'DELETE', - }); - - if (!result.success) { - return { success: false, error: result.error || '삭제에 실패했습니다.' }; - } - + await apiClient.delete(`/construction/handover-reports/${id}`); return { success: true }; } catch (error) { - console.error('deleteHandoverReport error:', error); + console.error('인수인계보고서 삭제 오류:', error); return { success: false, error: '삭제에 실패했습니다.' }; } } -interface DeleteHandoverReportsResult { +/** + * 인수인계보고서 일괄 삭제 + * DELETE /api/v1/construction/handover-reports/bulk + */ +export async function deleteHandoverReports(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; -} - -/** - * 인수인계보고서 일괄 삭제 - */ -export async function deleteHandoverReports(ids: string[]): Promise { +}> { try { - const result = await apiRequest('/construction/handover-reports/bulk', { - method: 'DELETE', - body: JSON.stringify({ ids: ids.map((id) => Number(id)) }), + await apiClient.delete('/construction/handover-reports/bulk', { + data: { ids: ids.map((id) => Number(id)) }, }); - - if (!result.success) { - return { success: false, error: result.error || '일괄 삭제에 실패했습니다.' }; - } - return { success: true, deletedCount: ids.length }; } catch (error) { - console.error('deleteHandoverReports error:', error); + console.error('인수인계보고서 일괄 삭제 오류:', error); return { success: false, error: '일괄 삭제에 실패했습니다.' }; } } -interface GetHandoverReportDetailResult { - success: boolean; - data?: HandoverReportDetail; - error?: string; -} - /** * 인수인계보고서 상세 조회 + * GET /api/v1/construction/handover-reports/{id} */ -export async function getHandoverReportDetail(id: string): Promise { - try { - const result = await apiRequest>(`/construction/handover-reports/${id}`); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '인수인계보고서를 찾을 수 없습니다.' }; - } - - return { success: true, data: transformHandoverReportDetail(result.data) }; - } catch (error) { - console.error('getHandoverReportDetail error:', error); - return { success: false, error: '인수인계보고서 상세 정보를 불러오는데 실패했습니다.' }; - } -} - -interface UpdateHandoverReportResult { +export async function getHandoverReportDetail(id: string): Promise<{ success: boolean; data?: HandoverReportDetail; error?: string; +}> { + try { + const response = await apiClient.get(`/construction/handover-reports/${id}`); + return { success: true, data: transformHandoverReportDetail(response) }; + } 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 { +): Promise<{ + success: boolean; + data?: HandoverReportDetail; + error?: string; +}> { try { const apiData = transformToApiRequest(data); - - const result = await apiRequest>(`/construction/handover-reports/${id}`, { - method: 'PUT', - body: JSON.stringify(apiData), - }); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '수정에 실패했습니다.' }; - } - - return { success: true, data: transformHandoverReportDetail(result.data) }; + const response = await apiClient.put(`/construction/handover-reports/${id}`, apiData); + return { success: true, data: transformHandoverReportDetail(response) }; } catch (error) { - console.error('updateHandoverReport error:', error); + console.error('인수인계보고서 수정 오류:', error); return { success: false, error: '수정에 실패했습니다.' }; } } -interface CreateHandoverReportResult { - success: boolean; - data?: HandoverReportDetail; - error?: string; -} - /** * 인수인계보고서 등록 + * POST /api/v1/construction/handover-reports */ export async function createHandoverReport( data: HandoverReportFormData -): Promise { +): Promise<{ + success: boolean; + data?: HandoverReportDetail; + error?: string; +}> { try { const apiData = transformToApiRequest(data); - - const result = await apiRequest>('/construction/handover-reports', { - method: 'POST', - body: JSON.stringify(apiData), - }); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '등록에 실패했습니다.' }; - } - - return { success: true, data: transformHandoverReportDetail(result.data) }; + const response = await apiClient.post('/construction/handover-reports', apiData); + return { success: true, data: transformHandoverReportDetail(response) }; } catch (error) { - console.error('createHandoverReport error:', error); + console.error('인수인계보고서 등록 오류:', error); return { success: false, error: '등록에 실패했습니다.' }; } } \ No newline at end of file