// AVL 기반 RFQ/ITB 생성 서비스 'use server' import { getServerSession } from 'next-auth/next' import { authOptions } from '@/app/api/auth/[...nextauth]/route' import db from '@/db/db' import { users, rfqsLast, rfqPrItems } from '@/db/schema' import { eq, desc, sql, and } from 'drizzle-orm' import type { AvlDetailItem } from './types' // RFQ/ITB 코드 생성 헬퍼 함수 async function generateAvlRfqItbCode(userCode: string, type: 'RFQ' | 'ITB'): Promise { try { // 동일한 userCode를 가진 마지막 RFQ/ITB 번호 조회 const lastRfq = await db .select({ rfqCode: rfqsLast.rfqCode }) .from(rfqsLast) .where( and( eq(rfqsLast.picCode, userCode), type === 'RFQ' ? sql`${rfqsLast.prNumber} IS NOT NULL AND ${rfqsLast.prNumber} != ''` : sql`${rfqsLast.projectCompany} IS NOT NULL AND ${rfqsLast.projectCompany} != ''` ) ) .orderBy(desc(rfqsLast.createdAt)) .limit(1); let nextNumber = 1; if (lastRfq.length > 0 && lastRfq[0].rfqCode) { // 마지막 코드에서 숫자 부분 추출 (ex: "RFQ001001" -> "001") const codeMatch = lastRfq[0].rfqCode.match(/([A-Z]{3})(\d{3})(\d{3})/); if (codeMatch) { const currentNumber = parseInt(codeMatch[3]); nextNumber = currentNumber + 1; } } // 코드 형식: RFQ/ITB + userCode(3자리) + 일련번호(3자리) const prefix = type === 'RFQ' ? 'RFQ' : 'ITB'; const paddedNumber = nextNumber.toString().padStart(3, '0'); return `${prefix}${userCode}${paddedNumber}`; } catch (error) { console.error('RFQ/ITB 코드 생성 오류:', error); // 오류 발생 시 기본 코드 생성 const prefix = type === 'RFQ' ? 'RFQ' : 'ITB'; return `${prefix}${userCode}001`; } } // AVL 기반 RFQ/ITB 생성을 위한 입력 타입 export interface CreateAvlRfqItbInput { // AVL 정보 avlItems: AvlDetailItem[] businessType: '조선' | '해양' // 조선: RFQ, 해양: ITB // RFQ/ITB 공통 정보 rfqTitle: string dueDate: Date remark?: string // 담당자 정보 picUserId: number // 추가 정보 (ITB용) projectCompany?: string projectFlag?: string projectSite?: string smCode?: string // PR 정보 (RFQ용) prNumber?: string prIssueDate?: Date series?: string } // RFQ/ITB 생성 결과 타입 export interface CreateAvlRfqItbResult { success: boolean message?: string data?: { id: number rfqCode: string type: 'RFQ' | 'ITB' } error?: string } /** * AVL 기반 RFQ/ITB 생성 서비스 * - 조선 사업: RFQ 생성 * - 해양 사업: ITB 생성 * - rfqLast 테이블에 직접 데이터 삽입 */ export async function createAvlRfqItbAction(input: CreateAvlRfqItbInput): Promise { try { // 세션 확인 const session = await getServerSession(authOptions) if (!session?.user?.id) { return { success: false, error: '로그인이 필요합니다.' } } // 입력 검증 if (!input.avlItems || input.avlItems.length === 0) { return { success: false, error: '견적 요청할 AVL 아이템이 없습니다.' } } if (!input.businessType || !['조선', '해양'].includes(input.businessType)) { return { success: false, error: '올바른 사업 유형을 선택해주세요.' } } // 담당자 정보 확인 const picUser = await db .select({ id: users.id, name: users.name, userCode: users.userCode }) .from(users) .where(eq(users.id, input.picUserId)) .limit(1) if (!picUser || picUser.length === 0) { return { success: false, error: '담당자를 찾을 수 없습니다.' } } const userCode = picUser[0].userCode; if (!userCode || userCode.length !== 3) { return { success: false, error: '담당자의 userCode가 올바르지 않습니다 (3자리 필요)' } } // 사업 유형에 따른 RFQ/ITB 구분 및 데이터 준비 const rfqType = input.businessType === '조선' ? 'RFQ' : 'ITB' const rfqTypeLabel = rfqType // RFQ/ITB 코드 생성 const rfqCode = await generateAvlRfqItbCode(userCode, rfqType) // 대표 아이템 정보 추출 (첫 번째 아이템) const representativeItem = input.avlItems[0] // 트랜잭션으로 RFQ/ITB 생성 const result = await db.transaction(async (tx) => { // 1. rfqsLast 테이블에 기본 정보 삽입 const [newRfq] = await tx .insert(rfqsLast) .values({ rfqCode, status: "RFQ 생성", dueDate: input.dueDate, // 대표 아이템 정보 itemCode: representativeItem.materialGroupCode || `AVL-${representativeItem.id}`, itemName: representativeItem.materialNameCustomerSide || representativeItem.materialGroupName || 'AVL 아이템', // 담당자 정보 pic: input.picUserId, picCode: userCode, picName: picUser[0].name || '', // 기타 정보 remark: input.remark || null, createdBy: Number(session.user.id), updatedBy: Number(session.user.id), createdAt: new Date(), updatedAt: new Date(), // 사업 유형별 추가 필드 ...(input.businessType === '조선' && { // RFQ 필드 prNumber: input.prNumber || rfqCode, // PR 번호가 없으면 RFQ 코드 사용 prIssueDate: input.prIssueDate || new Date(), series: input.series || null }), ...(input.businessType === '해양' && { // ITB 필드 projectCompany: input.projectCompany || 'AVL 기반 프로젝트', projectFlag: input.projectFlag || null, projectSite: input.projectSite || null, smCode: input.smCode || null }) }) .returning() // 2. rfqPrItems 테이블에 AVL 아이템들 삽입 const prItemsData = input.avlItems.map((item, index) => ({ rfqsLastId: newRfq.id, rfqItem: `${index + 1}`.padStart(3, '0'), // 001, 002, ... prItem: `${index + 1}`.padStart(3, '0'), prNo: rfqCode, materialCode: item.materialGroupCode || `AVL-${item.id}`, materialDescription: item.materialNameCustomerSide || item.materialGroupName || `AVL 아이템 ${index + 1}`, materialCategory: item.materialGroupCode || null, quantity: 1, // AVL에서는 수량 정보가 없으므로 1로 설정 uom: 'EA', // 기본 단위 majorYn: index === 0, // 첫 번째 아이템을 주요 아이템으로 설정 deliveryDate: input.dueDate, // 납기일은 RFQ 마감일과 동일하게 설정 })) await tx.insert(rfqPrItems).values(prItemsData) return newRfq }) // 성공 결과 반환 return { success: true, message: `${rfqTypeLabel}가 성공적으로 생성되었습니다.`, data: { id: result.id, rfqCode: result.rfqCode!, type: rfqTypeLabel as 'RFQ' | 'ITB' } } } catch (error) { console.error('AVL RFQ/ITB 생성 오류:', error) if (error instanceof Error) { return { success: false, error: error.message } } return { success: false, error: '알 수 없는 오류가 발생했습니다.' } } } /** * AVL 데이터에서 RFQ/ITB 생성을 위한 기본값 설정 헬퍼 함수 */ export async function prepareAvlRfqItbInput( selectedItems: AvlDetailItem[], businessType: '조선' | '해양', defaultValues?: Partial ): Promise { const now = new Date() const dueDate = new Date(now.getTime() + (30 * 24 * 60 * 60 * 1000)) // 30일 후 // 선택된 아이템들의 대표 정보를 추출하여 제목 생성 const representativeItem = selectedItems[0] const itemCount = selectedItems.length const titleSuffix = itemCount > 1 ? ` 외 ${itemCount - 1}건` : '' const defaultTitle = `${representativeItem?.materialNameCustomerSide || 'AVL 자재'}${titleSuffix}` return { avlItems: selectedItems, businessType, rfqTitle: defaultValues?.rfqTitle || `${businessType} - ${defaultTitle}`, dueDate: defaultValues?.dueDate || dueDate, remark: defaultValues?.remark || `AVL 기반 ${businessType} 견적 요청`, picUserId: defaultValues?.picUserId || 0, // 호출 측에서 설정 필요 // ITB용 필드들 projectCompany: defaultValues?.projectCompany, projectFlag: defaultValues?.projectFlag, projectSite: defaultValues?.projectSite, smCode: defaultValues?.smCode, // RFQ용 필드들 prNumber: defaultValues?.prNumber, prIssueDate: defaultValues?.prIssueDate, series: defaultValues?.series } }