From 5db6e59bbcc8159f445b1261dca3e5bb696b02d0 Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 9 Jan 2026 19:21:34 +0900 Subject: [PATCH] =?UTF-8?q?refactor(construction):=20=EA=B1=B4=EC=84=A4?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=203=EA=B0=9C=20=EB=AA=A8=EB=93=88=20apiClien?= =?UTF-8?q?t=20=ED=91=9C=EC=A4=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - contract/actions.ts: 커스텀 apiRequest → apiClient 변환 - partners/actions.ts: 커스텀 apiRequest → apiClient 변환 - site-management/actions.ts: 커스텀 apiRequest → apiClient 변환 공통 변경사항: - cookies() 직접 import 제거 - API_BASE_URL, API_KEY 상수 제거 - import { apiClient } from '@/lib/api' 사용 - 명시적 API 타입 정의 추가 (ApiContract, ApiPartner, ApiSite 등) --- CURRENT_WORKS.md | 65 +++ .../business/construction/contract/actions.ts | 423 ++++++++---------- .../business/construction/partners/actions.ts | 392 +++++++--------- .../construction/site-management/actions.ts | 275 +++++------- 4 files changed, 532 insertions(+), 623 deletions(-) diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index dd82e882..41ba9ca3 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,67 @@ # SAM React 작업 현황 +## 2026-01-09 (목) - Phase 1.3-1.5 건설관리 apiClient 표준화 + +### 작업 목표 +- 건설관리 모듈의 커스텀 `apiRequest` 함수를 표준 `apiClient` 패턴으로 변환 +- Phase 1.3: 계약관리(contract), Phase 1.4: 거래처관리(partners), Phase 1.5: 현장관리(site-management) + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `src/components/business/construction/contract/actions.ts` | 커스텀 apiRequest → apiClient 표준화 | +| `src/components/business/construction/partners/actions.ts` | 커스텀 apiRequest → apiClient 표준화 | +| `src/components/business/construction/site-management/actions.ts` | 커스텀 apiRequest → apiClient 표준화 | + +### 주요 변경 내용 + +#### 1. 제거된 코드 (각 파일에서) +- 커스텀 `apiRequest()` 함수 전체 +- `import { cookies } from 'next/headers'` +- `const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL` +- `const API_KEY = process.env.API_KEY` + +#### 2. 추가된 코드 +- `import { apiClient } from '@/lib/api'` +- 명시적 API 타입 정의: + - **contract**: `ApiContract`, `ApiContractFile`, `ApiAttachment`, `ApiContractStats`, `ApiContractStageCount` + - **partners**: `ApiPartner`, `ApiPartnerStats` + - **site-management**: `ApiSite`, `ApiSiteStats` + +#### 3. API 엔드포인트 (변경 없음) +**계약관리 (contract)** +- `GET /construction/contracts` - 목록 +- `GET /construction/contracts/stats` - 통계 +- `GET /construction/contracts/stage-counts` - 단계별 건수 +- `GET /construction/contracts/{id}` - 상세 +- `POST /construction/contracts` - 등록 +- `PUT /construction/contracts/{id}` - 수정 +- `DELETE /construction/contracts/{id}` - 삭제 +- `DELETE /construction/contracts/bulk` - 일괄 삭제 + +**거래처관리 (partners)** +- `GET /clients` - 목록 +- `GET /clients/stats` - 통계 +- `GET /clients/{id}` - 상세 +- `POST /clients` - 등록 +- `PUT /clients/{id}` - 수정 +- `DELETE /clients/{id}` - 삭제 +- `DELETE /clients/bulk` - 일괄 삭제 + +**현장관리 (site-management)** +- `GET /sites` - 목록 +- `GET /sites/stats` - 통계 +- `DELETE /sites/{id}` - 삭제 +- `DELETE /sites/bulk` - 일괄 삭제 + +### 빌드 검증 +✅ Next.js 빌드 성공 (349 페이지) + +### Git 커밋 +- 대기 중 + +--- + ## 2026-01-09 (목) - Phase 1.2 인수인계보고서 API 표준화 ### 작업 목표 @@ -34,6 +96,9 @@ ### 빌드 검증 ✅ Next.js 빌드 성공 (349 페이지) +### Git 커밋 +- React: `b7b8b90` refactor(handover-report): 커스텀 fetch → apiClient 표준화 + --- ## 2026-01-09 (목) - Phase 2.4 수주관리 API 연동 diff --git a/src/components/business/construction/contract/actions.ts b/src/components/business/construction/contract/actions.ts index a7229f94..dd22c65a 100644 --- a/src/components/business/construction/contract/actions.ts +++ b/src/components/business/construction/contract/actions.ts @@ -1,6 +1,5 @@ 'use server'; -import { cookies } from 'next/headers'; import type { Contract, ContractDetail, @@ -10,103 +9,134 @@ import type { ContractFilter, ContractFormData, } 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('🔵 [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 : '알 수 없는 오류가 발생했습니다.', - }; - } +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 응답 → 프론트엔드 타입 변환 + * API 응답 → Contract 타입 변환 */ -function transformContract(apiData: Record): Contract { +function transformContract(apiData: ApiContract): 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', + 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: 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 || ''), + 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 요청 타입 변환 + * 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): Record { const apiData: Record = {}; @@ -127,8 +157,13 @@ function transformToApiRequest(data: Partial): Record { try { - const params = new URLSearchParams(); + const queryParams: Record = {}; - 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) 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 = { contractDateDesc: { field: 'created_at', dir: 'desc' }, @@ -163,47 +209,40 @@ export async function getContractList(filter?: ContractFilter): Promise<{ }; const sort = sortMap[filter.sortBy]; if (sort) { - params.append('sort_by', sort.field); - params.append('sort_dir', sort.dir); + queryParams.sort_by = sort.field; + queryParams.sort_dir = sort.dir; } } - const queryString = params.toString(); - const endpoint = `/construction/contracts${queryString ? `?${queryString}` : ''}`; - - const result = await apiRequest<{ - data: Record[]; + const response = await apiClient.get<{ + data: ApiContract[]; current_page: number; per_page: number; total: number; last_page: number; - }>(endpoint); + }>('/construction/contracts', { params: queryParams }); - if (!result.success || !result.data) { - return { success: false, error: result.error || '계약 목록 조회에 실패했습니다.' }; - } - - const apiData = result.data; - const items = (apiData.data || []).map(transformContract); + const items = (response.data || []).map(transformContract); 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('getContractList error:', error); + console.error('계약 목록 조회 오류:', error); return { success: false, error: '계약 목록을 불러오는데 실패했습니다.' }; } } /** * 계약 통계 조회 + * GET /api/v1/construction/contracts/stats */ export async function getContractStats(): Promise<{ success: boolean; @@ -211,32 +250,25 @@ export async function getContractStats(): Promise<{ error?: string; }> { try { - const result = await apiRequest<{ - total_count: number; - pending_count: number; - completed_count: number; - }>('/construction/contracts/stats'); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '통계를 불러오는데 실패했습니다.' }; - } + const response = await apiClient.get('/construction/contracts/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('getContractStats error:', error); + console.error('계약 통계 조회 오류:', error); return { success: false, error: '통계를 불러오는데 실패했습니다.' }; } } /** * 단계별 건수 조회 + * GET /api/v1/construction/contracts/stage-counts */ export async function getContractStageCounts(): Promise<{ success: boolean; @@ -244,38 +276,28 @@ export async function getContractStageCounts(): Promise<{ error?: string; }> { try { - const result = await apiRequest<{ - estimate_selected: number; - estimate_progress: number; - delivery: number; - installation: number; - inspection: number; - other: number; - }>('/construction/contracts/stage-counts'); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '단계별 건수를 불러오는데 실패했습니다.' }; - } + const response = await apiClient.get('/construction/contracts/stage-counts'); return { success: true, 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, + estimateSelected: response.estimate_selected || 0, + estimateProgress: response.estimate_progress || 0, + delivery: response.delivery || 0, + installation: response.installation || 0, + inspection: response.inspection || 0, + other: response.other || 0, }, }; } catch (error) { - console.error('getContractStageCounts error:', error); + console.error('단계별 건수 조회 오류:', error); return { success: false, error: '단계별 건수를 불러오는데 실패했습니다.' }; } } /** * 계약 단건 조회 + * GET /api/v1/construction/contracts/{id} */ export async function getContract(id: string): Promise<{ success: boolean; @@ -283,21 +305,17 @@ export async function getContract(id: string): Promise<{ error?: string; }> { try { - const result = await apiRequest>(`/construction/contracts/${id}`); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '계약 정보를 찾을 수 없습니다.' }; - } - - return { success: true, data: transformContract(result.data) }; + const response = await apiClient.get(`/construction/contracts/${id}`); + return { success: true, data: transformContract(response) }; } catch (error) { - console.error('getContract error:', error); - return { success: false, error: '계약 정보를 불러오는데 실패했습니다.' }; + console.error('계약 조회 오류:', error); + return { success: false, error: '계약 정보를 찾을 수 없습니다.' }; } } /** * 계약 상세 조회 (첨부파일 포함) + * GET /api/v1/construction/contracts/{id} */ export async function getContractDetail(id: string): Promise<{ success: boolean; @@ -305,69 +323,36 @@ export async function getContractDetail(id: string): Promise<{ error?: string; }> { try { - const result = await apiRequest>(`/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 | null; - const attachmentsData = result.data.attachments as Record[] | 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 }; + const response = await apiClient.get(`/construction/contracts/${id}`); + return { success: true, data: transformContractDetail(response) }; } catch (error) { - console.error('getContractDetail error:', 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 }> { +export async function createContract(data: ContractFormData): Promise<{ + success: boolean; + data?: Contract; + error?: string; +}> { try { const apiData = transformToApiRequest(data); - - const result = await apiRequest>('/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) }; + const response = await apiClient.post('/construction/contracts', apiData); + return { success: true, data: transformContract(response) }; } catch (error) { - console.error('createContract error:', error); + console.error('계약 등록 오류:', error); return { success: false, error: '계약 등록에 실패했습니다.' }; } } /** * 계약 수정 + * PUT /api/v1/construction/contracts/{id} */ export async function updateContract( id: string, @@ -379,48 +364,34 @@ export async function updateContract( }> { try { const apiData = transformToApiRequest(data); - - const result = await apiRequest>(`/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) }; + const response = await apiClient.put(`/construction/contracts/${id}`, apiData); + return { success: true, data: transformContract(response) }; } catch (error) { - console.error('updateContract error:', 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 { - const result = await apiRequest(`/construction/contracts/${id}`, { - method: 'DELETE', - }); - - if (!result.success) { - return { success: false, error: result.error || '계약 삭제에 실패했습니다.' }; - } - + await apiClient.delete(`/construction/contracts/${id}`); return { success: true }; } catch (error) { - console.error('deleteContract error:', error); + console.error('계약 삭제 오류:', error); return { success: false, error: '계약 삭제에 실패했습니다.' }; } } /** * 계약 일괄 삭제 + * DELETE /api/v1/construction/contracts/bulk */ export async function deleteContracts(ids: string[]): Promise<{ success: boolean; @@ -428,18 +399,12 @@ export async function deleteContracts(ids: string[]): Promise<{ error?: string; }> { try { - const result = await apiRequest('/construction/contracts/bulk', { - method: 'DELETE', - body: JSON.stringify({ ids: ids.map(id => Number(id)) }), + await apiClient.delete('/construction/contracts/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('deleteContracts error:', error); + console.error('계약 일괄 삭제 오류:', error); return { success: false, error: '일괄 삭제에 실패했습니다.' }; } } \ No newline at end of file diff --git a/src/components/business/construction/partners/actions.ts b/src/components/business/construction/partners/actions.ts index ec09ce55..5b78610a 100644 --- a/src/components/business/construction/partners/actions.ts +++ b/src/components/business/construction/partners/actions.ts @@ -1,81 +1,63 @@ 'use server'; -import { cookies } from 'next/headers'; import type { Partner, PartnerStats, PartnerFilter, PartnerListResponse, PartnerFormData } 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('🔵 [Partner API]', options.method || 'GET', url); - - const response = await fetch(url, { - ...options, - headers: { - ...headers, - ...options.headers, - }, - }); - - const result = await response.json(); - console.log('🔵 [Partner 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 ApiPartner { + id: number; + client_code: string | null; + business_no: string | null; + name: string; + contact_person: string | null; + client_type: string | null; + business_type: string | null; + business_item: string | null; + address: string | null; + phone: string | null; + mobile: string | null; + fax: string | null; + email: string | null; + manager_name: string | null; + manager_tel: string | null; + system_manager: string | null; + outstanding_amount: number | null; + is_overdue: boolean; + has_bad_debt: boolean; + is_active: boolean; + created_at: string; + updated_at: string; } +interface ApiPartnerStats { + total: number; + sales: number; + purchase: number; + both: number; + badDebt: number; + normal: number; +} + +// ======================================== +// 타입 변환 함수 +// ======================================== + /** * client_type API → Frontend partnerType 변환 */ function transformClientType(clientType: string | null | undefined): Partner['partnerType'] { const typeMap: Record = { - 'SALES': 'sales', - 'PURCHASE': 'purchase', - 'BOTH': 'both', + SALES: 'sales', + PURCHASE: 'purchase', + BOTH: 'both', }; return typeMap[clientType || ''] || 'sales'; } @@ -85,59 +67,59 @@ function transformClientType(clientType: string | null | undefined): Partner['pa */ function transformPartnerType(partnerType: Partner['partnerType']): string { const typeMap: Record = { - 'sales': 'SALES', - 'purchase': 'PURCHASE', - 'both': 'BOTH', + sales: 'SALES', + purchase: 'PURCHASE', + both: 'BOTH', }; return typeMap[partnerType] || 'SALES'; } /** - * API 응답 → 프론트엔드 Partner 타입 변환 + * API 응답 → Partner 타입 변환 */ -function transformPartner(apiData: Record): Partner { +function transformPartner(apiData: ApiPartner): Partner { return { id: String(apiData.id), - partnerCode: String(apiData.client_code || ''), - businessNumber: String(apiData.business_no || ''), - partnerName: String(apiData.name || ''), - representative: String(apiData.contact_person || ''), - partnerType: transformClientType(apiData.client_type as string | null), - businessType: String(apiData.business_type || ''), - businessCategory: String(apiData.business_item || ''), - zipCode: '', // API에 없는 필드 - address1: String(apiData.address || ''), - address2: '', // API에 없는 필드 - phone: String(apiData.phone || ''), - mobile: String(apiData.mobile || ''), - fax: String(apiData.fax || ''), - email: String(apiData.email || ''), - manager: String(apiData.manager_name || ''), - managerPhone: String(apiData.manager_tel || ''), - systemManager: String(apiData.system_manager || ''), - logoUrl: null, // API에 없는 필드 - logoBlob: null, // API에 없는 필드 - salesPaymentDay: 0, // API에 없는 필드 - creditRating: '', // API에 없는 필드 - transactionGrade: '', // API에 없는 필드 - taxInvoiceEmail: String(apiData.email || ''), // 동일한 이메일 사용 - outstandingAmount: Number(apiData.outstanding_amount || 0), - overdueDays: apiData.is_overdue ? 30 : 0, // 연체 여부만 있음 - overdueToggle: Boolean(apiData.is_overdue), - badDebtToggle: Boolean(apiData.has_bad_debt), - memos: [], // API에 없는 필드 - documents: [], // API에 없는 필드 - category: '', // API에 없는 필드 - paymentDay: 0, // API에 없는 필드 - isBadDebt: Boolean(apiData.has_bad_debt), + partnerCode: apiData.client_code || '', + businessNumber: apiData.business_no || '', + partnerName: apiData.name || '', + representative: apiData.contact_person || '', + partnerType: transformClientType(apiData.client_type), + businessType: apiData.business_type || '', + businessCategory: apiData.business_item || '', + zipCode: '', + address1: apiData.address || '', + address2: '', + phone: apiData.phone || '', + mobile: apiData.mobile || '', + fax: apiData.fax || '', + email: apiData.email || '', + manager: apiData.manager_name || '', + managerPhone: apiData.manager_tel || '', + systemManager: apiData.system_manager || '', + logoUrl: null, + logoBlob: null, + salesPaymentDay: 0, + creditRating: '', + transactionGrade: '', + taxInvoiceEmail: apiData.email || '', + outstandingAmount: apiData.outstanding_amount || 0, + overdueDays: apiData.is_overdue ? 30 : 0, + overdueToggle: apiData.is_overdue, + badDebtToggle: apiData.has_bad_debt, + memos: [], + documents: [], + category: '', + paymentDay: 0, + isBadDebt: apiData.has_bad_debt, isActive: apiData.is_active !== false, - createdAt: String(apiData.created_at || ''), - updatedAt: String(apiData.updated_at || ''), + createdAt: apiData.created_at || '', + updatedAt: apiData.updated_at || '', }; } /** - * 프론트엔드 PartnerFormData → API 요청 데이터 변환 + * PartnerFormData → API 요청 데이터 변환 */ function transformPartnerToApi(data: PartnerFormData): Record { return { @@ -160,57 +142,45 @@ function transformPartnerToApi(data: PartnerFormData): Record { }; } -// ============================================================ -// API 연동 함수 -// ============================================================ +// ======================================== +// API 함수 +// ======================================== /** * 거래처 목록 조회 + * GET /api/v1/clients */ -export async function getPartnerList( - filter?: PartnerFilter -): Promise<{ success: boolean; data?: PartnerListResponse; error?: string }> { +export async function getPartnerList(filter?: PartnerFilter): Promise<{ + success: boolean; + data?: PartnerListResponse; + error?: string; +}> { try { - const queryParams = new URLSearchParams(); + const queryParams: Record = {}; // 검색어 - if (filter?.search) { - queryParams.append('q', filter.search); - } - - // 악성채권 필터 (Frontend badDebtFilter → 백엔드는 별도 필터 없음, 목록에서 처리) - // API는 전체 데이터 반환, 프론트에서 필터링 + if (filter?.search) queryParams.q = filter.search; // 페이지네이션 - if (filter?.page) queryParams.append('page', String(filter.page)); - if (filter?.size) queryParams.append('size', String(filter.size)); + if (filter?.page) queryParams.page = String(filter.page); + if (filter?.size) queryParams.size = String(filter.size); - const queryString = queryParams.toString(); - const endpoint = `/clients${queryString ? `?${queryString}` : ''}`; - - const result = await apiRequest<{ - data: Record[]; + const response = await apiClient.get<{ + data: ApiPartner[]; current_page: number; per_page: number; total: number; last_page: number; - }>(endpoint); + }>('/clients', { params: queryParams }); - if (!result.success || !result.data) { - return { success: false, error: result.error || '거래처 목록 조회에 실패했습니다.' }; - } - - const apiData = result.data; - let items = (apiData.data || []).map(transformPartner); + let items = (response.data || []).map(transformPartner); // 악성채권 필터 (프론트엔드에서 처리) if (filter?.badDebtFilter && filter.badDebtFilter !== 'all') { - items = items.filter((p) => - filter.badDebtFilter === 'badDebt' ? p.isBadDebt : !p.isBadDebt - ); + items = items.filter((p) => (filter.badDebtFilter === 'badDebt' ? p.isBadDebt : !p.isBadDebt)); } - // 정렬 (프론트엔드에서 처리 - API가 sort 미지원 시) + // 정렬 (프론트엔드에서 처리) if (filter?.sortBy) { switch (filter.sortBy) { case 'latest': @@ -232,162 +202,134 @@ export async function getPartnerList( 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('getPartnerList error:', error); - return { success: false, error: '거래처 목록 조회에 실패했습니다.' }; + console.error('거래처 목록 조회 오류:', error); + return { success: false, error: '거래처 목록을 불러오는데 실패했습니다.' }; } } /** * 거래처 상세 조회 + * GET /api/v1/clients/{id} */ -export async function getPartner( - id: string -): Promise<{ success: boolean; data?: Partner; error?: string }> { +export async function getPartner(id: string): Promise<{ + success: boolean; + data?: Partner; + error?: string; +}> { try { - const result = await apiRequest>(`/clients/${id}`); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '거래처를 찾을 수 없습니다.' }; - } - - return { success: true, data: transformPartner(result.data) }; + const response = await apiClient.get(`/clients/${id}`); + return { success: true, data: transformPartner(response) }; } catch (error) { - console.error('getPartner error:', error); - return { success: false, error: '거래처 조회에 실패했습니다.' }; + console.error('거래처 조회 오류:', error); + return { success: false, error: '거래처를 찾을 수 없습니다.' }; } } /** * 거래처 등록 + * POST /api/v1/clients */ -export async function createPartner( - data: PartnerFormData -): Promise<{ success: boolean; data?: Partner; error?: string }> { +export async function createPartner(data: PartnerFormData): Promise<{ + success: boolean; + data?: Partner; + error?: string; +}> { try { const apiData = transformPartnerToApi(data); - - const result = await apiRequest>('/clients', { - method: 'POST', - body: JSON.stringify(apiData), - }); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '거래처 등록에 실패했습니다.' }; - } - - return { success: true, data: transformPartner(result.data) }; + const response = await apiClient.post('/clients', apiData); + return { success: true, data: transformPartner(response) }; } catch (error) { - console.error('createPartner error:', error); + console.error('거래처 등록 오류:', error); return { success: false, error: '거래처 등록에 실패했습니다.' }; } } /** * 거래처 수정 + * PUT /api/v1/clients/{id} */ -export async function updatePartner( - id: string, - data: PartnerFormData -): Promise<{ success: boolean; data?: Partner; error?: string }> { +export async function updatePartner(id: string, data: PartnerFormData): Promise<{ + success: boolean; + data?: Partner; + error?: string; +}> { try { const apiData = transformPartnerToApi(data); - - const result = await apiRequest>(`/clients/${id}`, { - method: 'PUT', - body: JSON.stringify(apiData), - }); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '거래처 수정에 실패했습니다.' }; - } - - return { success: true, data: transformPartner(result.data) }; + const response = await apiClient.put(`/clients/${id}`, apiData); + return { success: true, data: transformPartner(response) }; } catch (error) { - console.error('updatePartner error:', error); + console.error('거래처 수정 오류:', error); return { success: false, error: '거래처 수정에 실패했습니다.' }; } } /** * 거래처 통계 조회 + * GET /api/v1/clients/stats */ -export async function getPartnerStats(): Promise<{ success: boolean; data?: PartnerStats; error?: string }> { +export async function getPartnerStats(): Promise<{ + success: boolean; + data?: PartnerStats; + error?: string; +}> { try { - const result = await apiRequest<{ - total: number; - sales: number; - purchase: number; - both: number; - badDebt: number; - normal: number; - }>('/clients/stats'); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '통계 조회에 실패했습니다.' }; - } + const response = await apiClient.get('/clients/stats'); return { success: true, data: { - total: result.data.total || 0, - unregistered: 0, // Client API에서 미지원 (거래처는 등록 완료 상태만) - badDebt: result.data.badDebt || 0, - normal: result.data.normal || 0, + total: response.total || 0, + unregistered: 0, + badDebt: response.badDebt || 0, + normal: response.normal || 0, }, }; } catch (error) { - console.error('getPartnerStats error:', error); - return { success: false, error: '통계 조회에 실패했습니다.' }; + console.error('거래처 통계 조회 오류:', error); + return { success: false, error: '통계를 불러오는데 실패했습니다.' }; } } /** * 거래처 삭제 + * DELETE /api/v1/clients/{id} */ -export async function deletePartner(id: string): Promise<{ success: boolean; error?: string }> { +export async function deletePartner(id: string): Promise<{ + success: boolean; + error?: string; +}> { try { - const result = await apiRequest(`/clients/${id}`, { - method: 'DELETE', - }); - - if (!result.success) { - return { success: false, error: result.error || '거래처 삭제에 실패했습니다.' }; - } - + await apiClient.delete(`/clients/${id}`); return { success: true }; } catch (error) { - console.error('deletePartner error:', error); + console.error('거래처 삭제 오류:', error); return { success: false, error: '거래처 삭제에 실패했습니다.' }; } } /** * 거래처 일괄 삭제 + * DELETE /api/v1/clients/bulk */ -export async function deletePartners(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> { +export async function deletePartners(ids: string[]): Promise<{ + success: boolean; + deletedCount?: number; + error?: string; +}> { try { - const result = await apiRequest<{ deleted_count: number }>('/clients/bulk', { - method: 'DELETE', - body: JSON.stringify({ ids: ids.map((id) => Number(id)) }), + await apiClient.delete('/clients/bulk', { + data: { ids: ids.map((id) => Number(id)) }, }); - - if (!result.success) { - return { success: false, error: result.error || '일괄 삭제에 실패했습니다.' }; - } - - return { - success: true, - deletedCount: result.data?.deleted_count || ids.length, - }; + return { success: true, deletedCount: ids.length }; } catch (error) { - console.error('deletePartners error:', error); + console.error('거래처 일괄 삭제 오류:', error); return { success: false, error: '일괄 삭제에 실패했습니다.' }; } } \ No newline at end of file diff --git a/src/components/business/construction/site-management/actions.ts b/src/components/business/construction/site-management/actions.ts index b668854d..2ccaadca 100644 --- a/src/components/business/construction/site-management/actions.ts +++ b/src/components/business/construction/site-management/actions.ts @@ -1,96 +1,64 @@ 'use server'; -import { cookies } from 'next/headers'; import type { Site, SiteStats, SiteStatus } 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('🔵 [Site API]', options.method || 'GET', url); - - const response = await fetch(url, { - ...options, - headers: { - ...headers, - ...options.headers, - }, - }); - - const result = await response.json(); - console.log('🔵 [Site 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 ApiSite { + id: number; + site_code: string | null; + client_id: number | null; + name: string; + address: string | null; + status: SiteStatus; + created_at: string; + updated_at: string; + client?: { + id: number; + name: string; + } | null; } -/** - * API 응답 → 프론트엔드 타입 변환 - */ -function transformSite(apiData: Record): Site { - // client 관계 데이터 추출 - const client = apiData.client as Record | null | undefined; +interface ApiSiteStats { + total: number; + construction: number; + unregistered: number; + suspended: number; + pending: number; +} +// ======================================== +// 타입 변환 함수 +// ======================================== + +/** + * API 응답 → Site 타입 변환 + */ +function transformSite(apiData: ApiSite): Site { return { id: String(apiData.id), - siteCode: String(apiData.site_code || ''), + siteCode: apiData.site_code || '', partnerId: apiData.client_id ? String(apiData.client_id) : '', - partnerName: client ? String(client.name || '') : '', - siteName: String(apiData.name || ''), - address: String(apiData.address || ''), - status: (apiData.status as SiteStatus) || 'unregistered', - createdAt: String(apiData.created_at || ''), - updatedAt: String(apiData.updated_at || ''), + partnerName: apiData.client?.name || '', + siteName: apiData.name || '', + address: apiData.address || '', + status: apiData.status || 'unregistered', + createdAt: apiData.created_at || '', + updatedAt: apiData.updated_at || '', }; } -// ============================================================ -// API 연동 함수 -// ============================================================ +// ======================================== +// API 함수 +// ======================================== interface GetSiteListParams { size?: number; @@ -103,7 +71,11 @@ interface GetSiteListParams { sortBy?: string; } -interface GetSiteListResult { +/** + * 현장 목록 조회 + * GET /api/v1/sites + */ +export async function getSiteList(params: GetSiteListParams = {}): Promise<{ success: boolean; data?: { items: Site[]; @@ -113,24 +85,26 @@ interface GetSiteListResult { totalPages: number; }; error?: string; -} - -/** - * 현장 목록 조회 - */ -export async function getSiteList(params: GetSiteListParams = {}): Promise { +}> { 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.clientId && params.clientId !== 'all') queryParams.append('client_id', params.clientId); - 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.search) queryParams.search = params.search; - // 정렬 파라미터 변환 + // 필터 + if (params.status && params.status !== 'all') queryParams.status = params.status; + if (params.clientId && params.clientId !== 'all') queryParams.client_id = params.clientId; + + // 날짜 범위 + if (params.startDate) queryParams.start_date = params.startDate; + if (params.endDate) queryParams.end_date = params.endDate; + + // 페이지네이션 + if (params.page) queryParams.page = String(params.page); + if (params.size) queryParams.per_page = String(params.size); + + // 정렬 if (params.sortBy) { const sortMap: Record = { latest: { field: 'created_at', dir: 'desc' }, @@ -142,135 +116,98 @@ export async function getSiteList(params: GetSiteListParams = {}): Promise[]; + const response = await apiClient.get<{ + data: ApiSite[]; current_page: number; per_page: number; total: number; last_page: number; - }>(endpoint); + }>('/sites', { params: queryParams }); - if (!result.success || !result.data) { - return { success: false, error: result.error || '현장 목록 조회에 실패했습니다.' }; - } - - const apiData = result.data; - const items = (apiData.data || []).map(transformSite); + const items = (response.data || []).map(transformSite); 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('getSiteList error:', error); + console.error('현장 목록 조회 오류:', error); return { success: false, error: '현장 목록을 불러오는데 실패했습니다.' }; } } -interface GetSiteStatsResult { +/** + * 현장 통계 조회 + * GET /api/v1/sites/stats + */ +export async function getSiteStats(): Promise<{ success: boolean; data?: SiteStats; error?: string; -} - -/** - * 현장 통계 조회 - */ -export async function getSiteStats(): Promise { +}> { try { - const result = await apiRequest<{ - total: number; - construction: number; - unregistered: number; - suspended: number; - pending: number; - }>('/sites/stats'); - - if (!result.success || !result.data) { - return { success: false, error: result.error || '현장 통계 조회에 실패했습니다.' }; - } + const response = await apiClient.get('/sites/stats'); return { success: true, data: { - total: result.data.total || 0, - construction: result.data.construction || 0, - unregistered: result.data.unregistered || 0, - suspended: result.data.suspended || 0, - pending: result.data.pending || 0, + total: response.total || 0, + construction: response.construction || 0, + unregistered: response.unregistered || 0, + suspended: response.suspended || 0, + pending: response.pending || 0, }, }; } catch (error) { - console.error('getSiteStats error:', error); + console.error('현장 통계 조회 오류:', error); return { success: false, error: '현장 통계를 불러오는데 실패했습니다.' }; } } -interface DeleteSiteResult { - success: boolean; - error?: string; -} - /** * 현장 삭제 + * DELETE /api/v1/sites/{id} */ -export async function deleteSite(id: string): Promise { +export async function deleteSite(id: string): Promise<{ + success: boolean; + error?: string; +}> { try { - const result = await apiRequest(`/sites/${id}`, { - method: 'DELETE', - }); - - if (!result.success) { - return { success: false, error: result.error || '현장 삭제에 실패했습니다.' }; - } - + await apiClient.delete(`/sites/${id}`); return { success: true }; } catch (error) { - console.error('deleteSite error:', error); + console.error('현장 삭제 오류:', error); return { success: false, error: '현장 삭제에 실패했습니다.' }; } } -interface DeleteSitesResult { +/** + * 현장 일괄 삭제 + * DELETE /api/v1/sites/bulk + */ +export async function deleteSites(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; -} - -/** - * 현장 일괄 삭제 - */ -export async function deleteSites(ids: string[]): Promise { +}> { try { - const result = await apiRequest<{ deleted_count: number }>('/sites/bulk', { - method: 'DELETE', - body: JSON.stringify({ ids: ids.map((id) => Number(id)) }), + await apiClient.delete('/sites/bulk', { + data: { ids: ids.map((id) => Number(id)) }, }); - - if (!result.success) { - return { success: false, error: result.error || '현장 일괄 삭제에 실패했습니다.' }; - } - - return { - success: true, - deletedCount: result.data?.deleted_count || ids.length, - }; + return { success: true, deletedCount: ids.length }; } catch (error) { - console.error('deleteSites error:', error); + console.error('현장 일괄 삭제 오류:', error); return { success: false, error: '현장 일괄 삭제에 실패했습니다.' }; } } \ No newline at end of file