From 871a6d46a769cbe9e87146434f4bcb2d6792ab81 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 30 Oct 2025 10:44:47 +0000 Subject: (최겸) 구매 PQ/실사 재개발(테스트 필요), 정규업체등록 결재 개발, 실사 의뢰 결재 후처리 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/site-visit/service.ts | 131 +++++++++++++++++++++++++++++ lib/site-visit/vendor-info-view-dialog.tsx | 110 +++++++++++++++++++----- 2 files changed, 219 insertions(+), 22 deletions(-) (limited to 'lib/site-visit') diff --git a/lib/site-visit/service.ts b/lib/site-visit/service.ts index d5e4a59b..1dc07c77 100644 --- a/lib/site-visit/service.ts +++ b/lib/site-visit/service.ts @@ -18,6 +18,137 @@ import { users } from "@/db/schema" +// 실사 ID로 모든 siteVisitRequests 조회 (복수 확정정보 지원) +export async function getAllSiteVisitRequestsForInvestigationAction(investigationId: number) { + try { + const confirmations = await db + .select({ + id: siteVisitRequests.id, + status: siteVisitRequests.status, + inspectionDuration: siteVisitRequests.inspectionDuration, + requestedStartDate: siteVisitRequests.requestedStartDate, + requestedEndDate: siteVisitRequests.requestedEndDate, + additionalRequests: siteVisitRequests.additionalRequests, + createdAt: siteVisitRequests.createdAt, + updatedAt: siteVisitRequests.updatedAt, + }) + .from(siteVisitRequests) + .where(eq(siteVisitRequests.investigationId, investigationId)) + .orderBy(desc(siteVisitRequests.createdAt)) + + return { success: true, confirmations } + } catch (error) { + console.error("실사 확정정보 조회 오류:", error) + return { success: false, error: "실사 확정정보 조회에 실패했습니다." } + } +} + +// 재방문 실사 요청을 위한 방문실사 생성 (동일 investigationId에 대해 여러 개 허용) +export async function createReinspectionSiteVisitAction(input: { + investigationId: number; + inspectionDuration: number; + requestedStartDate: Date; + requestedEndDate: Date; + shiAttendees: Record; // {userId: name} + vendorRequests: Record; + additionalRequests?: string; + investigationAddress: string; +}) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("Unauthorized"); + } + + // 실사 정보 확인 + const investigation = await db + .select() + .from(vendorInvestigations) + .where(eq(vendorInvestigations.id, input.investigationId)) + .limit(1); + + if (!investigation.length) { + return { + success: false, + error: "실사 정보를 찾을 수 없습니다." + }; + } + + // PQ 정보 확인 + const pqSubmission = await db + .select() + .from(vendorPQSubmissions) + .where(eq(vendorPQSubmissions.id, investigation[0].pqSubmissionId!)) + .limit(1); + + if (!pqSubmission.length) { + return { + success: false, + error: "PQ 정보를 찾을 수 없습니다." + }; + } + + // 방문실사 요청 생성 (재실사이므로 기존 확인 로직 생략) + const [newRequest] = await db + .insert(siteVisitRequests) + .values({ + investigationId: input.investigationId, + inspectionDuration: input.inspectionDuration, + requestedStartDate: input.requestedStartDate, + requestedEndDate: input.requestedEndDate, + shiAttendees: input.shiAttendees, + vendorRequests: input.vendorRequests, + additionalRequests: input.additionalRequests, + status: "REQUESTED", + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + // 벤더에게 이메일 발송 + const vendor = await db + .select({ email: vendors.email, vendorName: vendors.vendorName }) + .from(vendors) + .where(eq(vendors.id, investigation[0].vendorId)) + .limit(1); + + if (vendor.length && vendor[0].email) { + const headersList = await import("next/headers").then(m => m.headers()); + const host = headersList.get('host') || 'localhost:3000'; + const portalUrl = process.env.NEXTAUTH_URL || `http://${host}`; + + await sendEmail({ + to: vendor[0].email, + subject: `[eVCP] 재실사 방문요청 - ${vendor[0].vendorName}`, + template: "site-visit-request", + context: { + vendorName: vendor[0].vendorName, + inspectionDuration: input.inspectionDuration, + requestedStartDate: format(input.requestedStartDate, "yyyy년 MM월 dd일"), + requestedEndDate: format(input.requestedEndDate, "yyyy년 MM월 dd일"), + investigationAddress: input.investigationAddress, + additionalRequests: input.additionalRequests || "", + reviewUrl: `${portalUrl}/evcp/vendor-investigation`, + year: new Date().getFullYear(), + } + }); + } + + revalidatePath("/evcp/vendor-investigation"); + + return { + success: true, + siteVisitRequestId: newRequest.id + }; + } catch (error) { + console.error("재실사 방문요청 생성 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }; + } +} + // 방문실사 요청 서버 액션 export async function createSiteVisitRequestAction(input: { investigationId: number; diff --git a/lib/site-visit/vendor-info-view-dialog.tsx b/lib/site-visit/vendor-info-view-dialog.tsx index 431069b3..48aefeb0 100644 --- a/lib/site-visit/vendor-info-view-dialog.tsx +++ b/lib/site-visit/vendor-info-view-dialog.tsx @@ -15,6 +15,7 @@ import { } from "@/components/ui/dialog" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" import { toast } from "sonner" interface VendorInfo { @@ -53,46 +54,63 @@ interface VendorInfoViewDialogProps { isOpen: boolean onClose: () => void siteVisitRequestId: number | null + investigationId?: number | null // 실사 ID 추가 - 여러 확정정보 조회용 + isReinspection?: boolean // 재실사 모드 플래그 } export function VendorInfoViewDialog({ isOpen, onClose, siteVisitRequestId, + investigationId, }: VendorInfoViewDialogProps) { const [data, setData] = React.useState(null) const [attachments, setAttachments] = React.useState([]) + const [allConfirmations, setAllConfirmations] = React.useState([]) // 여러 확정정보 const [isLoading, setIsLoading] = React.useState(false) // 데이터 로드 React.useEffect(() => { - if (isOpen && siteVisitRequestId) { - loadVendorInfo() + if (isOpen && (siteVisitRequestId || investigationId)) { + loadData() } - }, [isOpen, siteVisitRequestId]) + }, [isOpen, siteVisitRequestId, investigationId]) - const loadVendorInfo = async () => { - if (!siteVisitRequestId) return + const loadData = async () => { + if (!siteVisitRequestId && !investigationId) return setIsLoading(true) try { - const { getVendorSiteVisitInfoAction } = await import("./service") - const result = await getVendorSiteVisitInfoAction(siteVisitRequestId) - - if (result.success && result.data) { - setData(result.data.vendorInfo) - setAttachments(result.data.attachments || []) - } else { - toast.error("협력업체 정보를 불러올 수 없습니다.") + // 단일 확정정보 조회 (기존) + if (siteVisitRequestId) { + const { getVendorSiteVisitInfoAction } = await import("./service") + const result = await getVendorSiteVisitInfoAction(siteVisitRequestId) + + if (result.success && result.data) { + setData(result.data.vendorInfo) + setAttachments(result.data.attachments || []) + } else { + toast.error("협력업체 정보를 불러올 수 없습니다.") + } + } + + // 여러 확정정보 조회 (신규 - 실사 ID로 모든 siteVisitRequests 조회) + if (investigationId) { + const { getAllSiteVisitRequestsForInvestigationAction } = await import("./service") + const result = await getAllSiteVisitRequestsForInvestigationAction(investigationId) + if (result.success) { + setAllConfirmations(result.confirmations || []) + } } } catch (error) { - console.error("협력업체 정보 로드 오류:", error) - toast.error("협력업체 정보를 불러오는 중 오류가 발생했습니다.") + console.error("데이터 로드 오류:", error) + toast.error("데이터를 불러오는 중 오류가 발생했습니다.") } finally { setIsLoading(false) } } + return ( !open && onClose()}> @@ -110,10 +128,11 @@ export function VendorInfoViewDialog({

