/** * 견적 관리 서버 액션 * * API Endpoints: * - GET /api/v1/quotes - 목록 조회 * - GET /api/v1/quotes/{id} - 상세 조회 * - POST /api/v1/quotes - 등록 * - PUT /api/v1/quotes/{id} - 수정 * - DELETE /api/v1/quotes/{id} - 삭제 * - DELETE /api/v1/quotes/bulk - 일괄 삭제 * - POST /api/v1/quotes/{id}/finalize - 최종 확정 * - POST /api/v1/quotes/{id}/cancel-finalize - 확정 취소 * - POST /api/v1/quotes/{id}/convert - 수주 전환 * - GET /api/v1/quotes/number/preview - 견적번호 미리보기 * - POST /api/v1/quotes/{id}/pdf - PDF 생성 * - POST /api/v1/quotes/{id}/send/email - 이메일 발송 * - POST /api/v1/quotes/{id}/send/kakao - 카카오 발송 */ 'use server'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import { executeServerAction } from '@/lib/api/execute-server-action'; import { executePaginatedAction } from '@/lib/api/execute-paginated-action'; import { buildApiUrl } from '@/lib/api/query-params'; import type { Quote, QuoteApiData, QuoteListParams, BomCalculationResult, } from './types'; import { transformApiToFrontend } from './types'; // ===== 견적 목록 조회 ===== export async function getQuotes(params?: QuoteListParams) { return executePaginatedAction({ url: buildApiUrl('/api/v1/quotes', { page: params?.page, size: params?.perPage, q: params?.search, status: params?.status, product_category: params?.productCategory, client_id: params?.clientId, date_from: params?.dateFrom, date_to: params?.dateTo, sort_by: params?.sortBy, sort_order: params?.sortOrder, }), transform: transformApiToFrontend, errorMessage: '견적 목록 조회에 실패했습니다.', }); } // ===== 견적 상세 조회 ===== export async function getQuoteById(id: string): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}`), transform: (data: QuoteApiData) => transformApiToFrontend(data), errorMessage: '견적 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } // ===== 견적 등록 ===== export async function createQuote( data: Record ): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl('/api/v1/quotes'), method: 'POST', body: data, transform: (d: QuoteApiData) => transformApiToFrontend(d), errorMessage: '견적 등록에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } // ===== 견적 수정 ===== export async function updateQuote( id: string, data: Record ): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}`), method: 'PUT', body: data, transform: (d: QuoteApiData) => transformApiToFrontend(d), errorMessage: '견적 수정에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } // ===== 견적 삭제 ===== export async function deleteQuote(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}`), method: 'DELETE', errorMessage: '견적 삭제에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, error: result.error }; } // ===== 견적 일괄 삭제 ===== export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl('/api/v1/quotes/bulk'), method: 'DELETE', body: { ids: ids.map(id => parseInt(id, 10)) }, errorMessage: '견적 일괄 삭제에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, error: result.error }; } // ===== 견적 최종 확정 ===== export async function finalizeQuote(id: string): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}/finalize`), method: 'POST', transform: (d: QuoteApiData) => transformApiToFrontend(d), errorMessage: '견적 확정에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } // ===== 견적 확정 취소 ===== export async function cancelFinalizeQuote(id: string): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}/cancel-finalize`), method: 'POST', transform: (d: QuoteApiData) => transformApiToFrontend(d), errorMessage: '견적 확정 취소에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } // ===== 견적 → 수주 전환 ===== export async function convertQuoteToOrder(id: string): Promise<{ success: boolean; data?: Quote; orderId?: string; error?: string; __authError?: boolean; }> { interface ConvertResponse { quote?: QuoteApiData; order?: { id: number }; } const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}/convert`), method: 'POST', errorMessage: '수주 전환에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: result.data.quote ? transformApiToFrontend(result.data.quote) : undefined, orderId: result.data.order?.id ? String(result.data.order.id) : undefined, }; } // ===== 견적번호 미리보기 ===== export async function getQuoteNumberPreview(): Promise<{ success: boolean; data?: string; error?: string; __authError?: boolean; }> { interface PreviewResponse { quote_number?: string } const result = await executeServerAction({ url: buildApiUrl('/api/v1/quotes/number/preview'), errorMessage: '견적번호 미리보기에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; const data = result.data; const quoteNumber = typeof data === 'string' ? data : (data as PreviewResponse).quote_number || ''; return { success: true, data: quoteNumber }; } // ===== PDF 생성 ===== export async function generateQuotePdf(id: string): Promise<{ success: boolean; data?: Blob; error?: string; __authError?: boolean; }> { try { const url = buildApiUrl(`/api/v1/quotes/${id}/pdf`); const { response, error } = await serverFetch(url, { method: 'POST', }); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED', }; } if (!response) { return { success: false, error: 'PDF 생성에 실패했습니다.', }; } if (!response.ok) { const result = await response.json().catch(() => ({})); return { success: false, error: result.message || 'PDF 생성에 실패했습니다.', }; } const blob = await response.blob(); return { success: true, data: blob, }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[QuoteActions] generateQuotePdf error:', error); return { success: false, error: '서버 오류가 발생했습니다.', }; } } // ===== 이메일 발송 ===== export async function sendQuoteEmail( id: string, emailData: { email: string; subject?: string; message?: string } ): Promise<{ success: boolean; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}/send/email`), method: 'POST', body: emailData, errorMessage: '이메일 발송에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, error: result.error }; } // ===== 카카오 발송 ===== export async function sendQuoteKakao( id: string, kakaoData: { phone: string; templateId?: string } ): Promise<{ success: boolean; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}/send/kakao`), method: 'POST', body: kakaoData, errorMessage: '카카오 발송에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, error: result.error }; } // ===== 완제품(FG) 목록 조회 ===== export interface FinishedGoods { id: number; item_code: string; item_name: string; item_category: string; specification?: string; unit?: string; has_bom?: boolean; bom?: unknown[]; } export async function getFinishedGoods(category?: string): Promise<{ success: boolean; data: FinishedGoods[]; error?: string; __authError?: boolean; }> { interface FGApiResponse { data?: Record[] } const result = await executeServerAction[]>({ url: buildApiUrl('/api/v1/items', { item_type: 'FG', item_category: category, size: '5000', }), errorMessage: '완제품 목록 조회에 실패했습니다.', }); if (result.__authError) return { success: false, data: [], __authError: true }; if (!result.success || !result.data) return { success: false, data: [], error: result.error }; const rawData = result.data; const items: Record[] = Array.isArray(rawData) ? rawData : (rawData as FGApiResponse).data || []; return { success: true, data: items.map((item) => ({ id: item.id as number, item_code: (item.item_code || item.code) as string, item_name: item.name as string, item_category: (item.item_category as string) || '', specification: item.specification as string | undefined, unit: item.unit as string | undefined, has_bom: item.has_bom as boolean | undefined, bom: item.bom as unknown[] | undefined, })), }; } // ===== BOM 기반 자동 견적 산출 (다건) ===== export interface BomCalculateItem { finished_goods_code: string; // React 필드명 (camelCase) - API가 내부에서 W0/H0 등으로 변환 openWidth: number; openHeight: number; quantity?: number; guideRailType?: string; motorPower?: string; controller?: string; wingSize?: number; inspectionFee?: number; } // BomCalculationResult는 types.ts에서 직접 import하세요 // import type { BomCalculationResult } from './types'; // API 서버 응답 구조 (QuoteCalculationService::calculateBomBulk) export interface BomBulkResponse { success: boolean; summary: { total_count: number; success_count: number; fail_count: number; grand_total: number; }; items: Array<{ index: number; finished_goods_code: string; inputs: Record; result: BomCalculationResult; }>; } export async function calculateBomBulk(items: BomCalculateItem[], debug: boolean = true): Promise<{ success: boolean; data: BomBulkResponse | null; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl('/api/v1/quotes/calculate/bom/bulk'), method: 'POST', body: { items, debug }, errorMessage: 'BOM 계산에 실패했습니다.', }); if (result.__authError) return { success: false, data: null, __authError: true }; return { success: result.success, data: result.data || null, error: result.error }; } // ===== 품목 단가 조회 ===== export interface ItemPriceResult { item_code: string; unit_price: number; } export async function getItemPrices(itemCodes: string[]): Promise<{ success: boolean; data: Record | null; error?: string; __authError?: boolean; }> { const result = await executeServerAction>({ url: buildApiUrl('/api/v1/quotes/items/prices'), method: 'POST', body: { item_codes: itemCodes }, errorMessage: '단가 조회에 실패했습니다.', }); if (result.__authError) return { success: false, data: null, __authError: true }; return { success: result.success, data: result.data || null, error: result.error }; } // ===== 견적 요약 통계 ===== export async function getQuotesSummary(params?: { dateFrom?: string; dateTo?: string; }): Promise<{ success: boolean; data?: { totalAmount: number; totalCount: number; draftCount: number; draftAmount: number; finalizedCount: number; finalizedAmount: number; convertedCount: number; convertedAmount: number; conversionRate: number; }; error?: string; __authError?: boolean; }> { try { // 목록 조회를 통해 통계 계산 (별도 API 없는 경우) const listResult = await getQuotes({ perPage: 1000, // 충분히 큰 수 dateFrom: params?.dateFrom, dateTo: params?.dateTo, }); if (!listResult.success) { return { success: false, error: listResult.error, __authError: listResult.__authError, }; } const quotes = listResult.data; const draftQuotes = quotes.filter(q => q.status === 'draft'); const finalizedQuotes = quotes.filter(q => q.isFinal); const convertedQuotes = quotes.filter(q => q.status === 'converted'); const totalAmount = quotes.reduce((sum, q) => sum + q.totalAmount, 0); const draftAmount = draftQuotes.reduce((sum, q) => sum + q.totalAmount, 0); const finalizedAmount = finalizedQuotes.reduce((sum, q) => sum + q.totalAmount, 0); const convertedAmount = convertedQuotes.reduce((sum, q) => sum + q.totalAmount, 0); const conversionRate = quotes.length > 0 ? (convertedQuotes.length / quotes.length) * 100 : 0; return { success: true, data: { totalAmount, totalCount: quotes.length, draftCount: draftQuotes.length, draftAmount, finalizedCount: finalizedQuotes.length, finalizedAmount, convertedCount: convertedQuotes.length, convertedAmount, conversionRate, }, }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[QuoteActions] getQuotesSummary error:', error); return { success: false, error: '서버 오류가 발생했습니다.', }; } } // ===== 견적 참조 데이터 조회 (현장명, 부호 목록) ===== export interface QuoteReferenceData { siteNames: string[]; locationCodes: string[]; } export async function getQuoteReferenceData(): Promise<{ success: boolean; data: QuoteReferenceData; error?: string; __authError?: boolean; }> { interface RefApiData { site_names?: string[]; location_codes?: string[] } const result = await executeServerAction({ url: buildApiUrl('/api/v1/quotes/reference-data'), errorMessage: '참조 데이터 조회에 실패했습니다.', }); const empty: QuoteReferenceData = { siteNames: [], locationCodes: [] }; if (result.__authError) return { success: false, data: empty, __authError: true }; if (!result.success || !result.data) return { success: false, data: empty, error: result.error }; return { success: true, data: { siteNames: result.data.site_names || [], locationCodes: result.data.location_codes || [], }, }; } // ===== 품목 카테고리 트리 조회 ===== export interface ItemCategoryNode { id: number; code: string; name: string; is_active: number; sort_order: number; children: ItemCategoryNode[]; } export async function getItemCategoryTree(): Promise<{ success: boolean; data: ItemCategoryNode[]; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl('/api/v1/categories/tree', { code_group: 'item_category', only_active: true }), errorMessage: '카테고리 조회에 실패했습니다.', }); if (result.__authError) return { success: false, data: [], __authError: true }; return { success: result.success, data: result.data || [], error: result.error }; }