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:
2026-01-09 19:21:34 +09:00
parent dcd79a2863
commit 5db6e59bbc
4 changed files with 532 additions and 623 deletions

View File

@@ -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: '일괄 삭제에 실패했습니다.' };
}
}

View File

@@ -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: '일괄 삭제에 실패했습니다.' };
}
}

View File

@@ -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: '현장 일괄 삭제에 실패했습니다.' };
}
}