"use server" import { oracleKnex } from '@/lib/oracle-db/db' import db from '@/db/db' import { basicContract, agreementComments } from '@/db/schema' import { eq, inArray, and } from 'drizzle-orm' import { revalidateTag } from 'next/cache' import { sendEmail } from '@/lib/mail/sendEmail' // SSLVW_PUR_INQ_REQ 테이블 데이터 타입 (실제 테이블 구조에 맞게 조정 필요) export interface SSLVWPurInqReq { [key: string]: string | number | Date | null | undefined } // 테스트 환경용 폴백 데이터 const FALLBACK_TEST_DATA: SSLVWPurInqReq[] = [ { id: 1, request_number: 'REQ001', status: 'PENDING', created_date: new Date('2025-01-01'), description: '테스트 요청 1' }, { id: 2, request_number: 'REQ002', status: 'APPROVED', created_date: new Date('2025-01-02'), description: '테스트 요청 2' } ] /** * SSLVW_PUR_INQ_REQ 테이블 전체 조회 * @returns 테이블 데이터 배열 */ export async function getSSLVWPurInqReqData(): Promise<{ success: boolean data: SSLVWPurInqReq[] error?: string isUsingFallback?: boolean }> { try { console.log('📋 [getSSLVWPurInqReqData] SSLVW_PUR_INQ_REQ 테이블 조회 시작...') const result = await oracleKnex.raw(` SELECT ID, PRGS_STAT_DSC, REQ_DT, REQ_NO, REQ_TIT, REQ_CONT, VEND_CD, VEND_NM, CNTR_CTGR_DSC, CNTR_AMT, CNTR_STRT_DT, CNTR_END_DT, RPLY_DT, RPLY_CONT, RPLY_USER_NM, RPLY_USER_ID, CREATED_AT, UPDATED_AT FROM SSLVW_PUR_INQ_REQ WHERE ROWNUM < 100 ORDER BY REQ_DT DESC `) // Oracle raw query의 결과는 rows 배열에 들어있음 const rows = (result.rows || result) as Array> console.log(`✅ [getSSLVWPurInqReqData] 조회 성공 - ${rows.length}건`) // 데이터 타입 변환 (필요에 따라 조정) const cleanedResult = rows.map((item) => { const convertedItem: SSLVWPurInqReq = {} for (const [key, value] of Object.entries(item)) { if (value instanceof Date) { convertedItem[key] = value } else if (value === null) { convertedItem[key] = null } else { convertedItem[key] = String(value) } } return convertedItem }) return { success: true, data: cleanedResult, isUsingFallback: false } } catch (error) { console.error('❌ [getSSLVWPurInqReqData] 오류:', error) console.log('🔄 [getSSLVWPurInqReqData] 폴백 테스트 데이터 사용') return { success: true, data: FALLBACK_TEST_DATA, isUsingFallback: true } } } /** * 법무검토 요청 * @param contractIds 계약서 ID 배열 * @returns 성공 여부 및 메시지 */ export async function requestLegalReview(contractIds: number[]): Promise<{ success: boolean message: string requested: number skipped: number errors: string[] }> { console.log(`📋 [requestLegalReview] 법무검토 요청 시작: ${contractIds.length}건`) if (!contractIds || contractIds.length === 0) { return { success: false, message: '선택된 계약서가 없습니다.', requested: 0, skipped: 0, errors: [] } } try { // 계약서 정보 조회 const contracts = await db .select() .from(basicContract) .where(inArray(basicContract.id, contractIds)) if (!contracts || contracts.length === 0) { return { success: false, message: '계약서를 찾을 수 없습니다.', requested: 0, skipped: 0, errors: [] } } let requestedCount = 0 let skippedCount = 0 const errors: string[] = [] for (const contract of contracts) { try { // 유효성 검사 if (contract.legalReviewRequestedAt) { console.log(`⚠️ [requestLegalReview] 계약서 ${contract.id}: 이미 법무검토 요청됨`) skippedCount++ errors.push(`${contract.id}: 이미 법무검토 요청됨`) continue } // 협의 완료 여부 확인 // 1. 협의 완료됨 (negotiationCompletedAt 있음) → 가능 // 2. 협의 없음 (코멘트 없음) → 가능 // 3. 협의 중 (negotiationCompletedAt 없고 코멘트 있음) → 불가 if (!contract.negotiationCompletedAt) { // 협의 완료되지 않은 경우, 코멘트 존재 여부 확인 // 삭제되지 않은 코멘트가 있으면 협의 중이므로 불가 const comments = await db .select() .from(agreementComments) .where( and( eq(agreementComments.basicContractId, contract.id), eq(agreementComments.isDeleted, false) ) ) .limit(1); // 삭제되지 않은 코멘트가 있으면 협의 중이므로 불가 if (comments.length > 0) { console.log(`⚠️ [requestLegalReview] 계약서 ${contract.id}: 협의 진행 중`) skippedCount++ errors.push(`${contract.id}: 협의가 진행 중입니다`) continue } // 코멘트가 없으면 협의 없음으로 간주하고 가능 console.log(`ℹ️ [requestLegalReview] 계약서 ${contract.id}: 협의 없음, 법무검토 요청 가능`) } // 법무검토 요청 상태로 업데이트 await db .update(basicContract) .set({ legalReviewRequestedAt: new Date(), updatedAt: new Date(), } as any) .where(eq(basicContract.id, contract.id)) requestedCount++ console.log(`✅ [requestLegalReview] 계약서 ${contract.id}: 법무검토 요청 완료`) // 법무팀에 이메일 알림 발송 (선택사항) try { // TODO: 법무팀 이메일 주소를 설정에서 가져오기 const legalTeamEmail = process.env.LEGAL_TEAM_EMAIL || 'legal@example.com' await sendEmail({ to: legalTeamEmail, subject: `[eVCP] 기본계약서 법무검토 요청 - ${contract.id}`, template: 'legal-review-request', context: { language: 'ko', contractId: contract.id, vendorName: contract.vendorId || '업체명 없음', contractUrl: `${process.env.NEXT_PUBLIC_APP_URL}/evcp/basic-contract/${contract.id}`, systemUrl: process.env.NEXT_PUBLIC_APP_URL || 'https://evcp.com', currentYear: new Date().getFullYear(), }, }) } catch (emailError) { console.error(`⚠️ [requestLegalReview] 이메일 발송 실패 (계약서 ${contract.id}):`, emailError) // 이메일 실패는 법무검토 요청 성공에 영향을 주지 않음 } } catch (error) { console.error(`❌ [requestLegalReview] 계약서 ${contract.id} 처리 실패:`, error) errors.push(`${contract.id}: 처리 중 오류 발생`) skippedCount++ } } // 캐시 무효화 revalidateTag('basic-contracts') const totalProcessed = requestedCount + skippedCount let message = '' if (requestedCount === contracts.length) { message = `${requestedCount}건의 계약서에 대한 법무검토가 요청되었습니다.` } else if (requestedCount > 0) { message = `${requestedCount}건 요청 완료, ${skippedCount}건 건너뜀` } else { message = `모든 계약서를 건너뛰었습니다. (${skippedCount}건)` } console.log(`✅ [requestLegalReview] 법무검토 요청 완료: ${message}`) return { success: requestedCount > 0, message, requested: requestedCount, skipped: skippedCount, errors } } catch (error) { console.error('❌ [requestLegalReview] 법무검토 요청 실패:', error) return { success: false, message: '법무검토 요청 중 오류가 발생했습니다.', requested: 0, skipped: contractIds.length, errors: [error instanceof Error ? error.message : '알 수 없는 오류'] } } }