- 40+ actions.ts 파일을 fetchWrapper 패턴으로 마이그레이션 - 토큰 리프레시 캐싱 로직 추가 (refresh-token.ts) - ApiErrorContext 추가로 전역 에러 처리 개선 - HR EmployeeForm 컴포넌트 개선 - 참조함(ReferenceBox) 기능 수정 - juil 테스트 URL 페이지 추가 - claudedocs 문서 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
243 lines
6.6 KiB
TypeScript
243 lines
6.6 KiB
TypeScript
'use server';
|
|
|
|
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
|
import type { PaymentApiData, PaymentHistory } from './types';
|
|
import { transformApiToFrontend } from './utils';
|
|
|
|
// ===== 결제 목록 조회 =====
|
|
export async function getPayments(params?: {
|
|
page?: number;
|
|
perPage?: number;
|
|
status?: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
search?: string;
|
|
}): Promise<{
|
|
success: boolean;
|
|
data: PaymentHistory[];
|
|
pagination: {
|
|
currentPage: number;
|
|
lastPage: number;
|
|
perPage: number;
|
|
total: number;
|
|
};
|
|
error?: string;
|
|
__authError?: boolean;
|
|
}> {
|
|
try {
|
|
// 쿼리 파라미터 생성
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page) searchParams.append('page', String(params.page));
|
|
if (params?.perPage) searchParams.append('per_page', String(params.perPage));
|
|
if (params?.status) searchParams.append('status', params.status);
|
|
if (params?.startDate) searchParams.append('start_date', params.startDate);
|
|
if (params?.endDate) searchParams.append('end_date', params.endDate);
|
|
if (params?.search) searchParams.append('search', params.search);
|
|
|
|
const queryString = searchParams.toString();
|
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/payments${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const { response, error } = await serverFetch(url, {
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
});
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
data: [],
|
|
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
|
error: error.message,
|
|
__authError: error.code === 'UNAUTHORIZED',
|
|
};
|
|
}
|
|
|
|
if (!response) {
|
|
return {
|
|
success: false,
|
|
data: [],
|
|
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
|
error: '결제 내역을 불러오는데 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
data: [],
|
|
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
|
error: result.message || '결제 내역을 불러오는데 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const payments = result.data.data.map(transformApiToFrontend);
|
|
|
|
return {
|
|
success: true,
|
|
data: payments,
|
|
pagination: {
|
|
currentPage: result.data.current_page,
|
|
lastPage: result.data.last_page,
|
|
perPage: result.data.per_page,
|
|
total: result.data.total,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('[PaymentActions] getPayments error:', error);
|
|
return {
|
|
success: false,
|
|
data: [],
|
|
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
|
|
error: '서버 오류가 발생했습니다.',
|
|
};
|
|
}
|
|
}
|
|
|
|
// ===== 결제 명세서 조회 =====
|
|
export async function getPaymentStatement(id: string): Promise<{
|
|
success: boolean;
|
|
data?: {
|
|
statementNo: string;
|
|
issuedAt: string;
|
|
payment: {
|
|
id: number;
|
|
amount: number;
|
|
formattedAmount: string;
|
|
paymentMethod: string;
|
|
paymentMethodLabel: string;
|
|
transactionId: string | null;
|
|
status: string;
|
|
statusLabel: string;
|
|
paidAt: string | null;
|
|
memo: string | null;
|
|
};
|
|
subscription: {
|
|
id: number;
|
|
startedAt: string | null;
|
|
endedAt: string | null;
|
|
status: string;
|
|
statusLabel: string;
|
|
};
|
|
plan: {
|
|
id: number;
|
|
name: string;
|
|
code: string;
|
|
price: number;
|
|
billingCycle: string;
|
|
billingCycleLabel: string;
|
|
} | null;
|
|
customer: {
|
|
tenantId: number;
|
|
companyName: string;
|
|
businessNumber: string | null;
|
|
representative: string | null;
|
|
address: string | null;
|
|
email: string | null;
|
|
phone: string | null;
|
|
};
|
|
items: Array<{
|
|
description: string;
|
|
quantity: number;
|
|
unitPrice: number;
|
|
amount: number;
|
|
}>;
|
|
subtotal: number;
|
|
tax: number;
|
|
total: number;
|
|
};
|
|
error?: string;
|
|
__authError?: boolean;
|
|
}> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/payments/${id}/statement`,
|
|
{
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
__authError: error.code === 'UNAUTHORIZED',
|
|
};
|
|
}
|
|
|
|
if (!response) {
|
|
return {
|
|
success: false,
|
|
error: '명세서를 불러오는데 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '명세서를 불러오는데 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
// snake_case → camelCase 변환
|
|
const data = result.data;
|
|
return {
|
|
success: true,
|
|
data: {
|
|
statementNo: data.statement_no,
|
|
issuedAt: data.issued_at,
|
|
payment: {
|
|
id: data.payment.id,
|
|
amount: data.payment.amount,
|
|
formattedAmount: data.payment.formatted_amount,
|
|
paymentMethod: data.payment.payment_method,
|
|
paymentMethodLabel: data.payment.payment_method_label,
|
|
transactionId: data.payment.transaction_id,
|
|
status: data.payment.status,
|
|
statusLabel: data.payment.status_label,
|
|
paidAt: data.payment.paid_at,
|
|
memo: data.payment.memo,
|
|
},
|
|
subscription: {
|
|
id: data.subscription.id,
|
|
startedAt: data.subscription.started_at,
|
|
endedAt: data.subscription.ended_at,
|
|
status: data.subscription.status,
|
|
statusLabel: data.subscription.status_label,
|
|
},
|
|
plan: data.plan ? {
|
|
id: data.plan.id,
|
|
name: data.plan.name,
|
|
code: data.plan.code,
|
|
price: data.plan.price,
|
|
billingCycle: data.plan.billing_cycle,
|
|
billingCycleLabel: data.plan.billing_cycle_label,
|
|
} : null,
|
|
customer: {
|
|
tenantId: data.customer.tenant_id,
|
|
companyName: data.customer.company_name,
|
|
businessNumber: data.customer.business_number,
|
|
representative: data.customer.representative,
|
|
address: data.customer.address,
|
|
email: data.customer.email,
|
|
phone: data.customer.phone,
|
|
},
|
|
items: data.items,
|
|
subtotal: data.subtotal,
|
|
tax: data.tax,
|
|
total: data.total,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('[PaymentActions] getPaymentStatement error:', error);
|
|
return {
|
|
success: false,
|
|
error: '서버 오류가 발생했습니다.',
|
|
};
|
|
}
|
|
}
|