From b28988f15f5ddcef59a97132e0c64da60cf691dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 21 Feb 2026 08:31:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[tax-invoice]=20Mock=E2=86=92=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20API=20=EC=97=B0=EB=8F=99=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getTaxInvoices: executePaginatedAction으로 목록 조회 - createTaxInvoice: issue-direct API로 생성+즉시발행 - getSupplierSettings/saveSupplierSettings: 공급자 설정 API 연동 - getTaxInvoiceById: 상세 조회 API 연동 - API↔Frontend 간 데이터 변환 함수 추가 --- .../accounting/TaxInvoiceIssuance/actions.ts | 331 +++++++++++++----- 1 file changed, 240 insertions(+), 91 deletions(-) diff --git a/src/components/accounting/TaxInvoiceIssuance/actions.ts b/src/components/accounting/TaxInvoiceIssuance/actions.ts index bb0deeb4..af88cf65 100644 --- a/src/components/accounting/TaxInvoiceIssuance/actions.ts +++ b/src/components/accounting/TaxInvoiceIssuance/actions.ts @@ -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> | 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 { + 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> { - // TODO: 실제 API 연동 시 아래 코드로 교체 - // return executePaginatedAction({ - // 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> { + return executePaginatedAction({ + 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> { - // 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({ + url: buildApiUrl('/api/v1/tax-invoices/issue-direct'), + method: 'POST', + body: transformFrontendToApi(data), + transform: transformApiToFrontend, + errorMessage: '세금계산서 발행에 실패했습니다.', + }); } -// ===== 공급자 설정 조회 (Mock) ===== +// ===== 공급자 설정 조회 ===== export async function getSupplierSettings(): Promise> { - // 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, 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> { - // 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, 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> { - // TODO: 실제 API 연동 시 교체 - // return executeServerAction({ - // url: buildApiUrl(`/api/v1/tax-invoices/${id}`), - // transform: transformDetailApiToFrontend, - // errorMessage: '세금계산서 조회에 실패했습니다.', - // }); - return { success: false, error: '세금계산서를 찾을 수 없습니다.' }; + id: string +): Promise< + ActionResult +> { + 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 연동) =====