feat: 거래처관리 API 연동 (Phase 2.2)
- partners/actions.ts: Mock → API 연동 전환 - apiRequest 헬퍼 함수 추가 (쿠키 기반 인증) - transform 함수: client_type ↔ partnerType 변환 - getPartnerList, getPartner, createPartner, updatePartner - getPartnerStats, deletePartner, deletePartners - 구현 문서 추가
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
# Phase 2.2 거래처관리 API 연동
|
||||||
|
|
||||||
|
**날짜**: 2026-01-09
|
||||||
|
**작업**: 거래처관리 Mock → API 연동
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
시공사 페이지 API 연동 계획 Phase 2.2 - 거래처관리(partners) API 연동 완료.
|
||||||
|
|
||||||
|
## 변경 사항
|
||||||
|
|
||||||
|
### Backend (API)
|
||||||
|
|
||||||
|
#### 1. 서비스 (ClientService.php)
|
||||||
|
- `stats()` - 거래처 통계 조회 (신규)
|
||||||
|
- total: 전체 거래처 수
|
||||||
|
- sales: 판매 거래처 (client_type='SALES')
|
||||||
|
- purchase: 구매 거래처 (client_type='PURCHASE')
|
||||||
|
- both: 판매/구매 거래처 (client_type='BOTH')
|
||||||
|
- badDebt: 악성채권 보유 거래처 수
|
||||||
|
- normal: 정상 거래처 수
|
||||||
|
- `bulkDestroy()` - 일괄 삭제 (신규)
|
||||||
|
- 주문 존재 시 해당 거래처는 건너뜀
|
||||||
|
|
||||||
|
#### 2. 컨트롤러 (ClientController.php)
|
||||||
|
- `stats()` - GET /api/v1/clients/stats
|
||||||
|
- `bulkDestroy()` - DELETE /api/v1/clients/bulk
|
||||||
|
|
||||||
|
#### 3. 라우트 (api.php)
|
||||||
|
```php
|
||||||
|
Route::get('/stats', [ClientController::class, 'stats']);
|
||||||
|
Route::delete('/bulk', [ClientController::class, 'bulkDestroy']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (React)
|
||||||
|
|
||||||
|
#### 1. actions.ts
|
||||||
|
- Mock 데이터 제거 (mockPartners 배열)
|
||||||
|
- API 연동 구현
|
||||||
|
- `getPartnerList()` - GET /api/v1/clients
|
||||||
|
- `getPartner()` - GET /api/v1/clients/{id}
|
||||||
|
- `createPartner()` - POST /api/v1/clients
|
||||||
|
- `updatePartner()` - PUT /api/v1/clients/{id}
|
||||||
|
- `getPartnerStats()` - GET /api/v1/clients/stats
|
||||||
|
- `deletePartner()` - DELETE /api/v1/clients/{id}
|
||||||
|
- `deletePartners()` - DELETE /api/v1/clients/bulk
|
||||||
|
|
||||||
|
#### 2. 변환 함수
|
||||||
|
- `transformClientType()` - client_type → partnerType 변환
|
||||||
|
- `transformPartnerType()` - partnerType → client_type 변환
|
||||||
|
- `transformPartner()` - API 응답 → Partner 타입 변환
|
||||||
|
- `transformPartnerToApi()` - PartnerFormData → API 요청 데이터 변환
|
||||||
|
|
||||||
|
## API 매핑
|
||||||
|
|
||||||
|
| Frontend | Backend | 비고 |
|
||||||
|
|----------|---------|------|
|
||||||
|
| id | id | string ↔ int |
|
||||||
|
| partnerCode | client_code | 자동 생성 |
|
||||||
|
| businessNumber | business_no | |
|
||||||
|
| partnerName | name | |
|
||||||
|
| representative | contact_person | |
|
||||||
|
| partnerType | client_type | sales/SALES, purchase/PURCHASE, both/BOTH |
|
||||||
|
| businessType | business_type | |
|
||||||
|
| businessCategory | business_item | |
|
||||||
|
| address1 | address | |
|
||||||
|
| phone | phone | |
|
||||||
|
| mobile | mobile | |
|
||||||
|
| fax | fax | |
|
||||||
|
| email | email | |
|
||||||
|
| manager | manager_name | |
|
||||||
|
| managerPhone | manager_tel | |
|
||||||
|
| systemManager | system_manager | |
|
||||||
|
| outstandingAmount | outstanding_amount | 계산 필드 (매출-입금) |
|
||||||
|
| overdueToggle | is_overdue | |
|
||||||
|
| isBadDebt | has_bad_debt | 계산 필드 |
|
||||||
|
| isActive | is_active | |
|
||||||
|
| createdAt | created_at | |
|
||||||
|
| updatedAt | updated_at | |
|
||||||
|
|
||||||
|
### Frontend 전용 필드 (기본값 사용)
|
||||||
|
- zipCode, address2: ''
|
||||||
|
- logoUrl, logoBlob: null
|
||||||
|
- salesPaymentDay, paymentDay: 0
|
||||||
|
- creditRating, transactionGrade: ''
|
||||||
|
- memos, documents: []
|
||||||
|
- category: ''
|
||||||
|
- overdueDays: is_overdue ? 30 : 0
|
||||||
|
|
||||||
|
## 설계 결정
|
||||||
|
|
||||||
|
### 기존 Client API 재사용
|
||||||
|
- `/api/v1/clients` 기존 엔드포인트 확장 사용
|
||||||
|
- 별도의 `/api/v1/construction/partners` 생성하지 않음
|
||||||
|
- accounting/vendors 와 construction/partners 모두 Client API 사용
|
||||||
|
|
||||||
|
### 악성채권 통계
|
||||||
|
- BadDebt 테이블과 연계하여 악성채권 보유 거래처 수 계산
|
||||||
|
- 상태가 '추심중' 또는 '법적조치'인 활성 악성채권만 카운트
|
||||||
|
|
||||||
|
### 필터링 전략
|
||||||
|
- 검색(`q`): API에서 처리 (name, client_code, contact_person)
|
||||||
|
- 악성채권 필터: 프론트엔드에서 처리 (API 전체 반환 후 필터)
|
||||||
|
- 정렬: 프론트엔드에서 처리 (API 기본 정렬 사용)
|
||||||
|
|
||||||
|
## 진행률
|
||||||
|
|
||||||
|
시공사 API 연동: 4/9 (44%)
|
||||||
|
- [x] Phase 1.1 견적관리
|
||||||
|
- [x] Phase 1.2 인수인계보고서관리
|
||||||
|
- [x] Phase 2.1 현장관리
|
||||||
|
- [x] Phase 2.2 거래처관리 ← 현재 완료
|
||||||
|
- [ ] Phase 2.3 자재관리
|
||||||
|
- [ ] Phase 3.1 발주관리
|
||||||
|
- [ ] Phase 3.2 재고관리
|
||||||
|
- [ ] Phase 4.1 정산관리
|
||||||
|
- [ ] Phase 4.2 급여관리
|
||||||
@@ -1,193 +1,241 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import type { Partner, PartnerStats, PartnerFilter, PartnerListResponse, PartnerFormData } from './types';
|
import type { Partner, PartnerStats, PartnerFilter, PartnerListResponse, PartnerFormData } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 주일 기업 - 거래처 관리 Server Actions
|
* 주일 기업 - 거래처 관리 Server Actions
|
||||||
* TODO: 실제 API 연동 시 구현
|
* API 연동 버전
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 목업 데이터 (확장된 타입 적용)
|
// API 기본 URL
|
||||||
const mockPartners: Partner[] = [
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://api.sam.kr';
|
||||||
{
|
const API_KEY = process.env.API_KEY || '';
|
||||||
id: '1',
|
|
||||||
partnerCode: 'P-001',
|
|
||||||
businessNumber: '123-12-12345',
|
|
||||||
partnerName: '대한건설',
|
|
||||||
representative: '홍길동',
|
|
||||||
partnerType: 'sales',
|
|
||||||
businessType: '건설업',
|
|
||||||
businessCategory: '토목건축',
|
|
||||||
zipCode: '06234',
|
|
||||||
address1: '서울특별시 서초구 서초대로 123',
|
|
||||||
address2: '대한건물 12층 1201호',
|
|
||||||
phone: '02-1234-1234',
|
|
||||||
mobile: '010-1234-1234',
|
|
||||||
fax: '02-1234-1235',
|
|
||||||
email: 'abc@email.com',
|
|
||||||
manager: '담당자명',
|
|
||||||
managerPhone: '010-1234-1234',
|
|
||||||
systemManager: '관리자명',
|
|
||||||
logoUrl: null,
|
|
||||||
logoBlob: null,
|
|
||||||
salesPaymentDay: 15,
|
|
||||||
creditRating: 'AAA',
|
|
||||||
transactionGrade: 'A',
|
|
||||||
taxInvoiceEmail: 'abc@email.com',
|
|
||||||
outstandingAmount: 11000000,
|
|
||||||
overdueDays: 15,
|
|
||||||
overdueToggle: true,
|
|
||||||
badDebtToggle: false,
|
|
||||||
memos: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
content: '2025-12-12 12:21 [홍길동] 메모 내용',
|
|
||||||
createdAt: '2025-12-12T12:21:00Z',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
documents: [],
|
|
||||||
category: '건설사',
|
|
||||||
paymentDay: 15,
|
|
||||||
isBadDebt: false,
|
|
||||||
isActive: true,
|
|
||||||
createdAt: '2025-01-01',
|
|
||||||
updatedAt: '2025-01-01',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
partnerCode: 'P-002',
|
|
||||||
businessNumber: '456-45-45678',
|
|
||||||
partnerName: '삼성시공',
|
|
||||||
representative: '김철수',
|
|
||||||
partnerType: 'purchase',
|
|
||||||
businessType: '시공업',
|
|
||||||
businessCategory: '건축시공',
|
|
||||||
zipCode: '06235',
|
|
||||||
address1: '서울특별시 강남구 테헤란로 456',
|
|
||||||
address2: '삼성빌딩 5층',
|
|
||||||
phone: '02-5678-5678',
|
|
||||||
mobile: '010-5678-5678',
|
|
||||||
fax: '02-5678-5679',
|
|
||||||
email: 'samsung@email.com',
|
|
||||||
manager: '이영희',
|
|
||||||
managerPhone: '010-5678-5678',
|
|
||||||
systemManager: '',
|
|
||||||
logoUrl: null,
|
|
||||||
logoBlob: null,
|
|
||||||
salesPaymentDay: 10,
|
|
||||||
creditRating: 'AA',
|
|
||||||
transactionGrade: 'B',
|
|
||||||
taxInvoiceEmail: 'tax@samsung.com',
|
|
||||||
outstandingAmount: 5000000,
|
|
||||||
overdueDays: 0,
|
|
||||||
overdueToggle: false,
|
|
||||||
badDebtToggle: false,
|
|
||||||
memos: [],
|
|
||||||
documents: [],
|
|
||||||
category: '시공사',
|
|
||||||
paymentDay: 10,
|
|
||||||
isBadDebt: false,
|
|
||||||
isActive: true,
|
|
||||||
createdAt: '2025-01-02',
|
|
||||||
updatedAt: '2025-01-02',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
partnerCode: 'P-003',
|
|
||||||
businessNumber: '789-78-78901',
|
|
||||||
partnerName: 'LG건설',
|
|
||||||
representative: '박영수',
|
|
||||||
partnerType: 'both',
|
|
||||||
businessType: '종합건설',
|
|
||||||
businessCategory: '건설',
|
|
||||||
zipCode: '06236',
|
|
||||||
address1: '서울특별시 영등포구 여의대로 789',
|
|
||||||
address2: 'LG타워 20층',
|
|
||||||
phone: '02-7890-7890',
|
|
||||||
mobile: '010-7890-7890',
|
|
||||||
fax: '02-7890-7891',
|
|
||||||
email: 'lg@email.com',
|
|
||||||
manager: '최민수',
|
|
||||||
managerPhone: '010-7890-7890',
|
|
||||||
systemManager: '시스템관리자',
|
|
||||||
logoUrl: null,
|
|
||||||
logoBlob: null,
|
|
||||||
salesPaymentDay: 20,
|
|
||||||
creditRating: 'BBB',
|
|
||||||
transactionGrade: 'C',
|
|
||||||
taxInvoiceEmail: 'tax@lg.com',
|
|
||||||
outstandingAmount: 20000000,
|
|
||||||
overdueDays: 30,
|
|
||||||
overdueToggle: true,
|
|
||||||
badDebtToggle: true,
|
|
||||||
memos: [],
|
|
||||||
documents: [],
|
|
||||||
category: '건설사',
|
|
||||||
paymentDay: 20,
|
|
||||||
isBadDebt: true,
|
|
||||||
isActive: true,
|
|
||||||
createdAt: '2025-01-03',
|
|
||||||
updatedAt: '2025-01-03',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 거래처 목록 조회
|
/**
|
||||||
|
* 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 : '알 수 없는 오류가 발생했습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
};
|
||||||
|
return typeMap[clientType || ''] || 'sales';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* partnerType Frontend → API client_type 변환
|
||||||
|
*/
|
||||||
|
function transformPartnerType(partnerType: Partner['partnerType']): string {
|
||||||
|
const typeMap: Record<Partner['partnerType'], string> = {
|
||||||
|
'sales': 'SALES',
|
||||||
|
'purchase': 'PURCHASE',
|
||||||
|
'both': 'BOTH',
|
||||||
|
};
|
||||||
|
return typeMap[partnerType] || 'SALES';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 응답 → 프론트엔드 Partner 타입 변환
|
||||||
|
*/
|
||||||
|
function transformPartner(apiData: Record<string, unknown>): 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),
|
||||||
|
isActive: apiData.is_active !== false,
|
||||||
|
createdAt: String(apiData.created_at || ''),
|
||||||
|
updatedAt: String(apiData.updated_at || ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프론트엔드 PartnerFormData → API 요청 데이터 변환
|
||||||
|
*/
|
||||||
|
function transformPartnerToApi(data: PartnerFormData): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
business_no: data.businessNumber,
|
||||||
|
name: data.partnerName,
|
||||||
|
contact_person: data.representative,
|
||||||
|
client_type: transformPartnerType(data.partnerType),
|
||||||
|
business_type: data.businessType,
|
||||||
|
business_item: data.businessCategory,
|
||||||
|
address: data.address1 + (data.address2 ? ` ${data.address2}` : ''),
|
||||||
|
phone: data.phone,
|
||||||
|
mobile: data.mobile,
|
||||||
|
fax: data.fax,
|
||||||
|
email: data.email,
|
||||||
|
manager_name: data.manager,
|
||||||
|
manager_tel: data.managerPhone,
|
||||||
|
system_manager: data.systemManager,
|
||||||
|
is_overdue: data.overdueToggle,
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API 연동 함수
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 거래처 목록 조회
|
||||||
|
*/
|
||||||
export async function getPartnerList(
|
export async function getPartnerList(
|
||||||
filter?: PartnerFilter
|
filter?: PartnerFilter
|
||||||
): Promise<{ success: boolean; data?: PartnerListResponse; error?: string }> {
|
): Promise<{ success: boolean; data?: PartnerListResponse; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// TODO: 실제 API 호출
|
const queryParams = new URLSearchParams();
|
||||||
let filtered = [...mockPartners];
|
|
||||||
|
|
||||||
// 검색 필터
|
// 검색어
|
||||||
if (filter?.search) {
|
if (filter?.search) {
|
||||||
const search = filter.search.toLowerCase();
|
queryParams.append('q', filter.search);
|
||||||
filtered = filtered.filter(
|
|
||||||
(p) =>
|
|
||||||
p.partnerName.toLowerCase().includes(search) ||
|
|
||||||
p.partnerCode.toLowerCase().includes(search) ||
|
|
||||||
p.representative.toLowerCase().includes(search)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 악성채권 필터
|
// 악성채권 필터 (Frontend badDebtFilter → 백엔드는 별도 필터 없음, 목록에서 처리)
|
||||||
|
// API는 전체 데이터 반환, 프론트에서 필터링
|
||||||
|
|
||||||
|
// 페이지네이션
|
||||||
|
if (filter?.page) queryParams.append('page', String(filter.page));
|
||||||
|
if (filter?.size) queryParams.append('size', String(filter.size));
|
||||||
|
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
const endpoint = `/clients${queryString ? `?${queryString}` : ''}`;
|
||||||
|
|
||||||
|
const result = await apiRequest<{
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
current_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
last_page: number;
|
||||||
|
}>(endpoint);
|
||||||
|
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
return { success: false, error: result.error || '거래처 목록 조회에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiData = result.data;
|
||||||
|
let items = (apiData.data || []).map(transformPartner);
|
||||||
|
|
||||||
|
// 악성채권 필터 (프론트엔드에서 처리)
|
||||||
if (filter?.badDebtFilter && filter.badDebtFilter !== 'all') {
|
if (filter?.badDebtFilter && filter.badDebtFilter !== 'all') {
|
||||||
filtered = filtered.filter((p) =>
|
items = items.filter((p) =>
|
||||||
filter.badDebtFilter === 'badDebt' ? p.isBadDebt : !p.isBadDebt
|
filter.badDebtFilter === 'badDebt' ? p.isBadDebt : !p.isBadDebt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 정렬
|
// 정렬 (프론트엔드에서 처리 - API가 sort 미지원 시)
|
||||||
if (filter?.sortBy) {
|
if (filter?.sortBy) {
|
||||||
switch (filter.sortBy) {
|
switch (filter.sortBy) {
|
||||||
case 'latest':
|
case 'latest':
|
||||||
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||||
break;
|
break;
|
||||||
case 'oldest':
|
case 'oldest':
|
||||||
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
items.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||||
break;
|
break;
|
||||||
case 'nameAsc':
|
case 'nameAsc':
|
||||||
filtered.sort((a, b) => a.partnerName.localeCompare(b.partnerName));
|
items.sort((a, b) => a.partnerName.localeCompare(b.partnerName));
|
||||||
break;
|
break;
|
||||||
case 'nameDesc':
|
case 'nameDesc':
|
||||||
filtered.sort((a, b) => b.partnerName.localeCompare(a.partnerName));
|
items.sort((a, b) => b.partnerName.localeCompare(a.partnerName));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = filter?.page ?? 1;
|
|
||||||
const size = filter?.size ?? 20;
|
|
||||||
const start = (page - 1) * size;
|
|
||||||
const paginatedItems = filtered.slice(start, start + size);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
items: paginatedItems,
|
items,
|
||||||
total: filtered.length,
|
total: apiData.total || 0,
|
||||||
page,
|
page: apiData.current_page || 1,
|
||||||
size,
|
size: apiData.per_page || 20,
|
||||||
totalPages: Math.ceil(filtered.length / size),
|
totalPages: apiData.last_page || 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -196,150 +244,102 @@ export async function getPartnerList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 거래처 상세 조회
|
/**
|
||||||
|
* 거래처 상세 조회
|
||||||
|
*/
|
||||||
export async function getPartner(
|
export async function getPartner(
|
||||||
id: string
|
id: string
|
||||||
): Promise<{ success: boolean; data?: Partner; error?: string }> {
|
): Promise<{ success: boolean; data?: Partner; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// TODO: 실제 API 호출
|
const result = await apiRequest<Record<string, unknown>>(`/clients/${id}`);
|
||||||
const partner = mockPartners.find((p) => p.id === id);
|
|
||||||
|
|
||||||
if (!partner) {
|
if (!result.success || !result.data) {
|
||||||
return { success: false, error: '거래처를 찾을 수 없습니다.' };
|
return { success: false, error: result.error || '거래처를 찾을 수 없습니다.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, data: partner };
|
return { success: true, data: transformPartner(result.data) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('getPartner error:', error);
|
console.error('getPartner error:', error);
|
||||||
return { success: false, error: '거래처 조회에 실패했습니다.' };
|
return { success: false, error: '거래처 조회에 실패했습니다.' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 거래처 등록
|
/**
|
||||||
|
* 거래처 등록
|
||||||
|
*/
|
||||||
export async function createPartner(
|
export async function createPartner(
|
||||||
data: PartnerFormData
|
data: PartnerFormData
|
||||||
): Promise<{ success: boolean; data?: Partner; error?: string }> {
|
): Promise<{ success: boolean; data?: Partner; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// TODO: 실제 API 호출
|
const apiData = transformPartnerToApi(data);
|
||||||
console.log('Create partner:', data);
|
|
||||||
|
|
||||||
const newPartner: Partner = {
|
const result = await apiRequest<Record<string, unknown>>('/clients', {
|
||||||
id: String(Date.now()),
|
method: 'POST',
|
||||||
partnerCode: `P-${String(mockPartners.length + 1).padStart(3, '0')}`,
|
body: JSON.stringify(apiData),
|
||||||
businessNumber: data.businessNumber,
|
});
|
||||||
partnerName: data.partnerName,
|
|
||||||
representative: data.representative,
|
|
||||||
partnerType: data.partnerType,
|
|
||||||
businessType: data.businessType,
|
|
||||||
businessCategory: data.businessCategory,
|
|
||||||
zipCode: data.zipCode,
|
|
||||||
address1: data.address1,
|
|
||||||
address2: data.address2,
|
|
||||||
phone: data.phone,
|
|
||||||
mobile: data.mobile,
|
|
||||||
fax: data.fax,
|
|
||||||
email: data.email,
|
|
||||||
manager: data.manager,
|
|
||||||
managerPhone: data.managerPhone,
|
|
||||||
systemManager: data.systemManager,
|
|
||||||
logoUrl: data.logoUrl,
|
|
||||||
logoBlob: data.logoBlob,
|
|
||||||
salesPaymentDay: data.salesPaymentDay,
|
|
||||||
creditRating: data.creditRating,
|
|
||||||
transactionGrade: data.transactionGrade,
|
|
||||||
taxInvoiceEmail: data.taxInvoiceEmail,
|
|
||||||
outstandingAmount: data.outstandingAmount,
|
|
||||||
overdueDays: data.overdueDays,
|
|
||||||
overdueToggle: data.overdueToggle,
|
|
||||||
badDebtToggle: data.badDebtToggle,
|
|
||||||
memos: data.memos,
|
|
||||||
documents: data.documents,
|
|
||||||
category: data.category,
|
|
||||||
paymentDay: data.salesPaymentDay,
|
|
||||||
isBadDebt: data.badDebtToggle,
|
|
||||||
isActive: true,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return { success: true, data: newPartner };
|
if (!result.success || !result.data) {
|
||||||
|
return { success: false, error: result.error || '거래처 등록에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: transformPartner(result.data) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('createPartner error:', error);
|
console.error('createPartner error:', error);
|
||||||
return { success: false, error: '거래처 등록에 실패했습니다.' };
|
return { success: false, error: '거래처 등록에 실패했습니다.' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 거래처 수정
|
/**
|
||||||
|
* 거래처 수정
|
||||||
|
*/
|
||||||
export async function updatePartner(
|
export async function updatePartner(
|
||||||
id: string,
|
id: string,
|
||||||
data: PartnerFormData
|
data: PartnerFormData
|
||||||
): Promise<{ success: boolean; data?: Partner; error?: string }> {
|
): Promise<{ success: boolean; data?: Partner; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// TODO: 실제 API 호출
|
const apiData = transformPartnerToApi(data);
|
||||||
console.log('Update partner:', id, data);
|
|
||||||
|
|
||||||
const existingPartner = mockPartners.find((p) => p.id === id);
|
const result = await apiRequest<Record<string, unknown>>(`/clients/${id}`, {
|
||||||
if (!existingPartner) {
|
method: 'PUT',
|
||||||
return { success: false, error: '거래처를 찾을 수 없습니다.' };
|
body: JSON.stringify(apiData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
return { success: false, error: result.error || '거래처 수정에 실패했습니다.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedPartner: Partner = {
|
return { success: true, data: transformPartner(result.data) };
|
||||||
...existingPartner,
|
|
||||||
businessNumber: data.businessNumber,
|
|
||||||
partnerName: data.partnerName,
|
|
||||||
representative: data.representative,
|
|
||||||
partnerType: data.partnerType,
|
|
||||||
businessType: data.businessType,
|
|
||||||
businessCategory: data.businessCategory,
|
|
||||||
zipCode: data.zipCode,
|
|
||||||
address1: data.address1,
|
|
||||||
address2: data.address2,
|
|
||||||
phone: data.phone,
|
|
||||||
mobile: data.mobile,
|
|
||||||
fax: data.fax,
|
|
||||||
email: data.email,
|
|
||||||
manager: data.manager,
|
|
||||||
managerPhone: data.managerPhone,
|
|
||||||
systemManager: data.systemManager,
|
|
||||||
logoUrl: data.logoUrl,
|
|
||||||
logoBlob: data.logoBlob,
|
|
||||||
salesPaymentDay: data.salesPaymentDay,
|
|
||||||
creditRating: data.creditRating,
|
|
||||||
transactionGrade: data.transactionGrade,
|
|
||||||
taxInvoiceEmail: data.taxInvoiceEmail,
|
|
||||||
outstandingAmount: data.outstandingAmount,
|
|
||||||
overdueDays: data.overdueDays,
|
|
||||||
overdueToggle: data.overdueToggle,
|
|
||||||
badDebtToggle: data.badDebtToggle,
|
|
||||||
memos: data.memos,
|
|
||||||
documents: data.documents,
|
|
||||||
category: data.category,
|
|
||||||
paymentDay: data.salesPaymentDay,
|
|
||||||
isBadDebt: data.badDebtToggle,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return { success: true, data: updatedPartner };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('updatePartner error:', error);
|
console.error('updatePartner error:', error);
|
||||||
return { success: false, error: '거래처 수정에 실패했습니다.' };
|
return { success: false, error: '거래처 수정에 실패했습니다.' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 거래처 통계 조회
|
/**
|
||||||
|
* 거래처 통계 조회
|
||||||
|
*/
|
||||||
export async function getPartnerStats(): Promise<{ success: boolean; data?: PartnerStats; error?: string }> {
|
export async function getPartnerStats(): Promise<{ success: boolean; data?: PartnerStats; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// TODO: 실제 API 호출
|
const result = await apiRequest<{
|
||||||
const total = mockPartners.length;
|
total: number;
|
||||||
const badDebt = mockPartners.filter((p) => p.isBadDebt).length;
|
sales: number;
|
||||||
|
purchase: number;
|
||||||
|
both: number;
|
||||||
|
badDebt: number;
|
||||||
|
normal: number;
|
||||||
|
}>('/clients/stats');
|
||||||
|
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
return { success: false, error: result.error || '통계 조회에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
total,
|
total: result.data.total || 0,
|
||||||
unregistered: 5, // 목업
|
unregistered: 0, // Client API에서 미지원 (거래처는 등록 완료 상태만)
|
||||||
badDebt,
|
badDebt: result.data.badDebt || 0,
|
||||||
normal: total - badDebt,
|
normal: result.data.normal || 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -348,11 +348,19 @@ export async function getPartnerStats(): Promise<{ success: boolean; data?: Part
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 거래처 삭제
|
/**
|
||||||
|
* 거래처 삭제
|
||||||
|
*/
|
||||||
export async function deletePartner(id: string): Promise<{ success: boolean; error?: string }> {
|
export async function deletePartner(id: string): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
// TODO: 실제 API 호출
|
const result = await apiRequest(`/clients/${id}`, {
|
||||||
console.log('Delete partner:', id);
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, error: result.error || '거래처 삭제에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('deletePartner error:', error);
|
console.error('deletePartner error:', error);
|
||||||
@@ -360,12 +368,24 @@ export async function deletePartner(id: string): Promise<{ success: boolean; err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 거래처 일괄 삭제
|
/**
|
||||||
|
* 거래처 일괄 삭제
|
||||||
|
*/
|
||||||
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 {
|
try {
|
||||||
// TODO: 실제 API 호출
|
const result = await apiRequest<{ deleted_count: number }>('/clients/bulk', {
|
||||||
console.log('Delete partners:', ids);
|
method: 'DELETE',
|
||||||
return { success: true, deletedCount: ids.length };
|
body: JSON.stringify({ 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,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('deletePartners error:', error);
|
console.error('deletePartners error:', error);
|
||||||
return { success: false, error: '일괄 삭제에 실패했습니다.' };
|
return { success: false, error: '일괄 삭제에 실패했습니다.' };
|
||||||
|
|||||||
Reference in New Issue
Block a user