From 25b916d040a512cd5248dff319d727ae144d0652 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 15 Sep 2025 03:24:12 +0000 Subject: (최겸) 구매 PCR 개발(po -> pcr, ecc pcr-confirm test 필) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pcr/actions.ts | 458 +++++++++++++ lib/pcr/service.ts | 753 +++++++++++++++++++++ lib/pcr/table/approve-reject-pcr-dialog.tsx | 231 +++++++ lib/pcr/table/create-pcr-dialog.tsx | 642 ++++++++++++++++++ .../table/detail-table/create-pcr-pr-dialog.tsx | 598 ++++++++++++++++ lib/pcr/table/detail-table/pcr-detail-column.tsx | 333 +++++++++ lib/pcr/table/detail-table/pcr-detail-table.tsx | 121 ++++ .../detail-table/pcr-detail-toolbar-action.tsx | 79 +++ lib/pcr/table/edit-pcr-sheet.tsx | 237 +++++++ lib/pcr/table/pcr-table-column.tsx | 416 ++++++++++++ lib/pcr/table/pcr-table-toolbar-actions.tsx | 120 ++++ lib/pcr/table/pcr-table.tsx | 396 +++++++++++ lib/pcr/types.ts | 189 ++++++ 13 files changed, 4573 insertions(+) create mode 100644 lib/pcr/actions.ts create mode 100644 lib/pcr/service.ts create mode 100644 lib/pcr/table/approve-reject-pcr-dialog.tsx create mode 100644 lib/pcr/table/create-pcr-dialog.tsx create mode 100644 lib/pcr/table/detail-table/create-pcr-pr-dialog.tsx create mode 100644 lib/pcr/table/detail-table/pcr-detail-column.tsx create mode 100644 lib/pcr/table/detail-table/pcr-detail-table.tsx create mode 100644 lib/pcr/table/detail-table/pcr-detail-toolbar-action.tsx create mode 100644 lib/pcr/table/edit-pcr-sheet.tsx create mode 100644 lib/pcr/table/pcr-table-column.tsx create mode 100644 lib/pcr/table/pcr-table-toolbar-actions.tsx create mode 100644 lib/pcr/table/pcr-table.tsx create mode 100644 lib/pcr/types.ts (limited to 'lib') diff --git a/lib/pcr/actions.ts b/lib/pcr/actions.ts new file mode 100644 index 00000000..00c15f4b --- /dev/null +++ b/lib/pcr/actions.ts @@ -0,0 +1,458 @@ +"use server" + +import { revalidatePath } from "next/cache" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { confirmPCR } from "@/lib/soap/ecc/send/pcr-confirm" +import { format } from "date-fns" +import db from "@/db/db" +import { pcrPo } from "@/db/schema" +import { eq } from "drizzle-orm" +import { + createPcrPo, + createPcrPr, + updatePcrPo, + updatePcrPr, + deletePcrPo, + deletePcrPr, + getPcrPoById, + getPcrPrById, +} from "./service" + +/** + * PCR_PO 생성 Server Action + */ +export async function createPcrPoAction(data: { + pcrApprovalStatus?: string; + changeType?: string; + details?: string; + project?: string; + pcrRequestDate: Date; + poContractNumber: string; + revItemNumber?: string; + purchaseContractManager?: string; + pcrCreator?: string; + poContractAmountBefore?: number; + poContractAmountAfter?: number; + contractCurrency?: string; + pcrReason?: string; + detailsReason?: string; + rejectionReason?: string; + pcrResponseDate?: Date; + vendorId?: number; +}) { + try { + const result = await createPcrPo(data); + + if (result.success) { + // 관련 페이지들 재검증 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { success: true, message: "PCR_PO가 성공적으로 생성되었습니다", data: result.data } + } else { + return { success: false, error: result.error } + } + } catch (error) { + console.error("PCR_PO 생성 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR_PO 생성에 실패했습니다" + } + } +} + +/** + * PCR_PR 생성 Server Action + */ +export async function createPcrPrAction(data: { + materialNumber: string; + materialDetails?: string; + quantityBefore?: number; + quantityAfter?: number; + weightBefore?: number; + weightAfter?: number; + subcontractorWeightBefore?: number; + subcontractorWeightAfter?: number; + supplierWeightBefore?: number; + supplierWeightAfter?: number; + specDrawingBefore?: string; + specDrawingAfter?: string; + initialPoContractDate?: Date; + specChangeDate?: Date; + poContractModifiedDate?: Date; + confirmationDate?: Date; + designManager?: string; + poContractNumber: string; +}) { + try { + const result = await createPcrPr(data); + + if (result.success) { + // 관련 페이지들 재검증 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { success: true, message: "PCR_PR이 성공적으로 생성되었습니다", data: result.data } + } else { + return { success: false, error: result.error } + } + } catch (error) { + console.error("PCR_PR 생성 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR_PR 생성에 실패했습니다" + } + } +} + +/** + * PCR_PO 업데이트 Server Action + */ +export async function updatePcrPoAction(id: number, data: Partial<{ + pcrApprovalStatus: string; + changeType: string; + details: string; + project: string; + pcrRequestDate: Date; + poContractNumber: string; + revItemNumber: string; + purchaseContractManager: string; + pcrCreator: string; + poContractAmountBefore: number; + poContractAmountAfter: number; + contractCurrency: string; + pcrReason: string; + detailsReason: string; + rejectionReason: string; + pcrResponseDate: Date; + vendorId: number; +}>) { + try { + const result = await updatePcrPo(id, data); + + if (result.success) { + // 관련 페이지들 재검증 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { success: true, message: "PCR_PO가 성공적으로 업데이트되었습니다", data: result.data } + } else { + return { success: false, error: result.error } + } + } catch (error) { + console.error("PCR_PO 업데이트 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR_PO 업데이트에 실패했습니다" + } + } +} + +/** + * PCR_PR 업데이트 Server Action + */ +export async function updatePcrPrAction(id: number, data: Partial<{ + materialNumber: string; + materialDetails: string; + quantityBefore: number; + quantityAfter: number; + weightBefore: number; + weightAfter: number; + subcontractorWeightBefore: number; + subcontractorWeightAfter: number; + supplierWeightBefore: number; + supplierWeightAfter: number; + specDrawingBefore: string; + specDrawingAfter: string; + initialPoContractDate: Date; + specChangeDate: Date; + poContractModifiedDate: Date; + confirmationDate: Date; + designManager: string; + poContractNumber: string; +}>) { + try { + const result = await updatePcrPr(id, data); + + if (result.success) { + // 관련 페이지들 재검증 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { success: true, message: "PCR_PR이 성공적으로 업데이트되었습니다", data: result.data } + } else { + return { success: false, error: result.error } + } + } catch (error) { + console.error("PCR_PR 업데이트 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR_PR 업데이트에 실패했습니다" + } + } +} + +/** + * PCR_PO 삭제 Server Action + */ +export async function deletePcrPoAction(id: number) { + try { + const result = await deletePcrPo(id); + + if (result.success) { + // 관련 페이지들 재검증 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { success: true, message: "PCR_PO가 성공적으로 삭제되었습니다" } + } else { + return { success: false, error: result.error } + } + } catch (error) { + console.error("PCR_PO 삭제 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR_PO 삭제에 실패했습니다" + } + } +} + +/** + * PCR_PR 삭제 Server Action + */ +export async function deletePcrPrAction(id: number) { + try { + const result = await deletePcrPr(id); + + if (result.success) { + // 관련 페이지들 재검증 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { success: true, message: "PCR_PR이 성공적으로 삭제되었습니다" } + } else { + return { success: false, error: result.error } + } + } catch (error) { + console.error("PCR_PR 삭제 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR_PR 삭제에 실패했습니다" + } + } +} + +/** + * PCR_PO 단일 조회 Server Action + */ +export async function getPcrPoByIdAction(id: number) { + try { + const data = await getPcrPoById(id); + return { success: true, data } + } catch (error) { + console.error("PCR_PO 조회 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR_PO 조회에 실패했습니다" + } + } +} + +/** + * PCR_PR 단일 조회 Server Action + */ +export async function getPcrPrByIdAction(id: number) { + try { + const data = await getPcrPrById(id); + return { success: true, data } + } catch (error) { + console.error("PCR_PR 조회 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR_PR 조회에 실패했습니다" + } + } +} + +/** + * PCR 승인 Server Action + */ +export async function approvePcrAction(id: number, reason?: string) { + try { + // PCR 데이터 조회 + const pcrData = await getPcrPoById(id); + if (!pcrData) { + return { success: false, error: "PCR 데이터를 찾을 수 없습니다" } + } + + // PCR 확인 데이터 구성 + const confirmData = { + PCR_REQ: pcrData.poContractNumber, // PO 계약번호를 PCR 요청번호로 사용 + PCR_REQ_SEQ: String(pcrData.id).padStart(5, '0'), // ID를 순번으로 사용 + PCR_DEC_DATE: format(new Date(), 'yyyyMMdd'), // 오늘 날짜 + EBELN: pcrData.poContractNumber, // PO 계약번호 + EBELP: pcrData.revItemNumber || '00010', // REV/품번 또는 기본값 + PCR_STATUS: 'A', // 승인 상태 + CONFIRM_CD: 'APPROVED', + CONFIRM_RSN: reason || 'PCR 승인 완료' + }; + + console.log(`🚀 PCR 승인 요청 시작: ${confirmData.PCR_REQ}-${confirmData.PCR_REQ_SEQ}`); + + // ECC로 승인 요청 전송 + const confirmResult = await confirmPCR(confirmData); + + if (!confirmResult.success) { + console.error(`❌ PCR 승인 실패: ${confirmResult.message}`); + return { success: false, error: confirmResult.message } + } + + // DB에서 PCR 상태 업데이트 + const updateResult = await updatePcrPo(id, { + pcrApprovalStatus: 'APPROVED', + pcrResponseDate: new Date() + }); + + if (!updateResult.success) { + console.error(`❌ PCR 승인 상태 업데이트 실패: ${updateResult.error}`); + return { success: false, error: updateResult.error } + } + + // 관련 페이지들 재검증 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + console.log(`✅ PCR 승인 완료: ${confirmData.PCR_REQ}-${confirmData.PCR_REQ_SEQ}`); + + return { + success: true, + message: "PCR이 성공적으로 승인되었습니다", + data: { + confirmResult, + updateResult: updateResult.data + } + } + } catch (error) { + console.error("PCR 승인 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR 승인에 실패했습니다" + } + } +} + +/** + * PCR 거절 Server Action + */ +export async function rejectPcrAction(id: number, reason: string) { + try { + // PCR 데이터 조회 + const pcrData = await getPcrPoById(id); + if (!pcrData) { + return { success: false, error: "PCR 데이터를 찾을 수 없습니다" } + } + + // PCR 확인 데이터 구성 + const confirmData = { + PCR_REQ: pcrData.poContractNumber, // PO 계약번호를 PCR 요청번호로 사용 + PCR_REQ_SEQ: String(pcrData.id).padStart(5, '0'), // ID를 순번으로 사용 + PCR_DEC_DATE: format(new Date(), 'yyyyMMdd'), // 오늘 날짜 + EBELN: pcrData.poContractNumber, // PO 계약번호 + EBELP: pcrData.revItemNumber || '00010', // REV/품번 또는 기본값 + PCR_STATUS: 'R', // 거절 상태 + REJ_CD: 'REJECTED', + REJ_RSN: reason + }; + + console.log(`🚫 PCR 거절 요청 시작: ${confirmData.PCR_REQ}-${confirmData.PCR_REQ_SEQ}`); + + // ECC로 거절 요청 전송 + const confirmResult = await confirmPCR(confirmData); + + if (!confirmResult.success) { + console.error(`❌ PCR 거절 실패: ${confirmResult.message}`); + return { success: false, error: confirmResult.message } + } + + // DB에서 PCR 상태 업데이트 + const updateResult = await updatePcrPo(id, { + pcrApprovalStatus: 'REJECTED', + rejectionReason: reason, + pcrResponseDate: new Date() + }); + + if (!updateResult.success) { + console.error(`❌ PCR 거절 상태 업데이트 실패: ${updateResult.error}`); + return { success: false, error: updateResult.error } + } + + // 관련 페이지들 재검증 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + console.log(`✅ PCR 거절 완료: ${confirmData.PCR_REQ}-${confirmData.PCR_REQ_SEQ}`); + + return { + success: true, + message: "PCR이 성공적으로 거절되었습니다", + data: { + confirmResult, + updateResult: updateResult.data + } + } + } catch (error) { + console.error("PCR 거절 액션 오류:", error) + return { + success: false, + error: error instanceof Error ? error.message : "PCR 거절에 실패했습니다" + } + } +} + +/** + * PCR 거절사유 수정 액션 + */ +export async function updatePcrRejectionReasonAction(data: { + id: number; + rejectionReason?: string; +}) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다."); + } + + const now = new Date(); + const updateData = { + rejectionReason: data.rejectionReason, + updatedBy: session.user.id, + updatedAt: now, + }; + + const result = await db + .update(pcrPo) + .set(updateData) + .where(eq(pcrPo.id, data.id)) + .returning(); + + if (result.length === 0) { + throw new Error("PCR 데이터를 찾을 수 없습니다."); + } + + // 캐시 무효화 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { + success: true, + data: result[0], + }; + } catch (error) { + console.error("PCR 수정 액션 오류:", error); + return { + success: false, + error: error instanceof Error ? error.message : "PCR 수정에 실패했습니다" + }; + } +} \ No newline at end of file diff --git a/lib/pcr/service.ts b/lib/pcr/service.ts new file mode 100644 index 00000000..8a85a9b1 --- /dev/null +++ b/lib/pcr/service.ts @@ -0,0 +1,753 @@ +"use server" + +import { unstable_noStore, revalidatePath } from "next/cache"; +import db from "@/db/db"; +import { + pcrPo, + pcrPr, + vendors, + pcrPrAttachment +} from "@/db/schema"; +import { and, desc, eq, ilike, or, sql, inArray, count, asc } from "drizzle-orm"; +import { getErrorMessage } from "@/lib/handle-error"; +import { + type PcrPoFilters, +} from "./types"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; + +// 정렬 타입 정의 +// 의도적으로 any 사용 - drizzle ORM의 orderBy 타입이 복잡함 +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type OrderByType = any; + +/** + * PCR_PO 데이터를 조회하는 기본 함수 (JOIN 포함) + */ +function selectPcrPoWithJoin() { + return db + .select({ + // PCR_PO 필드들 + id: pcrPo.id, + pcrApprovalStatus: pcrPo.pcrApprovalStatus, + changeType: pcrPo.changeType, + details: pcrPo.details, + project: pcrPo.project, + pcrRequestDate: pcrPo.pcrRequestDate, + poContractNumber: pcrPo.poContractNumber, + revItemNumber: pcrPo.revItemNumber, + purchaseContractManager: pcrPo.purchaseContractManager, + pcrCreator: pcrPo.pcrCreator, + poContractAmountBefore: pcrPo.poContractAmountBefore, + poContractAmountAfter: pcrPo.poContractAmountAfter, + contractCurrency: pcrPo.contractCurrency, + pcrReason: pcrPo.pcrReason, + detailsReason: pcrPo.detailsReason, + rejectionReason: pcrPo.rejectionReason, + pcrResponseDate: pcrPo.pcrResponseDate, + vendorId: pcrPo.vendorId, + createdBy: pcrPo.createdBy, + updatedBy: pcrPo.updatedBy, + createdAt: pcrPo.createdAt, + updatedAt: pcrPo.updatedAt, + + // JOIN된 필드들 + vendorName: vendors.vendorName, + }) + .from(pcrPo) + .leftJoin(vendors, eq(pcrPo.vendorId, vendors.id)) + .$dynamic(); +} + +/** + * PCR_PR 데이터를 조회하는 기본 함수 + */ +function selectPcrPrWithJoin() { + return db + .select({ + // PCR_PR 필드들 + id: pcrPr.id, + materialNumber: pcrPr.materialNumber, + materialDetails: pcrPr.materialDetails, + quantityBefore: pcrPr.quantityBefore, + quantityAfter: pcrPr.quantityAfter, + weightBefore: pcrPr.weightBefore, + weightAfter: pcrPr.weightAfter, + subcontractorWeightBefore: pcrPr.subcontractorWeightBefore, + subcontractorWeightAfter: pcrPr.subcontractorWeightAfter, + supplierWeightBefore: pcrPr.supplierWeightBefore, + supplierWeightAfter: pcrPr.supplierWeightAfter, + specDrawingBefore: pcrPr.specDrawingBefore, + specDrawingAfter: pcrPr.specDrawingAfter, + initialPoContractDate: pcrPr.initialPoContractDate, + specChangeDate: pcrPr.specChangeDate, + poContractModifiedDate: pcrPr.poContractModifiedDate, + confirmationDate: pcrPr.confirmationDate, + designManager: pcrPr.designManager, + poContractNumber: pcrPr.poContractNumber, + createdBy: pcrPr.createdBy, + updatedBy: pcrPr.updatedBy, + createdAt: pcrPr.createdAt, + updatedAt: pcrPr.updatedAt, + }) + .from(pcrPr) + .$dynamic(); +} + +/** + * PCR_PO 데이터를 조회하고 카운트하는 함수들 + */ +const selectPcrPoWithJoinDynamic = selectPcrPoWithJoin(); +const countPcrPoWithJoin = () => + db + .select({ count: count() }) + .from(pcrPo) + .leftJoin(vendors, eq(pcrPo.vendorId, vendors.id)); + +/** + * PCR_PR 데이터를 조회하고 카운트하는 함수들 + */ +const selectPcrPrWithJoinDynamic = selectPcrPrWithJoin(); + +/** + * PCR_PO 목록 조회 (EvcP 페이지용 - 모든 데이터 조회) + */ +export async function getPcrPoList(input: { + page?: number; + perPage?: number; + sort?: OrderByType; + filters?: PcrPoFilters; + search?: string; +}) { + unstable_noStore(); + + try { + const { page = 1, perPage = 10, sort, filters, search } = input; + + const offset = (page - 1) * perPage; + + // 필터링 조건 구성 + const whereConditions: any[] = []; + + if (filters) { + if (filters.pcrApprovalStatus) { + whereConditions.push(eq(pcrPo.pcrApprovalStatus, filters.pcrApprovalStatus)); + } + if (filters.changeType) { + whereConditions.push(eq(pcrPo.changeType, filters.changeType)); + } + if (filters.project) { + whereConditions.push(ilike(pcrPo.project, `%${filters.project}%`)); + } + if (filters.poContractNumber) { + whereConditions.push(ilike(pcrPo.poContractNumber, `%${filters.poContractNumber}%`)); + } + if (filters.vendorId) { + whereConditions.push(eq(pcrPo.vendorId, filters.vendorId)); + } + if (filters.startDate) { + whereConditions.push(sql`${pcrPo.pcrRequestDate} >= ${filters.startDate}`); + } + if (filters.endDate) { + whereConditions.push(sql`${pcrPo.pcrRequestDate} <= ${filters.endDate}`); + } + } + + // 검색 조건 + if (search) { + whereConditions.push( + or( + ilike(pcrPo.poContractNumber, `%${search}%`), + ilike(pcrPo.project, `%${search}%`), + ilike(pcrPo.pcrCreator, `%${search}%`), + ilike(vendors.vendorName, `%${search}%`) + ) + ); + } + + const whereClause = whereConditions.length > 0 ? and(...whereConditions) : undefined; + + // 데이터 조회 + const data = await selectPcrPoWithJoinDynamic + .where(whereClause) + .orderBy(sort ? sort : desc(pcrPo.createdAt)) + .limit(perPage) + .offset(offset); + + // 전체 카운트 + const totalCount = await countPcrPoWithJoin() + .where(whereClause) + .then((result) => result[0]?.count ?? 0); + + return { + data, + totalCount, + pageCount: Math.ceil(totalCount / perPage), + }; + } catch (error) { + console.error("PCR_PO 목록 조회 오류:", error); + throw new Error("PCR_PO 데이터를 조회하는데 실패했습니다."); + } +} + +/** + * PCR_PO 목록 조회 (Partners 페이지용 - 현재 사용자의 vendorId에 해당하는 데이터만 조회) + */ +export async function getPcrPoListForPartners(input: { + page?: number; + perPage?: number; + sort?: OrderByType; + filters?: Omit; + search?: string; + vendorId: number; +}) { + unstable_noStore(); + + try { + const { page = 1, perPage = 10, sort, filters, search, vendorId } = input; + + const offset = (page - 1) * perPage; + + // 필터링 조건 구성 (vendorId는 필수로 포함) + const whereConditions = [eq(pcrPo.vendorId, vendorId)]; + + if (filters) { + if (filters.pcrApprovalStatus) { + whereConditions.push(eq(pcrPo.pcrApprovalStatus, filters.pcrApprovalStatus)); + } + if (filters.changeType) { + whereConditions.push(eq(pcrPo.changeType, filters.changeType)); + } + if (filters.project) { + whereConditions.push(ilike(pcrPo.project, `%${filters.project}%`)); + } + if (filters.poContractNumber) { + whereConditions.push(ilike(pcrPo.poContractNumber, `%${filters.poContractNumber}%`)); + } + if (filters.startDate) { + whereConditions.push(sql`${pcrPo.pcrRequestDate} >= ${filters.startDate}`); + } + if (filters.endDate) { + whereConditions.push(sql`${pcrPo.pcrRequestDate} <= ${filters.endDate}`); + } + } + + // 검색 조건 + if (search) { + whereConditions.push( + or( + ilike(pcrPo.poContractNumber, `%${search}%`), + ilike(pcrPo.project, `%${search}%`), + ilike(pcrPo.pcrCreator, `%${search}%`) + ) + ); + } + + const whereClause = and(...whereConditions); + + // 데이터 조회 + const data = await selectPcrPoWithJoinDynamic + .where(whereClause) + .orderBy(sort ? sort : desc(pcrPo.createdAt)) + .limit(perPage) + .offset(offset); + + // 전체 카운트 + const totalCount = await countPcrPoWithJoin() + .where(whereClause) + .then((result) => result[0]?.count ?? 0); + + return { + data, + totalCount, + pageCount: Math.ceil(totalCount / perPage), + }; + } catch (error) { + console.error("PCR_PO Partners 목록 조회 오류:", error); + throw new Error("PCR_PO 데이터를 조회하는데 실패했습니다."); + } +} + +/** + * 특정 PO/계약 번호에 해당하는 PCR_PR 데이터 조회 + */ +export async function getPcrPrListByPoContractNumber(poContractNumber: string) { + unstable_noStore(); + + try { + // PCR_PR 데이터 조회 + const pcrPrData = await selectPcrPrWithJoinDynamic + .where(eq(pcrPr.poContractNumber, poContractNumber)) + .orderBy(asc(pcrPr.materialNumber)); + + // 첨부파일 데이터 조회 (해당 PCR_PR들의 ID로) + const pcrPrIds = pcrPrData.map(item => item.id); + let attachments: any[] = []; + + if (pcrPrIds.length > 0) { + const attachmentsResult = await db + .select() + .from(pcrPrAttachment) + .where(inArray(pcrPrAttachment.pcrPrId, pcrPrIds)) + .orderBy(pcrPrAttachment.pcrPrId, pcrPrAttachment.type); + + attachments = attachmentsResult; + } + + // PCR_PR 데이터에 첨부파일 추가 + const dataWithAttachments = pcrPrData.map(pcrPrItem => ({ + ...pcrPrItem, + attachments: attachments.filter(att => att.pcrPrId === pcrPrItem.id) + })); + + return dataWithAttachments; + } catch (error) { + console.error("PCR_PR 조회 오류:", error); + throw new Error("PCR_PR 데이터를 조회하는데 실패했습니다."); + } +} + +/** + * PCR_PO 단일 조회 + */ +export async function getPcrPoById(id: number) { + unstable_noStore(); + + try { + const data = await selectPcrPoWithJoinDynamic + .where(eq(pcrPo.id, id)) + .limit(1); + + if (data.length === 0) { + throw new Error("PCR_PO 데이터를 찾을 수 없습니다."); + } + + return data[0]; + } catch (error) { + console.error("PCR_PO 단일 조회 오류:", error); + throw new Error("PCR_PO 데이터를 조회하는데 실패했습니다."); + } +} + +/** + * PCR_PR 단일 조회 + */ +export async function getPcrPrById(id: number) { + unstable_noStore(); + + try { + const data = await selectPcrPrWithJoinDynamic + .where(eq(pcrPr.id, id)) + .limit(1); + + if (data.length === 0) { + throw new Error("PCR_PR 데이터를 찾을 수 없습니다."); + } + + return data[0]; + } catch (error) { + console.error("PCR_PR 단일 조회 오류:", error); + throw new Error("PCR_PR 데이터를 조회하는데 실패했습니다."); + } +} + +/** + * PCR_PO 생성 + */ +export async function createPcrPo(data: { + pcrApprovalStatus?: string; + changeType?: string; + details?: string; + project?: string; + pcrRequestDate: Date; + poContractNumber: string; + revItemNumber?: string; + purchaseContractManager?: string; + pcrCreator?: string; + poContractAmountBefore?: number; + poContractAmountAfter?: number; + contractCurrency?: string; + pcrReason?: string; + detailsReason?: string; + rejectionReason?: string; + pcrResponseDate?: Date; + vendorId?: number; +}) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다."); + } + + const now = new Date(); + const insertData = { + ...data, + pcrApprovalStatus: data.pcrApprovalStatus || 'PENDING', + changeType: data.changeType || 'OTHER', + contractCurrency: data.contractCurrency || 'KRW', + createdBy: session.user.id, + updatedBy: session.user.id, + createdAt: now, + updatedAt: now, + }; + + const result = await db.insert(pcrPo).values(insertData).returning(); + + // 캐시 무효화 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { + success: true, + data: result[0], + message: "PCR_PO가 성공적으로 생성되었습니다." + }; + } catch (error) { + console.error("PCR_PO 생성 오류:", error); + return { + success: false, + error: getErrorMessage(error), + }; + } +} + +/** + * PCR_PR 생성 + */ +export async function createPcrPr(data: { + materialNumber: string; + materialDetails?: string; + quantityBefore?: number; + quantityAfter?: number; + weightBefore?: number; + weightAfter?: number; + subcontractorWeightBefore?: number; + subcontractorWeightAfter?: number; + supplierWeightBefore?: number; + supplierWeightAfter?: number; + specDrawingBefore?: string; + specDrawingAfter?: string; + initialPoContractDate?: Date; + specChangeDate?: Date; + poContractModifiedDate?: Date; + confirmationDate?: Date; + designManager?: string; + poContractNumber: string; +}) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다."); + } + + const now = new Date(); + const insertData = { + ...data, + createdBy: session.user.id, + updatedBy: session.user.id, + createdAt: now, + updatedAt: now, + }; + + const result = await db.insert(pcrPr).values(insertData).returning(); + + // 캐시 무효화 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { + success: true, + data: result[0], + message: "PCR_PR이 성공적으로 생성되었습니다." + }; + } catch (error) { + console.error("PCR_PR 생성 오류:", error); + return { + success: false, + error: getErrorMessage(error), + }; + } +} + +/** + * PCR_PO 업데이트 + */ +export async function updatePcrPo(id: number, data: Partial<{ + pcrApprovalStatus: string; + changeType: string; + details: string; + project: string; + pcrRequestDate: Date; + poContractNumber: string; + revItemNumber: string; + purchaseContractManager: string; + pcrCreator: string; + poContractAmountBefore: number; + poContractAmountAfter: number; + contractCurrency: string; + pcrReason: string; + detailsReason: string; + rejectionReason: string; + pcrResponseDate: Date; + vendorId: number; +}>) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다."); + } + + const updateData = { + ...data, + updatedBy: session.user.id, + updatedAt: new Date(), + }; + + const result = await db + .update(pcrPo) + .set(updateData) + .where(eq(pcrPo.id, id)) + .returning(); + + if (result.length === 0) { + return { + success: false, + error: "PCR_PO 데이터를 찾을 수 없습니다.", + }; + } + + // 캐시 무효화 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { + success: true, + data: result[0], + message: "PCR_PO가 성공적으로 업데이트되었습니다." + }; + } catch (error) { + console.error("PCR_PO 업데이트 오류:", error); + return { + success: false, + error: getErrorMessage(error), + }; + } +} + +/** + * PCR_PR 업데이트 + */ +export async function updatePcrPr(id: number, data: Partial<{ + materialNumber: string; + materialDetails: string; + quantityBefore: number; + quantityAfter: number; + weightBefore: number; + weightAfter: number; + subcontractorWeightBefore: number; + subcontractorWeightAfter: number; + supplierWeightBefore: number; + supplierWeightAfter: number; + specDrawingBefore: string; + specDrawingAfter: string; + initialPoContractDate: Date; + specChangeDate: Date; + poContractModifiedDate: Date; + confirmationDate: Date; + designManager: string; + poContractNumber: string; +}>) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다."); + } + + const updateData = { + ...data, + updatedBy: session.user.id, + updatedAt: new Date(), + }; + + const result = await db + .update(pcrPr) + .set(updateData) + .where(eq(pcrPr.id, id)) + .returning(); + + if (result.length === 0) { + return { + success: false, + error: "PCR_PR 데이터를 찾을 수 없습니다.", + }; + } + + // 캐시 무효화 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { + success: true, + data: result[0], + message: "PCR_PR이 성공적으로 업데이트되었습니다." + }; + } catch (error) { + console.error("PCR_PR 업데이트 오류:", error); + return { + success: false, + error: getErrorMessage(error), + }; + } +} + +/** + * PCR_PO 삭제 + */ +export async function deletePcrPo(id: number) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다."); + } + + const result = await db + .delete(pcrPo) + .where(eq(pcrPo.id, id)) + .returning(); + + if (result.length === 0) { + return { + success: false, + error: "PCR_PO 데이터를 찾을 수 없습니다.", + }; + } + + // 캐시 무효화 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { + success: true, + message: "PCR_PO가 성공적으로 삭제되었습니다." + }; + } catch (error) { + console.error("PCR_PO 삭제 오류:", error); + return { + success: false, + error: getErrorMessage(error), + }; + } +} + +/** + * PCR_PR 삭제 + */ +export async function deletePcrPr(id: number) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다."); + } + + const result = await db + .delete(pcrPr) + .where(eq(pcrPr.id, id)) + .returning(); + + if (result.length === 0) { + return { + success: false, + error: "PCR_PR 데이터를 찾을 수 없습니다.", + }; + } + + // 캐시 무효화 + revalidatePath("/evcp/pcr"); + revalidatePath("/partners/pcr"); + + return { + success: true, + message: "PCR_PR이 성공적으로 삭제되었습니다." + }; + } catch (error) { + console.error("PCR_PR 삭제 오류:", error); + return { + success: false, + error: getErrorMessage(error), + }; + } +} + +/** + * 간단한 협력업체 목록 조회 (PCR 생성용) + */ +export async function getVendorsForPcr(input?: { + vendorId?: number; // 특정 vendorId만 조회 (Partners 페이지용) + limit?: number; // 조회 개수 제한 +}) { + unstable_noStore(); + + try { + const { vendorId, limit = 100 } = input || {}; + + let query = db + .select({ + id: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + }) + .from(vendors) + .orderBy(asc(vendors.vendorName)); + + // 특정 vendorId 필터링 (Partners 페이지용) + if (vendorId) { + query = query.where(eq(vendors.id, vendorId)); + } + + // 개수 제한 + if (limit) { + query = query.limit(limit); + } + + const result = await query; + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("협력업체 조회 오류:", error); + return { + success: false, + error: getErrorMessage(error), + data: [], + }; + } +} + +/** + * PCR_PR 첨부파일 조회 + */ +export async function getPcrPrAttachments(pcrPrId: number) { + unstable_noStore(); + + try { + const result = await db + .select() + .from(pcrPrAttachment) + .where(eq(pcrPrAttachment.pcrPrId, pcrPrId)) + .orderBy(pcrPrAttachment.type, pcrPrAttachment.createdAt); + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("PCR_PR 첨부파일 조회 오류:", error); + return { + success: false, + error: getErrorMessage(error), + data: [], + }; + } +} \ No newline at end of file diff --git a/lib/pcr/table/approve-reject-pcr-dialog.tsx b/lib/pcr/table/approve-reject-pcr-dialog.tsx new file mode 100644 index 00000000..065a30fa --- /dev/null +++ b/lib/pcr/table/approve-reject-pcr-dialog.tsx @@ -0,0 +1,231 @@ +"use client" + +import * as React from "react" +import { toast } from "sonner" +import { CheckCircle, XCircle, Loader2 } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Textarea } from "@/components/ui/textarea" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import * as z from "zod" +import { approvePcrAction, rejectPcrAction } from "@/lib/pcr/actions" +import { PcrPoData } from "@/lib/pcr/types" + +// 승인 다이얼로그 스키마 +const approveSchema = z.object({ + reason: z.string().optional(), +}) + +// 거절 다이얼로그 스키마 +const rejectSchema = z.object({ + reason: z.string().min(1, "거절 사유를 입력해주세요."), +}) + +type ApproveFormValues = z.infer +type RejectFormValues = z.infer + +interface ApproveRejectPcrDialogProps { + open?: boolean + onOpenChange?: (open: boolean) => void + pcrData?: PcrPoData | null + actionType: 'approve' | 'reject' + onSuccess?: () => void +} + +export function ApproveRejectPcrDialog({ + open, + onOpenChange, + pcrData, + actionType, + onSuccess, +}: ApproveRejectPcrDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [internalOpen, setInternalOpen] = React.useState(false) + + const dialogOpen = open !== undefined ? open : internalOpen + const setDialogOpen = onOpenChange || setInternalOpen + + const isApprove = actionType === 'approve' + + // 승인 폼 + const approveForm = useForm({ + resolver: zodResolver(approveSchema), + defaultValues: { + reason: "", + }, + }) + + // 거절 폼 + const rejectForm = useForm({ + resolver: zodResolver(rejectSchema), + defaultValues: { + reason: "", + }, + }) + + const currentForm = isApprove ? approveForm : rejectForm + + const handleSubmit = async (data: ApproveFormValues | RejectFormValues) => { + if (!pcrData) return + + try { + setIsLoading(true) + + const reason = 'reason' in data ? data.reason : undefined + const result = isApprove + ? await approvePcrAction(pcrData.id, reason) + : await rejectPcrAction(pcrData.id, reason || '') + + if (result.success) { + toast.success(result.message) + currentForm.reset() + setDialogOpen(false) + onSuccess?.() + } else { + toast.error(result.error || `${isApprove ? '승인' : '거절'}에 실패했습니다.`) + } + } catch (error) { + console.error(`PCR ${isApprove ? '승인' : '거절'} 오류:`, error) + toast.error(`${isApprove ? '승인' : '거절'} 중 오류가 발생했습니다.`) + } finally { + setIsLoading(false) + } + } + + if (!pcrData) return null + + return ( + + + +
+ {isApprove ? ( + + ) : ( + + )} + + PCR {isApprove ? '승인' : '거절'} 확인 + +
+ + 다음 PCR을 {isApprove ? '승인' : '거절'}하시겠습니까? + +
+ + {/* PCR 정보 표시 */} +
+
+
+ PO/계약번호: +

{pcrData.poContractNumber}

+
+
+ 프로젝트: +

{pcrData.project || '-'}

+
+
+ 변경 구분: +

{pcrData.changeType}

+
+
+ 요청일자: +

+ {pcrData.pcrRequestDate.toLocaleDateString('ko-KR')} +

+
+
+ {pcrData.details && ( +
+ 상세: +

{pcrData.details}

+
+ )} +
+ + {/* 승인/거절 사유 입력 폼 */} +
+ + ( + + + {isApprove ? '승인 사유 (선택)' : '거절 사유 (필수)'} + + +