- 부실채권 상세/목록/타입 개선 - 매출관리 SalesDetail 개선 - 매입관리 PurchaseDetail 개선 - 일일보고 UI 리팩토링 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
275 lines
9.4 KiB
TypeScript
275 lines
9.4 KiB
TypeScript
/**
|
|
* 악성채권 추심관리 서버 액션
|
|
*
|
|
* API Endpoints:
|
|
* - GET /api/v1/bad-debts - 목록 조회
|
|
* - GET /api/v1/bad-debts/{id} - 상세 조회
|
|
* - POST /api/v1/bad-debts - 등록
|
|
* - PUT /api/v1/bad-debts/{id} - 수정
|
|
* - DELETE /api/v1/bad-debts/{id} - 삭제
|
|
* - PATCH /api/v1/bad-debts/{id}/toggle - 활성화 토글
|
|
* - GET /api/v1/bad-debts/summary - 통계 조회
|
|
*/
|
|
|
|
'use server';
|
|
|
|
|
|
import { revalidatePath } from 'next/cache';
|
|
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
|
import { buildApiUrl } from '@/lib/api/query-params';
|
|
import type { PaginatedApiResponse } from '@/lib/api/types';
|
|
import type { BadDebtRecord, BadDebtItem, CollectionStatus } from './types';
|
|
|
|
// ===== API 응답 타입 =====
|
|
|
|
interface BadDebtItemApiData {
|
|
id: number;
|
|
debt_amount: number;
|
|
status: 'collecting' | 'legal_action' | 'recovered' | 'bad_debt';
|
|
overdue_days: number;
|
|
is_active: boolean;
|
|
occurred_at: string | null;
|
|
assigned_user?: { id: number; name: string } | null;
|
|
}
|
|
|
|
interface BadDebtApiData {
|
|
id: number;
|
|
client_id: number;
|
|
client_code: string;
|
|
client_name: string;
|
|
business_no: string | null;
|
|
contact_person: string | null;
|
|
phone: string | null;
|
|
mobile: string | null;
|
|
email: string | null;
|
|
address: string | null;
|
|
client_type: string | null;
|
|
total_debt_amount: number;
|
|
max_overdue_days: number;
|
|
bad_debt_count: number;
|
|
status: 'collecting' | 'legal_action' | 'recovered' | 'bad_debt';
|
|
is_active: boolean;
|
|
assigned_user?: { id: number; name: string } | null;
|
|
bad_debts: BadDebtItemApiData[];
|
|
}
|
|
|
|
interface BadDebtSummaryApiData {
|
|
total_amount: number;
|
|
collecting_amount: number;
|
|
legal_action_amount: number;
|
|
recovered_amount: number;
|
|
bad_debt_amount: number;
|
|
total_count: number;
|
|
collecting_count: number;
|
|
legal_action_count: number;
|
|
recovered_count: number;
|
|
bad_debt_count: number;
|
|
}
|
|
|
|
// ===== 헬퍼 함수 =====
|
|
|
|
function mapApiStatusToFrontend(apiStatus: string): CollectionStatus {
|
|
switch (apiStatus) {
|
|
case 'collecting': return 'collecting';
|
|
case 'legal_action': return 'legalAction';
|
|
case 'recovered':
|
|
case 'bad_debt':
|
|
case 'collection_end': return 'collectionEnd';
|
|
default: return 'collecting';
|
|
}
|
|
}
|
|
|
|
function mapFrontendStatusToApi(status: CollectionStatus): string {
|
|
switch (status) {
|
|
case 'collecting': return 'collecting';
|
|
case 'legalAction': return 'legal_action';
|
|
case 'collectionEnd': return 'collection_end';
|
|
default: return 'collecting';
|
|
}
|
|
}
|
|
|
|
function mapClientTypeToVendorType(clientType?: string | null): 'sales' | 'purchase' | 'both' {
|
|
switch (clientType) {
|
|
case 'customer': case 'sales': return 'sales';
|
|
case 'supplier': case 'purchase': return 'purchase';
|
|
default: return 'both';
|
|
}
|
|
}
|
|
|
|
function transformApiToFrontend(apiData: BadDebtApiData): BadDebtRecord {
|
|
const manager = apiData.assigned_user;
|
|
const firstBadDebt = apiData.bad_debts?.[0];
|
|
|
|
return {
|
|
id: String(apiData.id),
|
|
vendorId: String(apiData.client_id),
|
|
vendorCode: apiData.client_code || '',
|
|
vendorName: apiData.client_name || '거래처 없음',
|
|
businessNumber: apiData.business_no || '',
|
|
representativeName: '',
|
|
vendorType: mapClientTypeToVendorType(apiData.client_type),
|
|
businessType: '', businessCategory: '', zipCode: '',
|
|
address1: apiData.address || '', address2: '',
|
|
phone: apiData.phone || '', mobile: apiData.mobile || '',
|
|
fax: '', email: apiData.email || '',
|
|
contactName: apiData.contact_person || '', contactPhone: '', systemManager: '',
|
|
debtAmount: apiData.total_debt_amount || 0,
|
|
badDebtCount: apiData.bad_debt_count || 0,
|
|
status: mapApiStatusToFrontend(apiData.status),
|
|
overdueDays: apiData.max_overdue_days || 0,
|
|
overdueToggle: apiData.is_active,
|
|
occurrenceDate: firstBadDebt?.occurred_at || '',
|
|
endDate: null,
|
|
assignedManagerId: manager ? String(manager.id) : null,
|
|
assignedManager: manager ? {
|
|
id: String(manager.id), departmentName: '', name: manager.name, position: '', phone: '',
|
|
} : null,
|
|
settingToggle: apiData.is_active,
|
|
badDebts: (apiData.bad_debts || []).map(bd => ({
|
|
id: String(bd.id), debtAmount: bd.debt_amount || 0,
|
|
status: mapApiStatusToFrontend(bd.status), overdueDays: bd.overdue_days || 0,
|
|
isActive: bd.is_active, occurredAt: bd.occurred_at,
|
|
assignedManager: bd.assigned_user ? { id: String(bd.assigned_user.id), name: bd.assigned_user.name } : null,
|
|
})),
|
|
files: [], memos: [], createdAt: '', updatedAt: '',
|
|
};
|
|
}
|
|
|
|
function transformFrontendToApi(data: Partial<BadDebtRecord>): Record<string, unknown> {
|
|
return {
|
|
client_id: data.vendorId ? parseInt(data.vendorId) : null,
|
|
debt_amount: data.debtAmount || 0,
|
|
status: data.status ? mapFrontendStatusToApi(data.status) : 'collecting',
|
|
overdue_days: data.overdueDays || 0,
|
|
occurred_at: data.occurrenceDate || null,
|
|
closed_at: data.endDate || null,
|
|
assigned_manager_id: data.assignedManagerId ? parseInt(data.assignedManagerId) : null,
|
|
is_active: data.settingToggle ?? true,
|
|
note: null,
|
|
};
|
|
}
|
|
|
|
// ===== 악성채권 목록 조회 =====
|
|
export async function getBadDebts(params?: {
|
|
page?: number;
|
|
size?: number;
|
|
status?: string;
|
|
client_id?: string;
|
|
}): Promise<BadDebtRecord[]> {
|
|
const result = await executeServerAction({
|
|
url: buildApiUrl('/api/v1/bad-debts', {
|
|
page: params?.page,
|
|
size: params?.size,
|
|
status: params?.status && params.status !== 'all' ? mapFrontendStatusToApi(params.status as CollectionStatus) : undefined,
|
|
client_id: params?.client_id && params.client_id !== 'all' ? params.client_id : undefined,
|
|
}),
|
|
transform: (data: PaginatedApiResponse<BadDebtApiData>) => data.data.map(transformApiToFrontend),
|
|
errorMessage: '악성채권 목록 조회에 실패했습니다.',
|
|
});
|
|
return result.data || [];
|
|
}
|
|
|
|
// ===== 악성채권 상세 조회 =====
|
|
export async function getBadDebtById(id: string): Promise<BadDebtRecord | null> {
|
|
const result = await executeServerAction({
|
|
url: buildApiUrl(`/api/v1/bad-debts/${id}`),
|
|
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
|
errorMessage: '악성채권 조회에 실패했습니다.',
|
|
});
|
|
return result.data || null;
|
|
}
|
|
|
|
// ===== 악성채권 통계 조회 =====
|
|
export async function getBadDebtSummary(): Promise<BadDebtSummaryApiData | null> {
|
|
const result = await executeServerAction<BadDebtSummaryApiData>({
|
|
url: buildApiUrl('/api/v1/bad-debts/summary'),
|
|
errorMessage: '악성채권 통계 조회에 실패했습니다.',
|
|
});
|
|
return result.data || null;
|
|
}
|
|
|
|
// ===== 악성채권 등록 =====
|
|
export async function createBadDebt(
|
|
data: Partial<BadDebtRecord>
|
|
): Promise<ActionResult<BadDebtRecord>> {
|
|
const result = await executeServerAction({
|
|
url: buildApiUrl('/api/v1/bad-debts'),
|
|
method: 'POST',
|
|
body: transformFrontendToApi(data),
|
|
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
|
errorMessage: '악성채권 등록에 실패했습니다.',
|
|
});
|
|
if (result.success) revalidatePath('/accounting/bad-debt-collection');
|
|
return result;
|
|
}
|
|
|
|
// ===== 악성채권 수정 =====
|
|
export async function updateBadDebt(
|
|
id: string,
|
|
data: Partial<BadDebtRecord>
|
|
): Promise<ActionResult<BadDebtRecord>> {
|
|
const result = await executeServerAction({
|
|
url: buildApiUrl(`/api/v1/bad-debts/${id}`),
|
|
method: 'PUT',
|
|
body: transformFrontendToApi(data),
|
|
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
|
errorMessage: '악성채권 수정에 실패했습니다.',
|
|
});
|
|
if (result.success) revalidatePath('/accounting/bad-debt-collection');
|
|
return result;
|
|
}
|
|
|
|
// ===== 악성채권 삭제 =====
|
|
export async function deleteBadDebt(id: string): Promise<ActionResult> {
|
|
const result = await executeServerAction({
|
|
url: buildApiUrl(`/api/v1/bad-debts/${id}`),
|
|
method: 'DELETE',
|
|
errorMessage: '악성채권 삭제에 실패했습니다.',
|
|
});
|
|
if (result.success) revalidatePath('/accounting/bad-debt-collection');
|
|
return result;
|
|
}
|
|
|
|
// ===== 악성채권 활성화 토글 =====
|
|
export async function toggleBadDebt(id: string): Promise<ActionResult<BadDebtRecord>> {
|
|
const result = await executeServerAction({
|
|
url: buildApiUrl(`/api/v1/bad-debts/${id}/toggle`),
|
|
method: 'PATCH',
|
|
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
|
errorMessage: '상태 변경에 실패했습니다.',
|
|
});
|
|
if (result.success) revalidatePath('/accounting/bad-debt-collection');
|
|
return result;
|
|
}
|
|
|
|
// ===== 악성채권 메모 추가 =====
|
|
export async function addBadDebtMemo(
|
|
badDebtId: string,
|
|
content: string
|
|
): Promise<ActionResult<{ id: string; content: string; createdAt: string; createdBy: string }>> {
|
|
return executeServerAction({
|
|
url: buildApiUrl(`/api/v1/bad-debts/${badDebtId}/memos`),
|
|
method: 'POST',
|
|
body: { content },
|
|
transform: (memo: { id: number; content: string; created_at: string; created_by_user?: { name: string } | null }) => ({
|
|
id: String(memo.id),
|
|
content: memo.content,
|
|
createdAt: memo.created_at,
|
|
createdBy: memo.created_by_user?.name || '사용자',
|
|
}),
|
|
errorMessage: '메모 추가에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 악성채권 메모 삭제 =====
|
|
export async function deleteBadDebtMemo(
|
|
badDebtId: string,
|
|
memoId: string
|
|
): Promise<ActionResult> {
|
|
return executeServerAction({
|
|
url: buildApiUrl(`/api/v1/bad-debts/${badDebtId}/memos/${memoId}`),
|
|
method: 'DELETE',
|
|
errorMessage: '메모 삭제에 실패했습니다.',
|
|
});
|
|
}
|