feat(WEB): 건설 노무/협력사/현장관리 개선
- 노무관리 actions API 연동 개선 - 협력사 폼 및 actions 개선 - 현장관리 actions 추가
This commit is contained in:
@@ -1,89 +1,85 @@
|
||||
'use server';
|
||||
|
||||
import type { Labor, LaborListParams, LaborFormData, LaborStats } from './types';
|
||||
import type {
|
||||
Labor,
|
||||
LaborListParams,
|
||||
LaborFormData,
|
||||
LaborStats,
|
||||
} from './types';
|
||||
import { apiClient } from '@/lib/api';
|
||||
|
||||
// 목데이터 - 7건
|
||||
const mockLabors: Labor[] = [
|
||||
{
|
||||
id: '1',
|
||||
laborNumber: '123123',
|
||||
category: '가로',
|
||||
minM: 0,
|
||||
maxM: 6.00,
|
||||
laborPrice: 400000,
|
||||
status: '사용',
|
||||
createdAt: '2026-01-03T10:00:00Z',
|
||||
updatedAt: '2026-01-03T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
laborNumber: '123123',
|
||||
category: '세로할증',
|
||||
minM: 3.50,
|
||||
maxM: 3.00,
|
||||
laborPrice: null,
|
||||
status: '중지',
|
||||
createdAt: '2026-01-03T09:00:00Z',
|
||||
updatedAt: '2026-01-03T09:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
laborNumber: '123123',
|
||||
category: '가로',
|
||||
minM: 6.01,
|
||||
maxM: 7.00,
|
||||
laborPrice: null,
|
||||
status: '사용',
|
||||
createdAt: '2026-01-02T15:00:00Z',
|
||||
updatedAt: '2026-01-02T15:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
laborNumber: '123123',
|
||||
category: '세로할증',
|
||||
minM: 3.51,
|
||||
maxM: 4.50,
|
||||
laborPrice: 50000,
|
||||
status: '사용',
|
||||
createdAt: '2026-01-02T14:00:00Z',
|
||||
updatedAt: '2026-01-02T14:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
laborNumber: '123123',
|
||||
category: '가로',
|
||||
minM: 0,
|
||||
maxM: 6.00,
|
||||
laborPrice: null,
|
||||
status: '사용',
|
||||
createdAt: '2026-01-01T12:00:00Z',
|
||||
updatedAt: '2026-01-01T12:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
laborNumber: '123123',
|
||||
category: '세로할증',
|
||||
minM: 3.50,
|
||||
maxM: 0,
|
||||
laborPrice: 50000,
|
||||
status: '사용',
|
||||
createdAt: '2026-01-01T11:00:00Z',
|
||||
updatedAt: '2026-01-01T11:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
laborNumber: '123123',
|
||||
category: '가로',
|
||||
minM: 0,
|
||||
maxM: 0,
|
||||
laborPrice: null,
|
||||
status: '중지',
|
||||
createdAt: '2026-01-01T10:00:00Z',
|
||||
updatedAt: '2026-01-01T10:00:00Z',
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 시공관리 - 노임관리 Server Actions
|
||||
* 표준화된 apiClient 사용 버전
|
||||
*/
|
||||
|
||||
// 노임 목록 조회
|
||||
// ========================================
|
||||
// API 응답 타입
|
||||
// ========================================
|
||||
|
||||
interface ApiLabor {
|
||||
id: number;
|
||||
labor_number: string;
|
||||
category: '가로' | '세로할증';
|
||||
min_m: number;
|
||||
max_m: number;
|
||||
labor_price: number | null;
|
||||
status: '사용' | '중지';
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ApiLaborStats {
|
||||
total: number;
|
||||
active: number;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 타입 변환 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* API 응답 → Labor 타입 변환
|
||||
*/
|
||||
function transformLabor(apiData: ApiLabor): Labor {
|
||||
return {
|
||||
id: String(apiData.id),
|
||||
laborNumber: apiData.labor_number || '',
|
||||
category: apiData.category || '가로',
|
||||
minM: apiData.min_m || 0,
|
||||
maxM: apiData.max_m || 0,
|
||||
laborPrice: apiData.labor_price,
|
||||
status: apiData.status || '사용',
|
||||
createdAt: apiData.created_at || '',
|
||||
updatedAt: apiData.updated_at || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* LaborFormData → API 요청 데이터 변환
|
||||
*/
|
||||
function transformToApiRequest(data: Partial<LaborFormData>): Record<string, unknown> {
|
||||
const apiData: Record<string, unknown> = {};
|
||||
|
||||
if (data.laborNumber !== undefined) apiData.labor_number = data.laborNumber;
|
||||
if (data.category !== undefined) apiData.category = data.category;
|
||||
if (data.minM !== undefined) apiData.min_m = data.minM;
|
||||
if (data.maxM !== undefined) apiData.max_m = data.maxM;
|
||||
if (data.laborPrice !== undefined) apiData.labor_price = data.laborPrice;
|
||||
if (data.status !== undefined) apiData.status = data.status;
|
||||
|
||||
return apiData;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// API 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 노임 목록 조회
|
||||
* GET /api/v1/labor
|
||||
*/
|
||||
export async function getLaborList(params: LaborListParams = {}): Promise<{
|
||||
success: boolean;
|
||||
data?: Labor[];
|
||||
@@ -91,125 +87,120 @@ export async function getLaborList(params: LaborListParams = {}): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
let filtered = [...mockLabors];
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
// 검색어 필터
|
||||
if (params.search) {
|
||||
const searchLower = params.search.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(labor) =>
|
||||
labor.laborNumber.toLowerCase().includes(searchLower) ||
|
||||
labor.category.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
// 검색
|
||||
if (params.search) queryParams.search = params.search;
|
||||
|
||||
// 구분 필터
|
||||
if (params.category && params.category !== 'all') {
|
||||
filtered = filtered.filter((labor) => labor.category === params.category);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (params.status && params.status !== 'all') {
|
||||
filtered = filtered.filter((labor) => labor.status === params.status);
|
||||
}
|
||||
// 필터
|
||||
if (params.category && params.category !== 'all') queryParams.category = params.category;
|
||||
if (params.status && params.status !== 'all') queryParams.status = params.status;
|
||||
|
||||
// 날짜 필터
|
||||
if (params.startDate) {
|
||||
filtered = filtered.filter(
|
||||
(labor) => new Date(labor.createdAt) >= new Date(params.startDate!)
|
||||
);
|
||||
}
|
||||
if (params.endDate) {
|
||||
const endDate = new Date(params.endDate);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
filtered = filtered.filter(
|
||||
(labor) => new Date(labor.createdAt) <= endDate
|
||||
);
|
||||
}
|
||||
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.limit) queryParams.per_page = String(params.limit);
|
||||
|
||||
// 정렬
|
||||
if (params.sortOrder === '등록순') {
|
||||
filtered.sort(
|
||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
);
|
||||
queryParams.sort_by = 'created_at';
|
||||
queryParams.sort_dir = 'asc';
|
||||
} else {
|
||||
// 기본: 최신순
|
||||
filtered.sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
queryParams.sort_by = 'created_at';
|
||||
queryParams.sort_dir = 'desc';
|
||||
}
|
||||
|
||||
const total = filtered.length;
|
||||
const response = await apiClient.get<{
|
||||
data: ApiLabor[];
|
||||
current_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
last_page: number;
|
||||
}>('/labor', { params: queryParams });
|
||||
|
||||
// 페이지네이션
|
||||
if (params.page && params.limit) {
|
||||
const start = (params.page - 1) * params.limit;
|
||||
filtered = filtered.slice(start, start + params.limit);
|
||||
}
|
||||
const items = (response.data || []).map(transformLabor);
|
||||
|
||||
return { success: true, data: filtered, total };
|
||||
return {
|
||||
success: true,
|
||||
data: items,
|
||||
total: response.total || 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('노임 목록 조회 실패:', error);
|
||||
return { success: false, error: '노임 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// 노임 통계 조회
|
||||
/**
|
||||
* 노임 통계 조회
|
||||
* GET /api/v1/labor/stats
|
||||
*/
|
||||
export async function getLaborStats(): Promise<{
|
||||
success: boolean;
|
||||
data?: LaborStats;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const total = mockLabors.length;
|
||||
const active = mockLabors.filter((labor) => labor.status === '사용').length;
|
||||
return { success: true, data: { total, active } };
|
||||
const response = await apiClient.get<ApiLaborStats>('/labor/stats');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
total: response.total || 0,
|
||||
active: response.active || 0,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('노임 통계 조회 실패:', error);
|
||||
return { success: false, error: '노임 통계를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// 노임 상세 조회
|
||||
/**
|
||||
* 노임 상세 조회
|
||||
* GET /api/v1/labor/{id}
|
||||
*/
|
||||
export async function getLabor(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: Labor;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const labor = mockLabors.find((l) => l.id === id);
|
||||
if (!labor) {
|
||||
return { success: false, error: '노임 정보를 찾을 수 없습니다.' };
|
||||
}
|
||||
return { success: true, data: labor };
|
||||
const response = await apiClient.get<ApiLabor>(`/labor/${id}`);
|
||||
return { success: true, data: transformLabor(response) };
|
||||
} catch (error) {
|
||||
console.error('노임 상세 조회 실패:', error);
|
||||
return { success: false, error: '노임 정보를 불러오는데 실패했습니다.' };
|
||||
return { success: false, error: '노임 정보를 찾을 수 없습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// 노임 등록
|
||||
/**
|
||||
* 노임 등록
|
||||
* POST /api/v1/labor
|
||||
*/
|
||||
export async function createLabor(data: LaborFormData): Promise<{
|
||||
success: boolean;
|
||||
data?: Labor;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const newLabor: Labor = {
|
||||
id: String(Date.now()),
|
||||
...data,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
mockLabors.unshift(newLabor);
|
||||
return { success: true, data: newLabor };
|
||||
const apiData = transformToApiRequest(data);
|
||||
const response = await apiClient.post<ApiLabor>('/labor', apiData);
|
||||
return { success: true, data: transformLabor(response) };
|
||||
} catch (error) {
|
||||
console.error('노임 등록 실패:', error);
|
||||
return { success: false, error: '노임 등록에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// 노임 수정
|
||||
/**
|
||||
* 노임 수정
|
||||
* PUT /api/v1/labor/{id}
|
||||
*/
|
||||
export async function updateLabor(
|
||||
id: string,
|
||||
data: LaborFormData
|
||||
@@ -219,33 +210,25 @@ export async function updateLabor(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const index = mockLabors.findIndex((l) => l.id === id);
|
||||
if (index === -1) {
|
||||
return { success: false, error: '노임 정보를 찾을 수 없습니다.' };
|
||||
}
|
||||
mockLabors[index] = {
|
||||
...mockLabors[index],
|
||||
...data,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return { success: true, data: mockLabors[index] };
|
||||
const apiData = transformToApiRequest(data);
|
||||
const response = await apiClient.put<ApiLabor>(`/labor/${id}`, apiData);
|
||||
return { success: true, data: transformLabor(response) };
|
||||
} catch (error) {
|
||||
console.error('노임 수정 실패:', error);
|
||||
return { success: false, error: '노임 수정에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// 노임 삭제
|
||||
/**
|
||||
* 노임 삭제
|
||||
* DELETE /api/v1/labor/{id}
|
||||
*/
|
||||
export async function deleteLabor(id: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const index = mockLabors.findIndex((l) => l.id === id);
|
||||
if (index === -1) {
|
||||
return { success: false, error: '노임 정보를 찾을 수 없습니다.' };
|
||||
}
|
||||
mockLabors.splice(index, 1);
|
||||
await apiClient.delete(`/labor/${id}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('노임 삭제 실패:', error);
|
||||
@@ -253,22 +236,20 @@ export async function deleteLabor(id: string): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
// 노임 일괄 삭제
|
||||
/**
|
||||
* 노임 일괄 삭제
|
||||
* DELETE /api/v1/labor/bulk
|
||||
*/
|
||||
export async function deleteLaborBulk(ids: string[]): Promise<{
|
||||
success: boolean;
|
||||
deletedCount?: number;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
let deletedCount = 0;
|
||||
for (const id of ids) {
|
||||
const index = mockLabors.findIndex((l) => l.id === id);
|
||||
if (index !== -1) {
|
||||
mockLabors.splice(index, 1);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
return { success: true, deletedCount };
|
||||
await apiClient.delete('/labor/bulk', {
|
||||
data: { ids: ids.map((id) => Number(id)) },
|
||||
});
|
||||
return { success: true, deletedCount: ids.length };
|
||||
} catch (error) {
|
||||
console.error('노임 일괄 삭제 실패:', error);
|
||||
return { success: false, error: '노임 일괄 삭제에 실패했습니다.' };
|
||||
|
||||
@@ -33,13 +33,13 @@ import { toast } from 'sonner';
|
||||
import type { Partner, PartnerFormData, PartnerMemo, PartnerDocument } from './types';
|
||||
import {
|
||||
PARTNER_TYPE_OPTIONS,
|
||||
CATEGORY_OPTIONS,
|
||||
CREDIT_RATING_OPTIONS,
|
||||
TRANSACTION_GRADE_OPTIONS,
|
||||
PAYMENT_DAY_OPTIONS,
|
||||
getEmptyPartnerFormData,
|
||||
partnerToFormData,
|
||||
} from './types';
|
||||
import { createPartner, updatePartner, deletePartner } from './actions';
|
||||
|
||||
// 목업 문서 목록 (상세 모드에서 다운로드 버튼 테스트용)
|
||||
const MOCK_DOCUMENTS: PartnerDocument[] = [
|
||||
@@ -158,8 +158,19 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
|
||||
const handleConfirmSave = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: 실제 API 연동
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
let result;
|
||||
if (isNewMode) {
|
||||
result = await createPartner(formData);
|
||||
} else if (partnerId) {
|
||||
result = await updatePartner(partnerId, formData);
|
||||
} else {
|
||||
throw new Error('거래처 ID가 없습니다.');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '저장에 실패했습니다.');
|
||||
}
|
||||
|
||||
toast.success(isNewMode ? '거래처가 등록되었습니다.' : '수정이 완료되었습니다.');
|
||||
setShowSaveDialog(false);
|
||||
router.push('/ko/construction/project/bidding/partners');
|
||||
@@ -169,7 +180,7 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isNewMode, router]);
|
||||
}, [isNewMode, partnerId, formData, router]);
|
||||
|
||||
// 삭제 핸들러
|
||||
const handleDelete = useCallback(() => {
|
||||
@@ -177,10 +188,19 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
|
||||
}, []);
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (!partnerId) {
|
||||
toast.error('거래처 ID가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: 실제 API 연동
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const result = await deletePartner(partnerId);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
|
||||
toast.success('거래처가 삭제되었습니다.');
|
||||
setShowDeleteDialog(false);
|
||||
router.push('/ko/construction/project/bidding/partners');
|
||||
@@ -190,7 +210,7 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
}, [partnerId, router]);
|
||||
|
||||
// 메모 추가 핸들러
|
||||
const handleAddMemo = useCallback(() => {
|
||||
@@ -493,8 +513,7 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
|
||||
{renderField('대표자명', 'representative', formData.representative)}
|
||||
{renderSelectField('거래처 유형', 'partnerType', formData.partnerType, PARTNER_TYPE_OPTIONS)}
|
||||
{renderField('업태', 'businessType', formData.businessType)}
|
||||
{renderSelectField('업종', 'category', formData.category, CATEGORY_OPTIONS)}
|
||||
{renderField('업종(직접입력)', 'businessCategory', formData.businessCategory)}
|
||||
{renderField('업종', 'businessCategory', formData.businessCategory)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -74,6 +74,18 @@ function transformPartnerType(partnerType: Partner['partnerType']): string {
|
||||
return typeMap[partnerType] || 'SALES';
|
||||
}
|
||||
|
||||
/**
|
||||
* client_type → 구분 라벨 변환
|
||||
*/
|
||||
function getPartnerTypeLabel(clientType: string | null | undefined): string {
|
||||
const labelMap: Record<string, string> = {
|
||||
SALES: '매출',
|
||||
PURCHASE: '매입',
|
||||
BOTH: '복합',
|
||||
};
|
||||
return labelMap[clientType || ''] || '매출';
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 → Partner 타입 변환
|
||||
*/
|
||||
@@ -109,7 +121,7 @@ function transformPartner(apiData: ApiPartner): Partner {
|
||||
badDebtToggle: apiData.has_bad_debt,
|
||||
memos: [],
|
||||
documents: [],
|
||||
category: '',
|
||||
category: getPartnerTypeLabel(apiData.client_type),
|
||||
paymentDay: 0,
|
||||
isBadDebt: apiData.has_bad_debt,
|
||||
isActive: apiData.is_active !== false,
|
||||
@@ -165,15 +177,20 @@ export async function getPartnerList(filter?: PartnerFilter): Promise<{
|
||||
if (filter?.page) queryParams.page = String(filter.page);
|
||||
if (filter?.size) queryParams.size = String(filter.size);
|
||||
|
||||
// API 응답 구조: { success, data: { data: [...], current_page, per_page, total, last_page } }
|
||||
const response = await apiClient.get<{
|
||||
data: ApiPartner[];
|
||||
current_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
last_page: number;
|
||||
success: boolean;
|
||||
data: {
|
||||
data: ApiPartner[];
|
||||
current_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
last_page: number;
|
||||
};
|
||||
}>('/clients', { params: queryParams });
|
||||
|
||||
let items = (response.data || []).map(transformPartner);
|
||||
const paginated = response.data;
|
||||
let items = (paginated?.data || []).map(transformPartner);
|
||||
|
||||
// 악성채권 필터 (프론트엔드에서 처리)
|
||||
if (filter?.badDebtFilter && filter.badDebtFilter !== 'all') {
|
||||
@@ -202,10 +219,10 @@ export async function getPartnerList(filter?: PartnerFilter): Promise<{
|
||||
success: true,
|
||||
data: {
|
||||
items,
|
||||
total: response.total || 0,
|
||||
page: response.current_page || 1,
|
||||
size: response.per_page || 20,
|
||||
totalPages: response.last_page || 1,
|
||||
total: paginated?.total || 0,
|
||||
page: paginated?.current_page || 1,
|
||||
size: paginated?.per_page || 20,
|
||||
totalPages: paginated?.last_page || 1,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -224,8 +241,9 @@ export async function getPartner(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiPartner>(`/clients/${id}`);
|
||||
return { success: true, data: transformPartner(response) };
|
||||
// API 응답 구조: { success, data: {...single item} }
|
||||
const response = await apiClient.get<{ success: boolean; data: ApiPartner }>(`/clients/${id}`);
|
||||
return { success: true, data: transformPartner(response.data) };
|
||||
} catch (error) {
|
||||
console.error('거래처 조회 오류:', error);
|
||||
return { success: false, error: '거래처를 찾을 수 없습니다.' };
|
||||
@@ -243,8 +261,9 @@ export async function createPartner(data: PartnerFormData): Promise<{
|
||||
}> {
|
||||
try {
|
||||
const apiData = transformPartnerToApi(data);
|
||||
const response = await apiClient.post<ApiPartner>('/clients', apiData);
|
||||
return { success: true, data: transformPartner(response) };
|
||||
// API 응답 구조: { success, data: {...created item} }
|
||||
const response = await apiClient.post<{ success: boolean; data: ApiPartner }>('/clients', apiData);
|
||||
return { success: true, data: transformPartner(response.data) };
|
||||
} catch (error) {
|
||||
console.error('거래처 등록 오류:', error);
|
||||
return { success: false, error: '거래처 등록에 실패했습니다.' };
|
||||
@@ -262,8 +281,9 @@ export async function updatePartner(id: string, data: PartnerFormData): Promise<
|
||||
}> {
|
||||
try {
|
||||
const apiData = transformPartnerToApi(data);
|
||||
const response = await apiClient.put<ApiPartner>(`/clients/${id}`, apiData);
|
||||
return { success: true, data: transformPartner(response) };
|
||||
// API 응답 구조: { success, data: {...updated item} }
|
||||
const response = await apiClient.put<{ success: boolean; data: ApiPartner }>(`/clients/${id}`, apiData);
|
||||
return { success: true, data: transformPartner(response.data) };
|
||||
} catch (error) {
|
||||
console.error('거래처 수정 오류:', error);
|
||||
return { success: false, error: '거래처 수정에 실패했습니다.' };
|
||||
@@ -280,15 +300,17 @@ export async function getPartnerStats(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get<ApiPartnerStats>('/clients/stats');
|
||||
// API 응답 구조: { success, data: {...stats} }
|
||||
const response = await apiClient.get<{ success: boolean; data: ApiPartnerStats }>('/clients/stats');
|
||||
const stats = response.data;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
total: response.total || 0,
|
||||
total: stats?.total || 0,
|
||||
unregistered: 0,
|
||||
badDebt: response.badDebt || 0,
|
||||
normal: response.normal || 0,
|
||||
badDebt: stats?.badDebt || 0,
|
||||
normal: stats?.normal || 0,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -210,4 +210,40 @@ export async function deleteSites(ids: string[]): Promise<{
|
||||
console.error('현장 일괄 삭제 오류:', error);
|
||||
return { success: false, error: '현장 일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 현장 생성/수정 타입
|
||||
// ========================================
|
||||
|
||||
export interface CreateSiteData {
|
||||
siteName: string;
|
||||
partnerId: string;
|
||||
address?: string;
|
||||
status?: SiteStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현장 등록
|
||||
* POST /api/v1/sites
|
||||
*/
|
||||
export async function createSite(data: CreateSiteData): Promise<{
|
||||
success: boolean;
|
||||
data?: Site;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const apiData = {
|
||||
name: data.siteName,
|
||||
client_id: data.partnerId ? Number(data.partnerId) : null,
|
||||
address: data.address || null,
|
||||
status: data.status || 'unregistered',
|
||||
};
|
||||
|
||||
const response = await apiClient.post<{ success: boolean; data: ApiSite }>('/sites', apiData);
|
||||
return { success: true, data: transformSite(response.data) };
|
||||
} catch (error) {
|
||||
console.error('현장 등록 오류:', error);
|
||||
return { success: false, error: '현장 등록에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user