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:
@@ -1,10 +1,11 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type { ExpectedExpenseRecord, TransactionType, PaymentStatus, ApprovalStatus } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== API 응답 타입 =====
|
||||
interface ExpectedExpenseApiData {
|
||||
id: number;
|
||||
@@ -22,18 +23,12 @@ interface ExpectedExpenseApiData {
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
client?: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
bank_account?: {
|
||||
id: number;
|
||||
bank_name: string;
|
||||
account_name: string;
|
||||
} | null;
|
||||
client?: { id: number; name: string } | null;
|
||||
bank_account?: { id: number; bank_name: string; account_name: string } | null;
|
||||
}
|
||||
|
||||
interface PaginationMeta {
|
||||
interface ExpensePaginatedResponse {
|
||||
data: ExpectedExpenseApiData[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
@@ -48,6 +43,9 @@ interface SummaryData {
|
||||
by_month: Record<string, { count: number; amount: number }>;
|
||||
}
|
||||
|
||||
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
|
||||
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 50, total: 0 };
|
||||
|
||||
// ===== API → Frontend 변환 =====
|
||||
function transformApiToFrontend(apiData: ExpectedExpenseApiData): ExpectedExpenseRecord {
|
||||
return {
|
||||
@@ -55,14 +53,10 @@ function transformApiToFrontend(apiData: ExpectedExpenseApiData): ExpectedExpens
|
||||
expectedPaymentDate: apiData.expected_payment_date,
|
||||
settlementDate: apiData.settlement_date || '',
|
||||
transactionType: (apiData.transaction_type || 'other') as TransactionType,
|
||||
amount: typeof apiData.amount === 'string'
|
||||
? parseFloat(apiData.amount)
|
||||
: apiData.amount,
|
||||
amount: typeof apiData.amount === 'string' ? parseFloat(apiData.amount) : apiData.amount,
|
||||
vendorId: apiData.client_id ? String(apiData.client_id) : '',
|
||||
vendorName: apiData.client_name || apiData.client?.name || '',
|
||||
bankAccount: apiData.bank_account
|
||||
? `${apiData.bank_account.bank_name} ${apiData.bank_account.account_name}`
|
||||
: '',
|
||||
bankAccount: apiData.bank_account ? `${apiData.bank_account.bank_name} ${apiData.bank_account.account_name}` : '',
|
||||
accountSubject: apiData.account_code || '',
|
||||
paymentStatus: (apiData.payment_status || 'pending') as PaymentStatus,
|
||||
approvalStatus: (apiData.approval_status || 'none') as ApprovalStatus,
|
||||
@@ -75,7 +69,6 @@ function transformApiToFrontend(apiData: ExpectedExpenseApiData): ExpectedExpens
|
||||
// ===== Frontend → API 변환 =====
|
||||
function transformFrontendToApi(data: Partial<ExpectedExpenseRecord>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
if (data.expectedPaymentDate !== undefined) result.expected_payment_date = data.expectedPaymentDate;
|
||||
if (data.settlementDate !== undefined) result.settlement_date = data.settlementDate || null;
|
||||
if (data.transactionType !== undefined) result.transaction_type = data.transactionType;
|
||||
@@ -86,518 +79,152 @@ function transformFrontendToApi(data: Partial<ExpectedExpenseRecord>): Record<st
|
||||
if (data.paymentStatus !== undefined) result.payment_status = data.paymentStatus;
|
||||
if (data.approvalStatus !== undefined) result.approval_status = data.approvalStatus;
|
||||
if (data.note !== undefined) result.description = data.note || null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== 미지급비용 목록 조회 =====
|
||||
export async function getExpectedExpenses(params?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
transactionType?: string;
|
||||
paymentStatus?: string;
|
||||
approvalStatus?: string;
|
||||
clientId?: string;
|
||||
search?: string;
|
||||
sortBy?: string;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data: ExpectedExpenseRecord[];
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
page?: number; perPage?: number; startDate?: string; endDate?: string;
|
||||
transactionType?: string; paymentStatus?: string; approvalStatus?: string;
|
||||
clientId?: string; search?: string; sortBy?: string; sortDir?: 'asc' | 'desc';
|
||||
}): Promise<{ success: boolean; data: ExpectedExpenseRecord[]; pagination: FrontendPagination; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.transactionType && params.transactionType !== 'all') searchParams.set('transaction_type', params.transactionType);
|
||||
if (params?.paymentStatus && params.paymentStatus !== 'all') searchParams.set('payment_status', params.paymentStatus);
|
||||
if (params?.approvalStatus && params.approvalStatus !== 'all') searchParams.set('approval_status', params.approvalStatus);
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.transactionType && params.transactionType !== 'all') {
|
||||
searchParams.set('transaction_type', params.transactionType);
|
||||
}
|
||||
if (params?.paymentStatus && params.paymentStatus !== 'all') {
|
||||
searchParams.set('payment_status', params.paymentStatus);
|
||||
}
|
||||
if (params?.approvalStatus && params.approvalStatus !== 'all') {
|
||||
searchParams.set('approval_status', params.approvalStatus);
|
||||
}
|
||||
if (params?.clientId) searchParams.set('client_id', params.clientId);
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 50, total: 0 },
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[ExpectedExpenseActions] GET expected-expenses error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 50, total: 0 },
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 50, total: 0 },
|
||||
error: result.message || '미지급비용 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const paginationData = result.data;
|
||||
const expenses = (paginationData?.data || []).map(transformApiToFrontend);
|
||||
const meta: PaginationMeta = {
|
||||
current_page: paginationData?.current_page || 1,
|
||||
last_page: paginationData?.last_page || 1,
|
||||
per_page: paginationData?.per_page || 50,
|
||||
total: paginationData?.total || expenses.length,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: expenses,
|
||||
pagination: {
|
||||
currentPage: meta.current_page,
|
||||
lastPage: meta.last_page,
|
||||
perPage: meta.per_page,
|
||||
total: meta.total,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] getExpectedExpenses error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { currentPage: 1, lastPage: 1, perPage: 50, total: 0 },
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: ExpensePaginatedResponse) => ({
|
||||
items: (data?.data || []).map(transformApiToFrontend),
|
||||
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 50, total: data?.total || 0 },
|
||||
}),
|
||||
errorMessage: '미지급비용 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 미지급비용 상세 조회 =====
|
||||
export async function getExpectedExpenseById(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: ExpectedExpenseRecord;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${id}`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.error('[ExpectedExpenseActions] GET expected-expense error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '미지급비용 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] getExpectedExpenseById error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function getExpectedExpenseById(id: string): Promise<ActionResult<ExpectedExpenseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses/${id}`,
|
||||
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '미지급비용 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 미지급비용 등록 =====
|
||||
export async function createExpectedExpense(
|
||||
data: Partial<ExpectedExpenseRecord>
|
||||
): Promise<{ success: boolean; data?: ExpectedExpenseRecord; error?: string }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses`,
|
||||
{
|
||||
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 || '미지급비용 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] createExpectedExpense error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function createExpectedExpense(data: Partial<ExpectedExpenseRecord>): Promise<ActionResult<ExpectedExpenseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses`,
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '미지급비용 등록에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 미지급비용 수정 =====
|
||||
export async function updateExpectedExpense(
|
||||
id: string,
|
||||
data: Partial<ExpectedExpenseRecord>
|
||||
): Promise<{ success: boolean; data?: ExpectedExpenseRecord; error?: string }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${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 || '미지급비용 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] updateExpectedExpense error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function updateExpectedExpense(id: string, data: Partial<ExpectedExpenseRecord>): Promise<ActionResult<ExpectedExpenseRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses/${id}`,
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '미지급비용 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 미지급비용 삭제 =====
|
||||
export async function deleteExpectedExpense(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${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 || '미지급비용 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] deleteExpectedExpense error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function deleteExpectedExpense(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '미지급비용 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 미지급비용 일괄 삭제 =====
|
||||
export async function deleteExpectedExpenses(ids: string[]): Promise<{
|
||||
success: boolean;
|
||||
deletedCount?: number;
|
||||
error?: string;
|
||||
success: boolean; deletedCount?: number; error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
deletedCount: result.data?.deleted_count,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] deleteExpectedExpenses error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses`,
|
||||
method: 'DELETE',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
transform: (data: { deleted_count?: number }) => ({ deletedCount: data?.deleted_count }),
|
||||
errorMessage: '미지급비용 일괄 삭제에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, deletedCount: result.data?.deletedCount, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 예상 지급일 일괄 변경 =====
|
||||
export async function updateExpectedPaymentDate(
|
||||
ids: string[],
|
||||
expectedPaymentDate: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
updatedCount?: number;
|
||||
error?: string;
|
||||
export async function updateExpectedPaymentDate(ids: string[], expectedPaymentDate: string): Promise<{
|
||||
success: boolean; updatedCount?: number; error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/update-payment-date`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
expected_payment_date: expectedPaymentDate,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
updatedCount: result.data?.updated_count,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] updateExpectedPaymentDate error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/expected-expenses/update-payment-date`,
|
||||
method: 'PUT',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)), expected_payment_date: expectedPaymentDate },
|
||||
transform: (data: { updated_count?: number }) => ({ updatedCount: data?.updated_count }),
|
||||
errorMessage: '예상 지급일 변경에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, updatedCount: result.data?.updatedCount, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 미지급비용 요약 조회 =====
|
||||
export async function getExpectedExpenseSummary(params?: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
paymentStatus?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: SummaryData;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
startDate?: string; endDate?: string; paymentStatus?: string;
|
||||
}): Promise<ActionResult<SummaryData>> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.paymentStatus && params.paymentStatus !== 'all') searchParams.set('payment_status', params.paymentStatus);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
if (params?.paymentStatus && params.paymentStatus !== 'all') {
|
||||
searchParams.set('payment_status', params.paymentStatus);
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/summary${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
console.warn('[ExpectedExpenseActions] GET summary error:', response?.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response?.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '요약 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] getExpectedExpenseSummary error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
return executeServerAction<SummaryData>({
|
||||
url: `${API_URL}/api/v1/expected-expenses/summary${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '요약 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 거래처 목록 조회 =====
|
||||
export async function getClients(): Promise<{
|
||||
success: boolean;
|
||||
data: { id: string; name: string }[];
|
||||
error?: string;
|
||||
success: boolean; data: { id: string; name: string }[]; error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const clients = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: clients.map((c: { id: number; name: string }) => ({
|
||||
id: String(c.id),
|
||||
name: c.name,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] getClients error:', error);
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/clients?per_page=100`,
|
||||
transform: (data: { data?: { id: number; name: string }[] } | { id: number; name: string }[]) => {
|
||||
type ClientApi = { id: number; name: string };
|
||||
const clients: ClientApi[] = Array.isArray(data) ? data : (data as { data?: ClientApi[] })?.data || [];
|
||||
return clients.map(c => ({ id: String(c.id), name: c.name }));
|
||||
},
|
||||
errorMessage: '거래처 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data || [], error: result.error };
|
||||
}
|
||||
|
||||
// ===== 은행 계좌 목록 조회 =====
|
||||
export async function getBankAccounts(): Promise<{
|
||||
success: boolean;
|
||||
data: { id: string; bankName: string; accountName: string; accountNumber: string }[];
|
||||
error?: string;
|
||||
success: boolean; data: { id: string; bankName: string; accountName: string; accountNumber: string }[]; error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const accounts = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: accounts.map((a: { id: number; bank_name: string; account_name: string; account_number: string }) => ({
|
||||
id: String(a.id),
|
||||
bankName: a.bank_name,
|
||||
accountName: a.account_name,
|
||||
accountNumber: a.account_number,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[ExpectedExpenseActions] getBankAccounts error:', error);
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/bank-accounts?per_page=100`,
|
||||
transform: (data: { data?: { id: number; bank_name: string; account_name: string; account_number: string }[] } | { id: number; bank_name: string; account_name: string; account_number: string }[]) => {
|
||||
type AccountApi = { id: number; bank_name: string; account_name: string; account_number: string };
|
||||
const accounts: AccountApi[] = Array.isArray(data) ? data : (data as { data?: AccountApi[] })?.data || [];
|
||||
return accounts.map(a => ({ id: String(a.id), bankName: a.bank_name, accountName: a.account_name, accountNumber: a.account_number }));
|
||||
},
|
||||
errorMessage: '은행 계좌 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data || [], error: result.error };
|
||||
}
|
||||
Reference in New Issue
Block a user