feat: [tax-invoice] Mock→실제 API 연동 전환
- getTaxInvoices: executePaginatedAction으로 목록 조회 - createTaxInvoice: issue-direct API로 생성+즉시발행 - getSupplierSettings/saveSupplierSettings: 공급자 설정 API 연동 - getTaxInvoiceById: 상세 조회 API 연동 - API↔Frontend 간 데이터 변환 함수 추가
This commit is contained in:
@@ -1,28 +1,141 @@
|
||||
/**
|
||||
* 세금계산서 발행 서버 액션 (Mock)
|
||||
* 세금계산서 발행 서버 액션
|
||||
*
|
||||
* API Endpoints (예정):
|
||||
* API Endpoints:
|
||||
* - GET /api/v1/tax-invoices - 목록 조회
|
||||
* - POST /api/v1/tax-invoices - 발행
|
||||
* - POST /api/v1/tax-invoices - 세금계산서 생성 (draft)
|
||||
* - POST /api/v1/tax-invoices/issue-direct - 생성 + 즉시 발행
|
||||
* - GET /api/v1/tax-invoices/{id} - 상세 조회
|
||||
* - GET /api/v1/tax-invoices/supplier-settings - 공급자 설정 조회
|
||||
* - PUT /api/v1/tax-invoices/supplier-settings - 공급자 설정 저장
|
||||
* - GET /api/v1/tax-invoices/vendors - 거래처 검색
|
||||
* - GET /api/v1/clients - 거래처 검색
|
||||
*/
|
||||
|
||||
'use server';
|
||||
|
||||
import type { ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import type { PaginatedActionResult } from '@/lib/api/execute-paginated-action';
|
||||
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type {
|
||||
TaxInvoiceRecord,
|
||||
TaxInvoiceFormData,
|
||||
TaxInvoiceStatus,
|
||||
SupplierSettings,
|
||||
VendorSearchItem,
|
||||
} from './types';
|
||||
|
||||
// ===== 세금계산서 목록 조회 (Mock) =====
|
||||
export async function getTaxInvoices(_params?: {
|
||||
// ===== API → Frontend 변환 =====
|
||||
|
||||
interface TaxInvoiceApiData {
|
||||
id: number;
|
||||
nts_confirm_num: string | null;
|
||||
invoice_type: string;
|
||||
issue_type: string;
|
||||
direction: string;
|
||||
supplier_corp_num: string;
|
||||
supplier_corp_name: string;
|
||||
supplier_ceo_name: string | null;
|
||||
supplier_addr: string | null;
|
||||
supplier_biz_type: string | null;
|
||||
supplier_biz_class: string | null;
|
||||
supplier_contact_id: string | null;
|
||||
buyer_corp_num: string;
|
||||
buyer_corp_name: string;
|
||||
buyer_ceo_name: string | null;
|
||||
buyer_addr: string | null;
|
||||
buyer_biz_type: string | null;
|
||||
buyer_biz_class: string | null;
|
||||
buyer_contact_id: string | null;
|
||||
issue_date: string;
|
||||
supply_amount: string | number;
|
||||
tax_amount: string | number;
|
||||
total_amount: string | number;
|
||||
items: Array<Record<string, unknown>> | null;
|
||||
status: string;
|
||||
nts_send_status: string | null;
|
||||
issued_at: string | null;
|
||||
sent_at: string | null;
|
||||
cancelled_at: string | null;
|
||||
barobill_invoice_id: string | null;
|
||||
description: string | null;
|
||||
error_message: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
function mapApiStatusToFrontend(status: string): TaxInvoiceStatus {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return 'draft';
|
||||
case 'issued':
|
||||
return 'issued';
|
||||
case 'sent':
|
||||
return 'nts_sent';
|
||||
case 'failed':
|
||||
case 'cancelled':
|
||||
return 'error';
|
||||
default:
|
||||
return 'draft';
|
||||
}
|
||||
}
|
||||
|
||||
function transformApiToFrontend(raw: TaxInvoiceApiData): TaxInvoiceRecord {
|
||||
return {
|
||||
id: String(raw.id),
|
||||
invoiceNumber: raw.nts_confirm_num || `TI-${raw.id}`,
|
||||
vendorName: raw.direction === 'sales' ? raw.buyer_corp_name : raw.supplier_corp_name,
|
||||
vendorBusinessNumber: raw.direction === 'sales' ? raw.buyer_corp_num : raw.supplier_corp_num,
|
||||
writeDate: raw.issue_date || '',
|
||||
sendDate: raw.sent_at || null,
|
||||
supplyAmount: Number(raw.supply_amount) || 0,
|
||||
taxAmount: Number(raw.tax_amount) || 0,
|
||||
totalAmount: Number(raw.total_amount) || 0,
|
||||
status: mapApiStatusToFrontend(raw.status),
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Frontend → API 변환 =====
|
||||
|
||||
function transformFrontendToApi(data: TaxInvoiceFormData): Record<string, unknown> {
|
||||
return {
|
||||
invoice_type: 'tax_invoice',
|
||||
issue_type: 'normal',
|
||||
direction: 'sales',
|
||||
supplier_corp_num: data.supplier.businessNumber,
|
||||
supplier_corp_name: data.supplier.companyName,
|
||||
supplier_ceo_name: data.supplier.representativeName || null,
|
||||
supplier_addr: data.supplier.address || null,
|
||||
supplier_biz_type: data.supplier.businessType || null,
|
||||
supplier_biz_class: data.supplier.businessItem || null,
|
||||
supplier_contact_id: data.supplier.contactEmail || null,
|
||||
buyer_corp_num: data.receiver.businessNumber,
|
||||
buyer_corp_name: data.receiver.companyName,
|
||||
buyer_ceo_name: data.receiver.representativeName || null,
|
||||
buyer_addr: data.receiver.address || null,
|
||||
buyer_biz_type: data.receiver.businessType || null,
|
||||
buyer_biz_class: data.receiver.businessItem || null,
|
||||
buyer_contact_id: data.receiver.contactEmail || null,
|
||||
issue_date: data.writeDate,
|
||||
supply_amount: data.items.reduce((s, i) => s + i.supplyAmount, 0),
|
||||
tax_amount: data.items.reduce((s, i) => s + i.taxAmount, 0),
|
||||
items: data.items
|
||||
.filter((i) => i.itemName)
|
||||
.map((item) => ({
|
||||
name: item.itemName,
|
||||
spec: item.specification,
|
||||
qty: item.quantity,
|
||||
unit_price: item.unitPrice,
|
||||
supply_amt: item.supplyAmount,
|
||||
tax_amt: item.taxAmount,
|
||||
remark: '',
|
||||
})),
|
||||
description: data.memo || null,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 세금계산서 목록 조회 =====
|
||||
export async function getTaxInvoices(params?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
dateType?: string;
|
||||
@@ -32,105 +145,141 @@ export async function getTaxInvoices(_params?: {
|
||||
status?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
}): Promise<ActionResult<TaxInvoiceRecord[]>> {
|
||||
// TODO: 실제 API 연동 시 아래 코드로 교체
|
||||
// return executePaginatedAction<TaxInvoiceApiData, TaxInvoiceRecord>({
|
||||
// url: buildApiUrl('/api/v1/tax-invoices', {
|
||||
// page: params?.page,
|
||||
// per_page: params?.perPage,
|
||||
// date_type: params?.dateType,
|
||||
// start_date: params?.startDate,
|
||||
// end_date: params?.endDate,
|
||||
// vendor_search: params?.vendorSearch,
|
||||
// status: params?.status !== 'all' ? params?.status : undefined,
|
||||
// sort_by: params?.sortBy,
|
||||
// sort_order: params?.sortOrder,
|
||||
// }),
|
||||
// transform: transformApiToFrontend,
|
||||
// errorMessage: '세금계산서 목록 조회에 실패했습니다.',
|
||||
// });
|
||||
return { success: true, data: [] };
|
||||
}): Promise<PaginatedActionResult<TaxInvoiceRecord>> {
|
||||
return executePaginatedAction<TaxInvoiceApiData, TaxInvoiceRecord>({
|
||||
url: buildApiUrl('/api/v1/tax-invoices', {
|
||||
page: params?.page,
|
||||
per_page: params?.perPage,
|
||||
issue_date_from: params?.startDate,
|
||||
issue_date_to: params?.endDate,
|
||||
corp_name: params?.vendorSearch,
|
||||
status: params?.status !== 'all' ? params?.status : undefined,
|
||||
}),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '세금계산서 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 세금계산서 발행 (Mock) =====
|
||||
// ===== 세금계산서 발행 (생성 + 즉시 발행) =====
|
||||
export async function createTaxInvoice(
|
||||
_data: TaxInvoiceFormData
|
||||
data: TaxInvoiceFormData
|
||||
): Promise<ActionResult<TaxInvoiceRecord>> {
|
||||
// TODO: 실제 API 연동 시 아래 코드로 교체
|
||||
// return executeServerAction({
|
||||
// url: buildApiUrl('/api/v1/tax-invoices'),
|
||||
// method: 'POST',
|
||||
// body: transformFrontendToApi(data),
|
||||
// transform: transformApiToFrontend,
|
||||
// errorMessage: '세금계산서 발행에 실패했습니다.',
|
||||
// });
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: crypto.randomUUID(),
|
||||
invoiceNumber: `TI-${Date.now()}`,
|
||||
vendorName: _data.receiver.companyName,
|
||||
vendorBusinessNumber: _data.receiver.businessNumber,
|
||||
writeDate: _data.writeDate,
|
||||
sendDate: null,
|
||||
supplyAmount: _data.items.reduce((sum, item) => sum + item.supplyAmount, 0),
|
||||
taxAmount: _data.items.reduce((sum, item) => sum + item.taxAmount, 0),
|
||||
totalAmount: _data.items.reduce((sum, item) => sum + item.totalAmount, 0),
|
||||
status: 'draft',
|
||||
},
|
||||
};
|
||||
return executeServerAction<TaxInvoiceApiData, TaxInvoiceRecord>({
|
||||
url: buildApiUrl('/api/v1/tax-invoices/issue-direct'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: transformApiToFrontend,
|
||||
errorMessage: '세금계산서 발행에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 공급자 설정 조회 (Mock) =====
|
||||
// ===== 공급자 설정 조회 =====
|
||||
export async function getSupplierSettings(): Promise<ActionResult<SupplierSettings>> {
|
||||
// TODO: 실제 API 연동 시 아래 코드로 교체
|
||||
// return executeServerAction({
|
||||
// url: buildApiUrl('/api/v1/tax-invoices/supplier-settings'),
|
||||
// transform: transformSupplierSettings,
|
||||
// errorMessage: '공급자 설정 조회에 실패했습니다.',
|
||||
// });
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
businessNumber: '1231212345',
|
||||
companyName: '상호명',
|
||||
representativeName: '홍길동',
|
||||
address: '주소명',
|
||||
businessType: '업태명',
|
||||
businessItem: '종목명',
|
||||
contactName: '홍길동',
|
||||
contactPhone: '02-123-1234',
|
||||
contactEmail: 'abc@email.com',
|
||||
},
|
||||
};
|
||||
return executeServerAction<Record<string, string | null>, SupplierSettings>({
|
||||
url: buildApiUrl('/api/v1/tax-invoices/supplier-settings'),
|
||||
transform: (data) => ({
|
||||
businessNumber: data.business_number || '',
|
||||
companyName: data.company_name || '',
|
||||
representativeName: data.representative_name || '',
|
||||
address: data.address || '',
|
||||
businessType: data.business_type || '',
|
||||
businessItem: data.business_item || '',
|
||||
contactName: data.contact_name || '',
|
||||
contactPhone: data.contact_phone || '',
|
||||
contactEmail: data.contact_email || '',
|
||||
}),
|
||||
errorMessage: '공급자 설정 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 공급자 설정 저장 (Mock) =====
|
||||
// ===== 공급자 설정 저장 =====
|
||||
export async function saveSupplierSettings(
|
||||
_data: SupplierSettings
|
||||
data: SupplierSettings
|
||||
): Promise<ActionResult<SupplierSettings>> {
|
||||
// TODO: 실제 API 연동 시 아래 코드로 교체
|
||||
// return executeServerAction({
|
||||
// url: buildApiUrl('/api/v1/tax-invoices/supplier-settings'),
|
||||
// method: 'PUT',
|
||||
// body: transformSupplierSettingsToApi(data),
|
||||
// transform: transformSupplierSettings,
|
||||
// errorMessage: '공급자 설정 저장에 실패했습니다.',
|
||||
// });
|
||||
return { success: true, data: _data };
|
||||
return executeServerAction<Record<string, string | null>, SupplierSettings>({
|
||||
url: buildApiUrl('/api/v1/tax-invoices/supplier-settings'),
|
||||
method: 'PUT',
|
||||
body: {
|
||||
business_number: data.businessNumber,
|
||||
company_name: data.companyName,
|
||||
representative_name: data.representativeName,
|
||||
address: data.address,
|
||||
business_type: data.businessType,
|
||||
business_item: data.businessItem,
|
||||
contact_name: data.contactName,
|
||||
contact_phone: data.contactPhone,
|
||||
contact_email: data.contactEmail,
|
||||
},
|
||||
transform: (res) => ({
|
||||
businessNumber: res.business_number || '',
|
||||
companyName: res.company_name || '',
|
||||
representativeName: res.representative_name || '',
|
||||
address: res.address || '',
|
||||
businessType: res.business_type || '',
|
||||
businessItem: res.business_item || '',
|
||||
contactName: res.contact_name || '',
|
||||
contactPhone: res.contact_phone || '',
|
||||
contactEmail: res.contact_email || '',
|
||||
}),
|
||||
errorMessage: '공급자 설정 저장에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 세금계산서 상세 조회 (Mock) =====
|
||||
// ===== 세금계산서 상세 조회 =====
|
||||
export async function getTaxInvoiceById(
|
||||
_id: string
|
||||
): Promise<ActionResult<TaxInvoiceFormData & { id: string; invoiceNumber: string; status: TaxInvoiceRecord['status'] }>> {
|
||||
// TODO: 실제 API 연동 시 교체
|
||||
// return executeServerAction({
|
||||
// url: buildApiUrl(`/api/v1/tax-invoices/${id}`),
|
||||
// transform: transformDetailApiToFrontend,
|
||||
// errorMessage: '세금계산서 조회에 실패했습니다.',
|
||||
// });
|
||||
return { success: false, error: '세금계산서를 찾을 수 없습니다.' };
|
||||
id: string
|
||||
): Promise<
|
||||
ActionResult<TaxInvoiceFormData & { id: string; invoiceNumber: string; status: TaxInvoiceRecord['status'] }>
|
||||
> {
|
||||
return executeServerAction<
|
||||
TaxInvoiceApiData,
|
||||
TaxInvoiceFormData & { id: string; invoiceNumber: string; status: TaxInvoiceRecord['status'] }
|
||||
>({
|
||||
url: buildApiUrl(`/api/v1/tax-invoices/${id}`),
|
||||
transform: (raw) => ({
|
||||
id: String(raw.id),
|
||||
invoiceNumber: raw.nts_confirm_num || `TI-${raw.id}`,
|
||||
status: mapApiStatusToFrontend(raw.status),
|
||||
supplier: {
|
||||
businessNumber: raw.supplier_corp_num || '',
|
||||
companyName: raw.supplier_corp_name || '',
|
||||
representativeName: raw.supplier_ceo_name || '',
|
||||
address: raw.supplier_addr || '',
|
||||
businessType: raw.supplier_biz_type || '',
|
||||
businessItem: raw.supplier_biz_class || '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
contactEmail: raw.supplier_contact_id || '',
|
||||
},
|
||||
receiver: {
|
||||
businessNumber: raw.buyer_corp_num || '',
|
||||
companyName: raw.buyer_corp_name || '',
|
||||
representativeName: raw.buyer_ceo_name || '',
|
||||
address: raw.buyer_addr || '',
|
||||
businessType: raw.buyer_biz_type || '',
|
||||
businessItem: raw.buyer_biz_class || '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
contactEmail: raw.buyer_contact_id || '',
|
||||
},
|
||||
writeDate: raw.issue_date || '',
|
||||
items: (raw.items || []).map((item, idx) => ({
|
||||
id: String(idx),
|
||||
month: '',
|
||||
day: '',
|
||||
itemName: String(item.name || ''),
|
||||
specification: String(item.spec || ''),
|
||||
quantity: Number(item.qty) || 0,
|
||||
unitPrice: Number(item.unit_price) || 0,
|
||||
supplyAmount: Number(item.supply_amt) || 0,
|
||||
taxAmount: Number(item.tax_amt) || 0,
|
||||
totalAmount: (Number(item.supply_amt) || 0) + (Number(item.tax_amt) || 0),
|
||||
taxType: 'taxable' as const,
|
||||
})),
|
||||
memo: raw.description || '',
|
||||
}),
|
||||
errorMessage: '세금계산서 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 거래처 검색 (/api/v1/clients 연동) =====
|
||||
|
||||
Reference in New Issue
Block a user