feat: [tax-invoice] Mock→실제 API 연동 전환

- getTaxInvoices: executePaginatedAction으로 목록 조회
- createTaxInvoice: issue-direct API로 생성+즉시발행
- getSupplierSettings/saveSupplierSettings: 공급자 설정 API 연동
- getTaxInvoiceById: 상세 조회 API 연동
- API↔Frontend 간 데이터 변환 함수 추가
This commit is contained in:
김보곤
2026-02-21 08:31:43 +09:00
parent 6971336477
commit b28988f15f

View File

@@ -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 연동) =====