"use server"; import { GetVendorPOSchema } from "./validations"; import { VendorPO, VendorPOItem } from "./types"; import db from "@/db/db"; import { contracts, contractItems, ContractStatus } from "@/db/schema/contract"; import { projects } from "@/db/schema/projects"; import { vendors } from "@/db/schema/vendors"; import { items } from "@/db/schema/items"; import { revalidatePath } from "next/cache"; import { eq, and, or, ilike, count, desc, asc, SQL } from "drizzle-orm"; /** * 벤더 PO 목록 조회 * contracts 테이블에서 실제 데이터를 조회합니다. */ export async function getVendorPOs(input: GetVendorPOSchema) { try { // 실제 데이터베이스 조회 const offset = (input.page - 1) * input.perPage; // 검색 조건 구성 const whereConditions: SQL[] = []; if (input.search) { const searchTerm = `%${input.search}%`; const searchCondition = or( ilike(contracts.contractNo, searchTerm), ilike(contracts.contractName, searchTerm), ilike(projects.name, searchTerm), ilike(vendors.vendorName, searchTerm) ); if (searchCondition) whereConditions.push(searchCondition); } // 벤더 필터링 (partners 페이지에서 사용) if (input.vendorId && input.vendorId > 0) { whereConditions.push(eq(contracts.vendorId, input.vendorId)); } // 필터 조건 추가 if (input.filters && input.filters.length > 0) { for (const filter of input.filters) { if (filter.id && filter.value) { switch (filter.id) { case "contractStatus": const statusCondition = ilike(contracts.status, `%${filter.value}%`); if (statusCondition) whereConditions.push(statusCondition); break; case "contractType": const typeCondition = ilike(contracts.purchaseDocType, `%${filter.value}%`); if (typeCondition) whereConditions.push(typeCondition); break; case "currency": whereConditions.push(eq(contracts.currency, filter.value as string)); break; // 추가 필터 조건들... } } } } const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined; // 정렬 조건 const orderBy = input.sort && input.sort.length > 0 ? input.sort.map((item) => { switch (item.id) { case "contractNo": return item.desc ? desc(contracts.contractNo) : asc(contracts.contractNo); case "contractName": return item.desc ? desc(contracts.contractName) : asc(contracts.contractName); case "lastModifiedDate": return item.desc ? desc(contracts.updatedAt) : asc(contracts.updatedAt); default: return desc(contracts.updatedAt); } }) : [desc(contracts.updatedAt)]; // 데이터 조회 (조인 포함) const rawData = await db .select({ // contracts 테이블 필드들 id: contracts.id, contractNo: contracts.contractNo, contractName: contracts.contractName, status: contracts.status, startDate: contracts.startDate, endDate: contracts.endDate, currency: contracts.currency, totalAmount: contracts.totalAmount, totalAmountKrw: contracts.totalAmountKrw, paymentTerms: contracts.paymentTerms, deliveryTerms: contracts.deliveryTerms, deliveryLocation: contracts.deliveryLocation, shippmentPlace: contracts.shippmentPlace, exchangeRate: contracts.exchangeRate, // SAP ECC 추가 필드들 poVersion: contracts.poVersion, purchaseDocType: contracts.purchaseDocType, purchaseOrg: contracts.purchaseOrg, purchaseGroup: contracts.purchaseGroup, poConfirmStatus: contracts.poConfirmStatus, contractGuaranteeCode: contracts.contractGuaranteeCode, defectGuaranteeCode: contracts.defectGuaranteeCode, guaranteePeriodCode: contracts.guaranteePeriodCode, advancePaymentYn: contracts.advancePaymentYn, budgetAmount: contracts.budgetAmount, budgetCurrency: contracts.budgetCurrency, electronicContractYn: contracts.electronicContractYn, electronicApprovalDate: contracts.electronicApprovalDate, electronicApprovalTime: contracts.electronicApprovalTime, ownerApprovalYn: contracts.ownerApprovalYn, plannedInOutFlag: contracts.plannedInOutFlag, settlementStandard: contracts.settlementStandard, weightSettlementFlag: contracts.weightSettlementFlag, priceIndexYn: contracts.priceIndexYn, writtenContractNo: contracts.writtenContractNo, contractVersion: contracts.contractVersion, // 계약서 내용 및 노트 contractContent: contracts.contractContent, remarks: contracts.remarks, createdAt: contracts.createdAt, updatedAt: contracts.updatedAt, // 조인된 테이블 필드들 projectName: projects.name, vendorName: vendors.vendorName, }) .from(contracts) .leftJoin(projects, eq(contracts.projectId, projects.id)) .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) .where(finalWhere) .orderBy(...orderBy) .offset(offset) .limit(input.perPage); // 총 개수 조회 const [{ totalCount }] = await db .select({ totalCount: count() }) .from(contracts) .leftJoin(projects, eq(contracts.projectId, projects.id)) .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) .where(finalWhere); const pageCount = Math.ceil(totalCount / input.perPage); // VendorPO 타입으로 변환 const data: VendorPO[] = rawData.map(row => ({ id: row.id, contractNo: row.contractNo || '', revision: 'Rev.01', // mock 데이터용 기본값 itemNo: 'ITM-AUTO', // mock 데이터용 기본값 contractStatus: row.status || '', contractType: row.purchaseDocType || '', details: '상세보기', // mock 데이터용 기본값 projectName: row.projectName || '', contractName: row.contractName || '', contractPeriod: row.startDate && row.endDate ? `${row.startDate} ~ ${row.endDate}` : '', contractQuantity: '1 LOT', // 기본값 (실제로는 contract_items에서 계산 필요) currency: row.currency || 'KRW', paymentTerms: row.paymentTerms || '', tax: '10%', // 기본값 (실제로는 contract_items에서 계산 필요) exchangeRate: row.exchangeRate?.toString() || '', deliveryTerms: row.deliveryTerms || '', purchaseManager: '', // 사용자 테이블 조인 필요 poReceiveDate: row.createdAt?.toISOString().split('T')[0] || '', contractDate: row.startDate || '', lcNo: undefined, priceIndexTarget: row.priceIndexYn === 'Y', linkedContractNo: undefined, lastModifiedDate: row.updatedAt?.toISOString().split('T')[0] || '', lastModifiedBy: '', // 사용자 테이블 조인 필요 // SAP ECC 추가 필드들 poVersion: row.poVersion || undefined, purchaseDocType: row.purchaseDocType || undefined, purchaseOrg: row.purchaseOrg || undefined, purchaseGroup: row.purchaseGroup || undefined, poConfirmStatus: row.poConfirmStatus || undefined, contractGuaranteeCode: row.contractGuaranteeCode || undefined, defectGuaranteeCode: row.defectGuaranteeCode || undefined, guaranteePeriodCode: row.guaranteePeriodCode || undefined, advancePaymentYn: row.advancePaymentYn || undefined, budgetAmount: row.budgetAmount ? Number(row.budgetAmount) : undefined, budgetCurrency: row.budgetCurrency || undefined, totalAmount: row.totalAmount ? Number(row.totalAmount) : undefined, totalAmountKrw: row.totalAmountKrw ? Number(row.totalAmountKrw) : undefined, electronicContractYn: row.electronicContractYn || undefined, electronicApprovalDate: row.electronicApprovalDate || undefined, electronicApprovalTime: row.electronicApprovalTime || undefined, ownerApprovalYn: row.ownerApprovalYn || undefined, plannedInOutFlag: row.plannedInOutFlag || undefined, settlementStandard: row.settlementStandard || undefined, weightSettlementFlag: row.weightSettlementFlag || undefined, priceIndexYn: row.priceIndexYn || undefined, writtenContractNo: row.writtenContractNo || undefined, contractVersion: row.contractVersion || undefined, })); return { data, pageCount }; } catch (err) { console.error("Error in getVendorPOs:", err); return { data: [], pageCount: 0 }; } } /** * 벤더 PO 상세 정보 조회 */ export async function getVendorPOById(id: number): Promise { try { const [row] = await db .select({ id: contracts.id, contractNo: contracts.contractNo, contractName: contracts.contractName, status: contracts.status, startDate: contracts.startDate, endDate: contracts.endDate, currency: contracts.currency, totalAmount: contracts.totalAmount, totalAmountKrw: contracts.totalAmountKrw, paymentTerms: contracts.paymentTerms, deliveryTerms: contracts.deliveryTerms, exchangeRate: contracts.exchangeRate, poVersion: contracts.poVersion, purchaseDocType: contracts.purchaseDocType, purchaseOrg: contracts.purchaseOrg, purchaseGroup: contracts.purchaseGroup, poConfirmStatus: contracts.poConfirmStatus, contractGuaranteeCode: contracts.contractGuaranteeCode, defectGuaranteeCode: contracts.defectGuaranteeCode, guaranteePeriodCode: contracts.guaranteePeriodCode, advancePaymentYn: contracts.advancePaymentYn, budgetAmount: contracts.budgetAmount, budgetCurrency: contracts.budgetCurrency, electronicContractYn: contracts.electronicContractYn, electronicApprovalDate: contracts.electronicApprovalDate, electronicApprovalTime: contracts.electronicApprovalTime, ownerApprovalYn: contracts.ownerApprovalYn, plannedInOutFlag: contracts.plannedInOutFlag, settlementStandard: contracts.settlementStandard, weightSettlementFlag: contracts.weightSettlementFlag, priceIndexYn: contracts.priceIndexYn, writtenContractNo: contracts.writtenContractNo, contractVersion: contracts.contractVersion, createdAt: contracts.createdAt, updatedAt: contracts.updatedAt, projectName: projects.name, }) .from(contracts) .leftJoin(projects, eq(contracts.projectId, projects.id)) .where(eq(contracts.id, id)) .limit(1); if (!row) return null; // VendorPO 타입으로 변환 const po: VendorPO = { id: row.id, contractNo: row.contractNo || '', revision: 'Rev.01', itemNo: 'ITM-AUTO', contractStatus: row.status || '', contractType: row.purchaseDocType || '', details: '상세보기', projectName: row.projectName || '', contractName: row.contractName || '', contractPeriod: row.startDate && row.endDate ? `${row.startDate} ~ ${row.endDate}` : '', contractQuantity: '1 LOT', currency: row.currency || 'KRW', paymentTerms: row.paymentTerms || '', tax: '10%', exchangeRate: row.exchangeRate?.toString() || '', deliveryTerms: row.deliveryTerms || '', purchaseManager: '', poReceiveDate: row.createdAt?.toISOString().split('T')[0] || '', contractDate: row.startDate || '', lcNo: undefined, priceIndexTarget: row.priceIndexYn === 'Y', linkedContractNo: undefined, lastModifiedDate: row.updatedAt?.toISOString().split('T')[0] || '', lastModifiedBy: '', poVersion: row.poVersion || undefined, purchaseDocType: row.purchaseDocType || undefined, purchaseOrg: row.purchaseOrg || undefined, purchaseGroup: row.purchaseGroup || undefined, poConfirmStatus: row.poConfirmStatus || undefined, contractGuaranteeCode: row.contractGuaranteeCode || undefined, defectGuaranteeCode: row.defectGuaranteeCode || undefined, guaranteePeriodCode: row.guaranteePeriodCode || undefined, advancePaymentYn: row.advancePaymentYn || undefined, budgetAmount: row.budgetAmount ? Number(row.budgetAmount) : undefined, budgetCurrency: row.budgetCurrency || undefined, totalAmount: row.totalAmount ? Number(row.totalAmount) : undefined, totalAmountKrw: row.totalAmountKrw ? Number(row.totalAmountKrw) : undefined, electronicContractYn: row.electronicContractYn || undefined, electronicApprovalDate: row.electronicApprovalDate || undefined, electronicApprovalTime: row.electronicApprovalTime || undefined, ownerApprovalYn: row.ownerApprovalYn || undefined, plannedInOutFlag: row.plannedInOutFlag || undefined, settlementStandard: row.settlementStandard || undefined, weightSettlementFlag: row.weightSettlementFlag || undefined, priceIndexYn: row.priceIndexYn || undefined, writtenContractNo: row.writtenContractNo || undefined, contractVersion: row.contractVersion || undefined, }; return po; } catch (err) { console.error("Error in getVendorPOById:", err); return null; } } /** * 벤더 PO 액션 처리 * PCR생성, 승인, 거절 등의 액션을 처리 */ export async function handleVendorPOAction( poId: number, action: string ): Promise<{ success: boolean; message: string }> { try { // 목업에서는 성공 응답만 반환 // 실제 구현시에는 각 액션별로 비즈니스 로직 구현 switch (action) { case "pcr-create": return { success: true, message: "개발중" }; case "approve": return { success: true, message: '개발중' }; case "cancel-approve": return { success: true, message: '개발중' }; case "reject-contract": return { success: true, message: '개발중' }; case "print-contract": return { success: true, message: '개발중' }; default: return { success: false, message: '개발중' }; } } catch (err) { console.error("Error in handleVendorPOAction:", err); return { success: false, message: "액션 처리 중 오류가 발생했습니다." }; } } /** * 특정 계약의 상세품목 조회 * contract_items 테이블에서 실제 데이터를 조회합니다. */ export async function getVendorPOItems(contractId: number): Promise { try { const rawItems = await db .select({ // contract_items 테이블 필드들 id: contractItems.id, contractId: contractItems.contractId, itemId: contractItems.itemId, description: contractItems.description, quantity: contractItems.quantity, unitPrice: contractItems.unitPrice, taxRate: contractItems.taxRate, taxAmount: contractItems.taxAmount, totalLineAmount: contractItems.totalLineAmount, remark: contractItems.remark, // contracts 테이블 필드들 contractNo: contracts.contractNo, // items 테이블 필드들 itemCode: items.itemCode, itemName: items.itemName, packageCode: items.packageCode, unitOfMeasure: items.unitOfMeasure, gradeMaterial: items.gradeMaterial, steelType: items.steelType, smCode: items.smCode, }) .from(contractItems) .leftJoin(contracts, eq(contractItems.contractId, contracts.id)) .leftJoin(items, eq(contractItems.itemId, items.id)) .where(eq(contractItems.contractId, contractId)) .orderBy(contractItems.id); // VendorPOItem 타입으로 변환 const vendorPOItems: VendorPOItem[] = rawItems.map(row => ({ contractNo: row.contractNo || '', itemNo: row.itemCode || 'AUTO-ITEM', // mock 데이터용 prNo: `PR-${new Date().getFullYear()}-${String(row.id).padStart(4, '0')}`, // mock 데이터용 materialGroup: row.packageCode || 'Unknown Group', priceStandard: 'FOB', // mock 데이터용 기본값 materialNo: row.itemCode || '', itemDescription: row.itemName || '', materialSpec: row.description || '', fittingNo: undefined, // contract_items에 없는 필드 cert: undefined, // contract_items에 없는 필드 material: row.gradeMaterial || undefined, specification: row.description || '', quantity: row.quantity || 1, quantityUnit: row.unitOfMeasure || 'EA', weight: undefined, // contract_items에 없는 필드 weightUnit: undefined, // contract_items에 없는 필드 totalWeight: undefined, // contract_items에 없는 필드 unitPrice: row.unitPrice ? Number(row.unitPrice) : 0, priceUnit: 'KRW', // 기본값 priceUnitValue: '원/EA', // 기본값 contractAmount: row.totalLineAmount ? Number(row.totalLineAmount) : 0, adjustmentAmount: undefined, // contract_items에 없는 필드 deliveryDate: new Date().toISOString().split('T')[0], // 기본값 (오늘 날짜) vatType: row.taxRate && Number(row.taxRate) > 0 ? '과세' : '면세', steelSpec: row.steelType || undefined, prManager: 'AUTO-MANAGER', // mock 데이터용 기본값 })); return vendorPOItems; } catch (err) { console.error("Error in getVendorPOItems:", err); throw err; } } /** * 계약번호로 상세품목 조회 (외부에서 contractNo로 호출할 때 사용) */ export async function getVendorPOItemsByContractNo(contractNo: string): Promise { try { // 먼저 계약 ID 조회 const contract = await db.query.contracts.findFirst({ where: eq(contracts.contractNo, contractNo), }); if (!contract) { console.warn(`Contract not found: ${contractNo}`); return []; } return await getVendorPOItems(contract.id); } catch (err) { console.error("Error in getVendorPOItemsByContractNo:", err); throw err; } } /** * PCR 생성 요청: PCR 생성 요청 후, 상태 변경 */ export async function createPcrRequest(contractId: number) { try { // TODO PCR 생성 요청 로직 구현 // PCR 생성 요청 상태로 변경 await db.update(contracts).set({ status: ContractStatus.PCR_REQUEST }).where(eq(contracts.id, contractId)); // 캐시 무효화하여 변경사항 즉시 반영 revalidatePath("/partners/po"); } catch (err) { console.error("Error in createPcrRequest:", err); throw err; } return { success: true, message: "PCR 생성 요청이 성공적으로 완료되었습니다." }; } /** * 계약 승인 처리: 상태만 변경 */ export async function acceptContract(contractId: number) { try { await db.update(contracts).set({ status: ContractStatus.COMPLETE_THE_CONTRACT }).where(eq(contracts.id, contractId)); // 캐시 무효화하여 변경사항 즉시 반영 revalidatePath("/partners/po"); } catch (err) { console.error("Error in acceptContract:", err); throw err; } return { success: true, message: "계약이 성공적으로 승인되었습니다." }; } /** * 계약 승인 취소 처리: 상태만 변경 */ export async function cancelAcceptContract(contractId: number) { try { // 계약 승인 상태에서만 취소 가능 const contract = await db.query.contracts.findFirst({ where: eq(contracts.id, contractId), }); if (!contract) { throw new Error("계약을 찾을 수 없습니다."); } if (contract.status !== ContractStatus.COMPLETE_THE_CONTRACT) { throw new Error("계약 승인 상태가 아닙니다."); } // 취소 처리 await db.update(contracts).set({ status: ContractStatus.CONTRACT_ACCEPT_REQUEST }).where(eq(contracts.id, contractId)); // 캐시 무효화하여 변경사항 즉시 반영 revalidatePath("/partners/po"); } catch (err) { console.error("Error in cancelAcceptContract:", err); throw err; } return { success: true, message: "계약이 성공적으로 승인 취소되었습니다." }; } /** * 계약 거절 처리: 거절 사유를 입력받고, 상태 변경 */ export async function rejectContract(contractId: number, rejectionReason: string) { try { await db.update(contracts).set({ status: ContractStatus.REJECT_TO_ACCEPT_CONTRACT, rejectionReason }).where(eq(contracts.id, contractId)); // 캐시 무효화하여 변경사항 즉시 반영 revalidatePath("/partners/po"); } catch (err) { console.error("Error in rejectContract:", err); throw err; } return { success: true, message: "계약이 성공적으로 거절되었습니다." }; } /** * 벤더 코멘트 저장 */ export async function saveVendorComment(contractId: number, vendorComment: string) { try { await db.update(contracts).set({ vendorComment, updatedAt: new Date() }).where(eq(contracts.id, contractId)); // 캐시 무효화하여 변경사항 즉시 반영 revalidatePath("/partners/po"); } catch (err) { console.error("Error in saveVendorComment:", err); throw err; } return { success: true, message: "의견이 성공적으로 저장되었습니다." }; } /** * SHI 코멘트 저장 (EVCP용) */ export async function saveSHIComment(contractId: number, shiComment: string) { try { await db.update(contracts).set({ shiComment, updatedAt: new Date() }).where(eq(contracts.id, contractId)); // 캐시 무효화하여 변경사항 즉시 반영 revalidatePath("/evcp/po"); revalidatePath(`/evcp/po/${contractId}`); } catch (err) { console.error("Error in saveSHIComment:", err); throw err; } return { success: true, message: "SHI 의견이 성공적으로 저장되었습니다." }; } /** * 특정 계약의 상세 정보 조회 (EVCP/SHI용) */ export async function getContractDetail(contractId: number) { try { // contractId 유효성 검사 if (!contractId || isNaN(contractId)) { return { success: false, error: "유효하지 않은 계약 ID입니다." }; } // 계약 기본 정보 조회 const [contractData] = await db .select({ // contracts 테이블 필드들 id: contracts.id, contractNo: contracts.contractNo, contractName: contracts.contractName, status: contracts.status, startDate: contracts.startDate, endDate: contracts.endDate, contractDate: contracts.createdAt, currency: contracts.currency, totalAmount: contracts.totalAmount, totalAmountKrw: contracts.totalAmountKrw, paymentTerms: contracts.paymentTerms, deliveryTerms: contracts.deliveryTerms, exchangeRate: contracts.exchangeRate, rejectionReason: contracts.rejectionReason, // SAP ECC 추가 필드들 poVersion: contracts.poVersion, purchaseDocType: contracts.purchaseDocType, purchaseOrg: contracts.purchaseOrg, purchaseGroup: contracts.purchaseGroup, poConfirmStatus: contracts.poConfirmStatus, // 계약/보증 관련 contractGuaranteeCode: contracts.contractGuaranteeCode, defectGuaranteeCode: contracts.defectGuaranteeCode, guaranteePeriodCode: contracts.guaranteePeriodCode, advancePaymentYn: contracts.advancePaymentYn, // 계약서 내용 및 노트 contractContent: contracts.contractContent, remarks: contracts.remarks, vendorComment: contracts.vendorComment, shiComment: contracts.shiComment, createdAt: contracts.createdAt, updatedAt: contracts.updatedAt, // 조인된 테이블 필드들 projectId: projects.id, projectName: projects.name, projectCode: projects.code, vendorId: vendors.id, vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, }) .from(contracts) .leftJoin(projects, eq(contracts.projectId, projects.id)) .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) .where(eq(contracts.id, contractId)) .limit(1); if (!contractData) { return { success: false, error: "계약 정보를 찾을 수 없습니다." }; } // 계약 품목 조회 const items = await getVendorPOItems(contractId); return { success: true, data: { ...contractData, items, }, }; } catch (error) { console.error("Error fetching contract detail:", error); return { success: false, error: "계약 상세 정보 조회 중 오류가 발생했습니다." }; } } /** * 특정 계약의 상세 정보 조회 (벤더용 계약 상세 페이지) */ export async function getVendorContractDetail(contractId: number, vendorId: number) { try { // contractId 유효성 검사 if (!contractId || isNaN(contractId)) { return { success: false, error: "유효하지 않은 계약 ID입니다." }; } // 계약 기본 정보 조회 (벤더 필터링 포함) const [contractData] = await db .select({ // contracts 테이블 필드들 id: contracts.id, contractNo: contracts.contractNo, contractName: contracts.contractName, status: contracts.status, startDate: contracts.startDate, endDate: contracts.endDate, contractDate: contracts.createdAt, currency: contracts.currency, totalAmount: contracts.totalAmount, totalAmountKrw: contracts.totalAmountKrw, paymentTerms: contracts.paymentTerms, deliveryTerms: contracts.deliveryTerms, exchangeRate: contracts.exchangeRate, // SAP ECC 추가 필드들 poVersion: contracts.poVersion, purchaseDocType: contracts.purchaseDocType, purchaseOrg: contracts.purchaseOrg, purchaseGroup: contracts.purchaseGroup, poConfirmStatus: contracts.poConfirmStatus, // 계약/보증 관련 contractGuaranteeCode: contracts.contractGuaranteeCode, defectGuaranteeCode: contracts.defectGuaranteeCode, guaranteePeriodCode: contracts.guaranteePeriodCode, advancePaymentYn: contracts.advancePaymentYn, // 계약서 내용 및 노트 contractContent: contracts.contractContent, remarks: contracts.remarks, vendorComment: contracts.vendorComment, shiComment: contracts.shiComment, createdAt: contracts.createdAt, updatedAt: contracts.updatedAt, // 조인된 테이블 필드들 projectId: projects.id, projectName: projects.name, projectCode: projects.code, vendorId: vendors.id, vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, }) .from(contracts) .leftJoin(projects, eq(contracts.projectId, projects.id)) .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) .where( and( eq(contracts.id, contractId), eq(contracts.vendorId, vendorId) // 벤더 권한 체크 ) ) .limit(1); if (!contractData) { return { success: false, error: "계약 정보를 찾을 수 없거나 접근 권한이 없습니다." }; } // 계약 품목 조회 const items = await getVendorPOItems(contractId); return { success: true, data: { ...contractData, items, }, }; } catch (error) { console.error("Error fetching contract detail:", error); return { success: false, error: "계약 상세 정보 조회 중 오류가 발생했습니다." }; } }