협력업체 정보를 불러오는 중...

- ) : data ? ( + ) : (data || allConfirmations.length > 0) ? (
- {/* 협력업체 정보 */} - + {/* 협력업체 정보 - 단일 확정정보 조회 시에만 표시 */} + {data && ( + @@ -173,6 +192,7 @@ export function VendorInfoViewDialog({
+ )} {/* 첨부파일 */} {attachments.length > 0 && ( @@ -226,8 +246,54 @@ export function VendorInfoViewDialog({ )} + {/* 실사 실시 확정정보 (복수 지원) */} + {allConfirmations.length > 0 && ( +
+

실사 실시 확정정보

+ {allConfirmations.map((confirmation, index) => ( + + + + + + 실사 확정정보 #{index + 1} + + + {confirmation.status === "COMPLETED" ? "완료" : "진행중"} + + + + +
+
+ 실사 기간: {confirmation.inspectionDuration}일 +
+
+ 요청 시작일: + {confirmation.requestedStartDate ? formatDate(confirmation.requestedStartDate, "kr") : "미정"} +
+
+ 요청 종료일: + {confirmation.requestedEndDate ? formatDate(confirmation.requestedEndDate, "kr") : "미정"} +
+
+ 생성일: {formatDate(confirmation.createdAt, "kr")} +
+ {confirmation.additionalRequests && ( +
+ 추가 요청사항: +
{confirmation.additionalRequests}
+
+ )} +
+
+
+ ))} +
+ )} + {/* 기타 정보 */} - {data.otherInfo && ( + {data?.otherInfo && ( @@ -236,7 +302,7 @@ export function VendorInfoViewDialog({ -

{data.otherInfo}

+

{data?.otherInfo}

)} @@ -253,8 +319,8 @@ export function VendorInfoViewDialog({
-
제출일: {formatDate(data.submittedAt, "kr")}
-
첨부파일: {data.hasAttachments ? "있음" : "없음"}
+
제출일: {formatDate(data?.submittedAt, "kr")}
+
첨부파일: {data?.hasAttachments ? "있음" : "없음"}
-- cgit v1.2.3