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