summaryrefslogtreecommitdiff
path: root/lib/avl/avl-itb-rfq-service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/avl/avl-itb-rfq-service.ts')
-rw-r--r--lib/avl/avl-itb-rfq-service.ts289
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
+ }
+}