refactor(construction): 건설관리 3개 모듈 apiClient 표준화
- 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 등)
This commit is contained in:
@@ -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<T>(
|
||||
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<string, string> = {
|
||||
'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<string, unknown>): 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<ContractFormData>): Record<string, unknown> {
|
||||
const apiData: Record<string, unknown> = {};
|
||||
@@ -127,8 +157,13 @@ function transformToApiRequest(data: Partial<ContractFormData>): Record<string,
|
||||
return apiData;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// API 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 계약 목록 조회
|
||||
* GET /api/v1/construction/contracts
|
||||
*/
|
||||
export async function getContractList(filter?: ContractFilter): Promise<{
|
||||
success: boolean;
|
||||
@@ -136,20 +171,31 @@ export async function getContractList(filter?: ContractFilter): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
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<string, { field: string; dir: string }> = {
|
||||
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<string, unknown>[];
|
||||
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<ApiContractStats>('/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<ApiContractStageCount>('/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<Record<string, unknown>>(`/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<ApiContract>(`/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<Record<string, unknown>>(`/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<string, unknown> | null;
|
||||
const attachmentsData = result.data.attachments as Record<string, unknown>[] | 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<ApiContract>(`/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<Record<string, unknown>>('/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<ApiContract>('/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<Record<string, unknown>>(`/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<ApiContract>(`/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: '일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -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<T>(
|
||||
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<string, string> = {
|
||||
'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<string, Partner['partnerType']> = {
|
||||
'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<Partner['partnerType'], string> = {
|
||||
'sales': 'SALES',
|
||||
'purchase': 'PURCHASE',
|
||||
'both': 'BOTH',
|
||||
sales: 'SALES',
|
||||
purchase: 'PURCHASE',
|
||||
both: 'BOTH',
|
||||
};
|
||||
return typeMap[partnerType] || 'SALES';
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 → 프론트엔드 Partner 타입 변환
|
||||
* API 응답 → Partner 타입 변환
|
||||
*/
|
||||
function transformPartner(apiData: Record<string, unknown>): 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<string, unknown> {
|
||||
return {
|
||||
@@ -160,57 +142,45 @@ function transformPartnerToApi(data: PartnerFormData): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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<string, string> = {};
|
||||
|
||||
// 검색어
|
||||
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<string, unknown>[];
|
||||
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<Record<string, unknown>>(`/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<ApiPartner>(`/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<Record<string, unknown>>('/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<ApiPartner>('/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<Record<string, unknown>>(`/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<ApiPartner>(`/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<ApiPartnerStats>('/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: '일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -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<T>(
|
||||
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<string, string> = {
|
||||
'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<string, unknown>): Site {
|
||||
// client 관계 데이터 추출
|
||||
const client = apiData.client as Record<string, unknown> | 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<GetSiteListResult> {
|
||||
}> {
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
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<string, { field: string; dir: string }> = {
|
||||
latest: { field: 'created_at', dir: 'desc' },
|
||||
@@ -142,135 +116,98 @@ export async function getSiteList(params: GetSiteListParams = {}): Promise<GetSi
|
||||
};
|
||||
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 = `/sites${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const result = await apiRequest<{
|
||||
data: Record<string, unknown>[];
|
||||
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<GetSiteStatsResult> {
|
||||
}> {
|
||||
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<ApiSiteStats>('/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<DeleteSiteResult> {
|
||||
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<DeleteSitesResult> {
|
||||
}> {
|
||||
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: '현장 일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user