diff options
Diffstat (limited to 'lib/avl/avl-itb-rfq-service.ts')
| -rw-r--r-- | lib/avl/avl-itb-rfq-service.ts | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/lib/avl/avl-itb-rfq-service.ts b/lib/avl/avl-itb-rfq-service.ts new file mode 100644 index 00000000..f7662c2e --- /dev/null +++ b/lib/avl/avl-itb-rfq-service.ts @@ -0,0 +1,289 @@ +// 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<string> { + 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<CreateAvlRfqItbResult> { + 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<CreateAvlRfqItbInput> +): Promise<CreateAvlRfqItbInput> { + 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 + } +} |
