refactor(WEB): Server Action 공통화 및 보안 강화
- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,20 +14,13 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { BadDebtRecord, BadDebtItem, CollectionStatus } from './types';
|
||||
|
||||
// ============================================
|
||||
// API 응답 타입 정의
|
||||
// ============================================
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
// ===== API 응답 타입 =====
|
||||
|
||||
interface PaginatedResponse<T> {
|
||||
current_page: number;
|
||||
@@ -37,7 +30,6 @@ interface PaginatedResponse<T> {
|
||||
last_page: number;
|
||||
}
|
||||
|
||||
// API 개별 악성채권 타입
|
||||
interface BadDebtItemApiData {
|
||||
id: number;
|
||||
debt_amount: number;
|
||||
@@ -45,13 +37,9 @@ interface BadDebtItemApiData {
|
||||
overdue_days: number;
|
||||
is_active: boolean;
|
||||
occurred_at: string | null;
|
||||
assigned_user?: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
assigned_user?: { id: number; name: string } | null;
|
||||
}
|
||||
|
||||
// API 악성채권 데이터 타입 (거래처 기준)
|
||||
interface BadDebtApiData {
|
||||
id: number;
|
||||
client_id: number;
|
||||
@@ -64,23 +52,15 @@ interface BadDebtApiData {
|
||||
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;
|
||||
// 개별 악성채권 목록
|
||||
assigned_user?: { id: number; name: string } | null;
|
||||
bad_debts: BadDebtItemApiData[];
|
||||
}
|
||||
|
||||
// 통계 API 응답 타입
|
||||
interface BadDebtSummaryApiData {
|
||||
total_amount: number;
|
||||
collecting_amount: number;
|
||||
@@ -94,15 +74,8 @@ interface BadDebtSummaryApiData {
|
||||
bad_debt_count: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
// ===== 헬퍼 함수 =====
|
||||
|
||||
/**
|
||||
* API 상태 → 프론트엔드 상태 변환
|
||||
* API: legal_action, bad_debt (snake_case)
|
||||
* Frontend: legalAction, badDebt (camelCase)
|
||||
*/
|
||||
function mapApiStatusToFrontend(apiStatus: string): CollectionStatus {
|
||||
switch (apiStatus) {
|
||||
case 'collecting': return 'collecting';
|
||||
@@ -113,9 +86,6 @@ function mapApiStatusToFrontend(apiStatus: string): CollectionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 프론트엔드 상태 → API 상태 변환
|
||||
*/
|
||||
function mapFrontendStatusToApi(status: CollectionStatus): string {
|
||||
switch (status) {
|
||||
case 'collecting': return 'collecting';
|
||||
@@ -126,50 +96,31 @@ function mapFrontendStatusToApi(status: CollectionStatus): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API client_type → 프론트엔드 vendorType 변환
|
||||
*/
|
||||
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';
|
||||
case 'customer': case 'sales': return 'sales';
|
||||
case 'supplier': case 'purchase': return 'purchase';
|
||||
default: return 'both';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 데이터 → 프론트엔드 타입 변환 (거래처 기준)
|
||||
*/
|
||||
function transformApiToFrontend(apiData: BadDebtApiData): BadDebtRecord {
|
||||
const manager = apiData.assigned_user;
|
||||
const firstBadDebt = apiData.bad_debts?.[0];
|
||||
|
||||
return {
|
||||
id: String(apiData.id), // Client ID
|
||||
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: '',
|
||||
// 집계 데이터
|
||||
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),
|
||||
@@ -179,36 +130,19 @@ function transformApiToFrontend(apiData: BadDebtApiData): BadDebtRecord {
|
||||
endDate: null,
|
||||
assignedManagerId: manager ? String(manager.id) : null,
|
||||
assignedManager: manager ? {
|
||||
id: String(manager.id),
|
||||
departmentName: '',
|
||||
name: manager.name,
|
||||
position: '',
|
||||
phone: '',
|
||||
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,
|
||||
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: '',
|
||||
files: [], memos: [], createdAt: '', updatedAt: '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 프론트엔드 데이터 → API 요청 형식 변환
|
||||
*/
|
||||
function transformFrontendToApi(data: Partial<BadDebtRecord>): Record<string, unknown> {
|
||||
return {
|
||||
client_id: data.vendorId ? parseInt(data.vendorId) : null,
|
||||
@@ -223,382 +157,131 @@ function transformFrontendToApi(data: Partial<BadDebtRecord>): Record<string, un
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 호출 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 악성채권 목록 조회
|
||||
*/
|
||||
// ===== 악성채권 목록 조회 =====
|
||||
export async function getBadDebts(params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
status?: string;
|
||||
client_id?: string;
|
||||
}): Promise<BadDebtRecord[]> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', mapFrontendStatusToApi(params.status as CollectionStatus));
|
||||
}
|
||||
if (params?.client_id && params.client_id !== 'all') {
|
||||
searchParams.set('client_id', params.client_id);
|
||||
}
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts?${searchParams.toString()}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
console.error('[BadDebtActions] GET list error:', error.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[BadDebtActions] GET list error:', response?.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<BadDebtApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[BadDebtActions] No data in response');
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.data.data.map(transformApiToFrontend);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] getBadDebts error:', error);
|
||||
return [];
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', mapFrontendStatusToApi(params.status as CollectionStatus));
|
||||
}
|
||||
if (params?.client_id && params.client_id !== 'all') {
|
||||
searchParams.set('client_id', params.client_id);
|
||||
}
|
||||
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts?${searchParams.toString()}`,
|
||||
transform: (data: PaginatedResponse<BadDebtApiData>) => data.data.map(transformApiToFrontend),
|
||||
errorMessage: '악성채권 목록 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 상세 조회
|
||||
*/
|
||||
// ===== 악성채권 상세 조회 =====
|
||||
export async function getBadDebtById(id: string): Promise<BadDebtRecord | null> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('[BadDebtActions] GET detail error:', error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[BadDebtActions] GET detail error:', response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<BadDebtApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformApiToFrontend(result.data);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] getBadDebtById error:', error);
|
||||
return null;
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts/${id}`,
|
||||
transform: (data: BadDebtApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '악성채권 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 통계 조회
|
||||
*/
|
||||
// ===== 악성채권 통계 조회 =====
|
||||
export async function getBadDebtSummary(): Promise<BadDebtSummaryApiData | null> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/summary`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('[BadDebtActions] GET summary error:', error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[BadDebtActions] GET summary error:', response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<BadDebtSummaryApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] getBadDebtSummary error:', error);
|
||||
return null;
|
||||
}
|
||||
const result = await executeServerAction<BadDebtSummaryApiData>({
|
||||
url: `${API_URL}/api/v1/bad-debts/summary`,
|
||||
errorMessage: '악성채권 통계 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 등록
|
||||
*/
|
||||
// ===== 악성채권 등록 =====
|
||||
export async function createBadDebt(
|
||||
data: Partial<BadDebtRecord>
|
||||
): Promise<{ success: boolean; data?: BadDebtRecord; error?: string }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[BadDebtActions] POST request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || '악성채권 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
revalidatePath('/accounting/bad-debt-collection');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] createBadDebt error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult<BadDebtRecord>> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/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<{ success: boolean; data?: BadDebtRecord; error?: string }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
console.log('[BadDebtActions] PUT request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || '악성채권 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
revalidatePath('/accounting/bad-debt-collection');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] updateBadDebt error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult<BadDebtRecord>> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/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<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || '악성채권 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
revalidatePath('/accounting/bad-debt-collection');
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] deleteBadDebt error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
// ===== 악성채권 삭제 =====
|
||||
export async function deleteBadDebt(id: string): Promise<ActionResult> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/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<{ success: boolean; data?: BadDebtRecord; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}/toggle`,
|
||||
{ method: 'PATCH' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || '상태 변경에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
revalidatePath('/accounting/bad-debt-collection');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] toggleBadDebt error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
// ===== 악성채권 활성화 토글 =====
|
||||
export async function toggleBadDebt(id: string): Promise<ActionResult<BadDebtRecord>> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/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<{ success: boolean; data?: { id: string; content: string; createdAt: string; createdBy: string }; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${badDebtId}/memos`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || '메모 추가에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const memo = result.data;
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: String(memo.id),
|
||||
content: memo.content,
|
||||
createdAt: memo.created_at,
|
||||
createdBy: memo.created_by_user?.name || '사용자',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] addBadDebtMemo error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult<{ id: string; content: string; createdAt: string; createdBy: string }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/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<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${badDebtId}/memos/${memoId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
const result = await response?.json();
|
||||
|
||||
if (!response?.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || '메모 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BadDebtActions] deleteBadDebtMemo error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/bad-debts/${badDebtId}/memos/${memoId}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '메모 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user