From 8a19a6fa336768d8b6712752c9d713360067ecb0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 8 Dec 2025 08:45:20 +0000 Subject: (최겸) 구매 피드백 수정, 안전담당자, pq항목 내 첨부, 내외자 구분, 도로명주소 api 반영(운영기준) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/site-visit/client-site-visit-wrapper.tsx | 1 + lib/site-visit/service.ts | 5 +- lib/site-visit/site-visit-detail-dialog.tsx | 583 +++++++++++++++------------ 3 files changed, 328 insertions(+), 261 deletions(-) (limited to 'lib/site-visit') diff --git a/lib/site-visit/client-site-visit-wrapper.tsx b/lib/site-visit/client-site-visit-wrapper.tsx index e2664ac3..ab63fa0b 100644 --- a/lib/site-visit/client-site-visit-wrapper.tsx +++ b/lib/site-visit/client-site-visit-wrapper.tsx @@ -100,6 +100,7 @@ interface SiteVisitRequest { // 협력업체 정보 vendorInfo?: { id: number + country?: string siteVisitRequestId: number factoryName: string factoryLocation: string diff --git a/lib/site-visit/service.ts b/lib/site-visit/service.ts index 684e73f1..54f2bf54 100644 --- a/lib/site-visit/service.ts +++ b/lib/site-visit/service.ts @@ -593,6 +593,7 @@ export async function getSiteVisitRequestAction(investigationId: number) { vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, vendorEmail: vendors.email, + vendorCountry: vendors.country, }) .from(siteVisitRequests) .leftJoin( @@ -658,7 +659,9 @@ export async function getSiteVisitRequestAction(investigationId: number) { ...item, shiAttendees: item.shiAttendees as Record | null, vendorRequests: item.vendorRequests as Record | null, - vendorInfo, + vendorInfo: vendorInfo + ? { ...vendorInfo, country: (item as any).vendorCountry || "" } + : null, shiAttachments, }; }) diff --git a/lib/site-visit/site-visit-detail-dialog.tsx b/lib/site-visit/site-visit-detail-dialog.tsx index 7788454a..74e749c4 100644 --- a/lib/site-visit/site-visit-detail-dialog.tsx +++ b/lib/site-visit/site-visit-detail-dialog.tsx @@ -1,261 +1,324 @@ -"use client" - -import * as React from "react" -import { format } from "date-fns" -import { ko } from "date-fns/locale" -import { FileText, Download } from "lucide-react" -import { toast } from "sonner" - -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { Separator } from "@/components/ui/separator" -import { formatDate } from "../utils" - -interface SiteVisitRequest { - id: number - investigationId: number - requesterId: number | null - inspectionDuration: string | null - requestedStartDate: Date | null - requestedEndDate: Date | null - shiAttendees: Record | null - shiAttendeeDetails?: string | null - vendorRequests: Record | null - additionalRequests: string | null - status: string - sentAt: Date | null - createdAt: Date - updatedAt: Date - - // 실사정보 - investigationMethod: string | null // QM담당자가 작성한 실사방법 - investigationAddress: string | null - investigationNotes: string | null - forecastedAt: Date | null - actualAt: Date | null - result: string | null - resultNotes: string | null - - // PQ 정보 - pqItems: string | null | Array<{itemCode: string, itemName: string}> - - // 요청자 정보 - requesterName: string | null - requesterEmail: string | null - requesterTitle: string | null - - // QM 매니저 정보 - qmManagerName: string | null - qmManagerEmail: string | null - qmManagerTitle: string | null - - // 협력업체 정보 - vendorInfo?: { - id: number - siteVisitRequestId: number - factoryName: string - factoryLocation: string - factoryAddress: string - factoryPicName: string - factoryPicPhone: string - factoryPicEmail: string - factoryDirections: string | null - accessProcedure: string | null - hasAttachments: boolean - otherInfo: string | null - submittedAt: Date - submittedBy: number - createdAt: Date - updatedAt: Date - } | null - - // SHI 첨부파일 - shiAttachments?: Array<{ - id: number - siteVisitRequestId: number - vendorSiteVisitInfoId: number | null - fileName: string - originalFileName: string - filePath: string - fileSize: number - mimeType: string - createdAt: Date - updatedAt: Date - }> | null -} - -interface SiteVisitDetailDialogProps { - isOpen: boolean - onOpenChange: (open: boolean) => void - selectedRequest: SiteVisitRequest | null -} - -export function SiteVisitDetailDialog({ - isOpen, - onOpenChange, - selectedRequest, -}: SiteVisitDetailDialogProps) { - - return ( - - - - 방문실사 상세 정보 - - 작성한 방문실사 정보의 상세 내용입니다. - - - - {selectedRequest && ( -
- {/* 기본 정보 */} - - {/* 협력업체 정보 */} - {selectedRequest.vendorInfo && ( - <> - -
-

작성한 협력업체 정보

-
-
-
-
-

공장 기본 정보

-
-
공장명: {selectedRequest.vendorInfo.factoryName}
-
공장위치: {selectedRequest.vendorInfo.factoryLocation}
-
공장주소: {selectedRequest.vendorInfo.factoryAddress}
-
-
- -
-

공장 담당자 정보

-
-
이름: {selectedRequest.vendorInfo.factoryPicName}
-
전화번호: {selectedRequest.vendorInfo.factoryPicPhone}
-
이메일: {selectedRequest.vendorInfo.factoryPicEmail}
-
-
-
- -
- {selectedRequest.vendorInfo.factoryDirections && ( -
-

공장 가는 법

-
-

{selectedRequest.vendorInfo.factoryDirections}

-
-
- )} - - {selectedRequest.vendorInfo.accessProcedure && ( -
-

공장 출입절차

-
-

{selectedRequest.vendorInfo.accessProcedure}

-
-
- )} -
-
- - {/* 기타 정보 */} - {selectedRequest.vendorInfo.otherInfo && ( -
-

기타 정보

-
-

{selectedRequest.vendorInfo.otherInfo}

-
-
- )} - - {/* 제출 정보 */} -
-

제출 정보

-
-
제출일: {formatDate(selectedRequest.vendorInfo.submittedAt, "kr")}
-
첨부파일: {selectedRequest.vendorInfo.hasAttachments ? "있음" : "없음"}
-
-
-
-
- - )} - - - - {/* SHI 첨부파일 */} - {selectedRequest.shiAttachments && selectedRequest.shiAttachments.length > 0 && ( - <> -
-

SHI 첨부파일 ({selectedRequest.shiAttachments.length}개)

-
-
- {selectedRequest.shiAttachments.map((attachment) => ( -
-
- - {attachment.originalFileName} - - ({Math.round((attachment.fileSize || 0) / 1024)}KB) - -
- -
- ))} -
-
-
- - )} - - {/* 추가 요청사항 */} - {selectedRequest.additionalRequests && ( - <> -
-

SHI 추가 요청사항

-
-

{selectedRequest.additionalRequests}

-
-
- - - )} -
- )} -
-
- ) +"use client" + +import * as React from "react" +import { format } from "date-fns" +import { ko } from "date-fns/locale" +import { FileText, Download } from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Separator } from "@/components/ui/separator" +import { formatDate } from "../utils" + +interface SiteVisitRequest { + id: number + investigationId: number + requesterId: number | null + inspectionDuration: string | null + requestedStartDate: Date | null + requestedEndDate: Date | null + shiAttendees: Record | null + shiAttendeeDetails?: string | null + vendorRequests: Record | null + additionalRequests: string | null + status: string + sentAt: Date | null + createdAt: Date + updatedAt: Date + + // 실사정보 + investigationMethod: string | null // QM담당자가 작성한 실사방법 + investigationAddress: string | null + investigationNotes: string | null + forecastedAt: Date | null + actualAt: Date | null + result: string | null + resultNotes: string | null + + // PQ 정보 + pqItems: string | null | Array<{itemCode: string, itemName: string}> + + // 요청자 정보 + requesterName: string | null + requesterEmail: string | null + requesterTitle: string | null + + // QM 매니저 정보 + qmManagerName: string | null + qmManagerEmail: string | null + qmManagerTitle: string | null + + // 협력업체 정보 + vendorInfo?: { + id: number + country?: string + siteVisitRequestId: number + factoryName: string + factoryLocation: string + factoryAddress: string + factoryPicName: string + factoryPicPhone: string + factoryPicEmail: string + factoryDirections: string | null + accessProcedure: string | null + hasAttachments: boolean + otherInfo: string | null + submittedAt: Date + submittedBy: number + createdAt: Date + updatedAt: Date + } | null + + // SHI 첨부파일 + shiAttachments?: Array<{ + id: number + siteVisitRequestId: number + vendorSiteVisitInfoId: number | null + fileName: string + originalFileName: string + filePath: string + fileSize: number + mimeType: string + createdAt: Date + updatedAt: Date + }> | null +} + +interface SiteVisitDetailDialogProps { + isOpen: boolean + onOpenChange: (open: boolean) => void + selectedRequest: SiteVisitRequest | null +} + +export function SiteVisitDetailDialog({ + isOpen, + onOpenChange, + selectedRequest, +}: SiteVisitDetailDialogProps) { + const vendorCountry = (selectedRequest?.vendorInfo as any)?.country || "" + const isDomestic = vendorCountry === "KR" + + const [factoryLocation, setFactoryLocation] = React.useState( + selectedRequest?.vendorInfo?.factoryLocation || "" + ) + const [factoryAddress, setFactoryAddress] = React.useState( + selectedRequest?.vendorInfo?.factoryAddress || "" + ) + + React.useEffect(() => { + setFactoryLocation(selectedRequest?.vendorInfo?.factoryLocation || "") + setFactoryAddress(selectedRequest?.vendorInfo?.factoryAddress || "") + }, [selectedRequest?.vendorInfo?.factoryLocation, selectedRequest?.vendorInfo?.factoryAddress]) + + React.useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (!event.data || event.data.type !== "JUSO_SELECTED") return + const { roadAddrPart1, roadAddrPart2, addrDetail } = event.data.payload || {} + const combinedRoad = [roadAddrPart1, roadAddrPart2].filter(Boolean).join(" ").trim() + setFactoryLocation(combinedRoad || factoryLocation) + setFactoryAddress(addrDetail || factoryAddress) + } + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [factoryLocation, factoryAddress]) + + const handleJusoSearch = () => { + window.open( + "/api/juso", + "jusoSearch", + "width=570,height=420,scrollbars=yes,resizable=yes" + ) + } + + return ( + + + + 방문실사 상세 정보 + + 작성한 방문실사 정보의 상세 내용입니다. + + + + {selectedRequest && ( +
+ {/* 기본 정보 */} + + {/* 협력업체 정보 */} + {selectedRequest.vendorInfo && ( + <> + +
+

작성한 협력업체 정보

+
+
+
+
+

공장 기본 정보

+
+
공장명: {selectedRequest.vendorInfo.factoryName}
+
+ 공장위치(도로명): + {isDomestic && ( + + )} +
+ setFactoryLocation(e.target.value)} + readOnly={!isDomestic} + className={!isDomestic ? "" : "bg-muted text-muted-foreground"} + placeholder="도로명 주소" + /> +
공장주소(상세):
+ setFactoryAddress(e.target.value)} + readOnly={!isDomestic} + className={!isDomestic ? "" : "bg-muted text-muted-foreground"} + placeholder="상세 주소" + /> +
+
+ +
+

공장 담당자 정보

+
+
이름: {selectedRequest.vendorInfo.factoryPicName}
+
전화번호: {selectedRequest.vendorInfo.factoryPicPhone}
+
이메일: {selectedRequest.vendorInfo.factoryPicEmail}
+
+
+
+ +
+ {selectedRequest.vendorInfo.factoryDirections && ( +
+

공장 가는 법

+
+

{selectedRequest.vendorInfo.factoryDirections}

+
+
+ )} + + {selectedRequest.vendorInfo.accessProcedure && ( +
+

공장 출입절차

+
+

{selectedRequest.vendorInfo.accessProcedure}

+
+
+ )} +
+
+ + {/* 기타 정보 */} + {selectedRequest.vendorInfo.otherInfo && ( +
+

기타 정보

+
+

{selectedRequest.vendorInfo.otherInfo}

+
+
+ )} + + {/* 제출 정보 */} +
+

제출 정보

+
+
제출일: {formatDate(selectedRequest.vendorInfo.submittedAt, "kr")}
+
첨부파일: {selectedRequest.vendorInfo.hasAttachments ? "있음" : "없음"}
+
+
+
+
+ + )} + + + + {/* SHI 첨부파일 */} + {selectedRequest.shiAttachments && selectedRequest.shiAttachments.length > 0 && ( + <> +
+

SHI 첨부파일 ({selectedRequest.shiAttachments.length}개)

+
+
+ {selectedRequest.shiAttachments.map((attachment) => ( +
+
+ + {attachment.originalFileName} + + ({Math.round((attachment.fileSize || 0) / 1024)}KB) + +
+ +
+ ))} +
+
+
+ + )} + + {/* 추가 요청사항 */} + {selectedRequest.additionalRequests && ( + <> +
+

SHI 추가 요청사항

+
+

{selectedRequest.additionalRequests}

+
+
+ + + )} +
+ )} +
+
+ ) } \ No newline at end of file -- cgit v1.2.3 From b5ef49dce92c8994530f6ff670c81693c8716daf Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 8 Dec 2025 10:19:28 +0000 Subject: (최겸) 구매 방문실사 도로명주소 적용 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/site-visit/client-site-visit-wrapper.tsx | 1 + lib/site-visit/site-visit-detail-dialog.tsx | 67 +--------------------------- lib/site-visit/vendor-info-sheet.tsx | 58 ++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 68 deletions(-) (limited to 'lib/site-visit') diff --git a/lib/site-visit/client-site-visit-wrapper.tsx b/lib/site-visit/client-site-visit-wrapper.tsx index ab63fa0b..f2655475 100644 --- a/lib/site-visit/client-site-visit-wrapper.tsx +++ b/lib/site-visit/client-site-visit-wrapper.tsx @@ -496,6 +496,7 @@ export function ClientSiteVisitWrapper({ hasAttachments: siteVisitRequests.find(r => r.id === selectedSiteVisitRequestId)?.vendorInfo?.hasAttachments || false, otherInfo: siteVisitRequests.find(r => r.id === selectedSiteVisitRequestId)?.vendorInfo?.otherInfo || "", } : null} + vendorCountry={siteVisitRequests.find(r => r.id === selectedSiteVisitRequestId)?.vendorInfo?.country || ""} /> )} diff --git a/lib/site-visit/site-visit-detail-dialog.tsx b/lib/site-visit/site-visit-detail-dialog.tsx index 74e749c4..634d2aef 100644 --- a/lib/site-visit/site-visit-detail-dialog.tsx +++ b/lib/site-visit/site-visit-detail-dialog.tsx @@ -7,7 +7,6 @@ import { FileText, Download } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" import { Dialog, DialogContent, @@ -59,7 +58,6 @@ interface SiteVisitRequest { // 협력업체 정보 vendorInfo?: { id: number - country?: string siteVisitRequestId: number factoryName: string factoryLocation: string @@ -103,40 +101,6 @@ export function SiteVisitDetailDialog({ onOpenChange, selectedRequest, }: SiteVisitDetailDialogProps) { - const vendorCountry = (selectedRequest?.vendorInfo as any)?.country || "" - const isDomestic = vendorCountry === "KR" - - const [factoryLocation, setFactoryLocation] = React.useState( - selectedRequest?.vendorInfo?.factoryLocation || "" - ) - const [factoryAddress, setFactoryAddress] = React.useState( - selectedRequest?.vendorInfo?.factoryAddress || "" - ) - - React.useEffect(() => { - setFactoryLocation(selectedRequest?.vendorInfo?.factoryLocation || "") - setFactoryAddress(selectedRequest?.vendorInfo?.factoryAddress || "") - }, [selectedRequest?.vendorInfo?.factoryLocation, selectedRequest?.vendorInfo?.factoryAddress]) - - React.useEffect(() => { - const handleMessage = (event: MessageEvent) => { - if (!event.data || event.data.type !== "JUSO_SELECTED") return - const { roadAddrPart1, roadAddrPart2, addrDetail } = event.data.payload || {} - const combinedRoad = [roadAddrPart1, roadAddrPart2].filter(Boolean).join(" ").trim() - setFactoryLocation(combinedRoad || factoryLocation) - setFactoryAddress(addrDetail || factoryAddress) - } - window.addEventListener("message", handleMessage) - return () => window.removeEventListener("message", handleMessage) - }, [factoryLocation, factoryAddress]) - - const handleJusoSearch = () => { - window.open( - "/api/juso", - "jusoSearch", - "width=570,height=420,scrollbars=yes,resizable=yes" - ) - } return ( @@ -165,35 +129,8 @@ export function SiteVisitDetailDialog({

공장 기본 정보

공장명: {selectedRequest.vendorInfo.factoryName}
-
- 공장위치(도로명): - {isDomestic && ( - - )} -
- setFactoryLocation(e.target.value)} - readOnly={!isDomestic} - className={!isDomestic ? "" : "bg-muted text-muted-foreground"} - placeholder="도로명 주소" - /> -
공장주소(상세):
- setFactoryAddress(e.target.value)} - readOnly={!isDomestic} - className={!isDomestic ? "" : "bg-muted text-muted-foreground"} - placeholder="상세 주소" - /> +
공장위치: {selectedRequest.vendorInfo.factoryLocation}
+
공장주소: {selectedRequest.vendorInfo.factoryAddress}
diff --git a/lib/site-visit/vendor-info-sheet.tsx b/lib/site-visit/vendor-info-sheet.tsx index 2a20e212..ad2fa16b 100644 --- a/lib/site-visit/vendor-info-sheet.tsx +++ b/lib/site-visit/vendor-info-sheet.tsx @@ -61,6 +61,7 @@ interface VendorInfoSheetProps { onSubmit: (data: VendorInfoFormValues & { attachments?: File[] }) => Promise siteVisitRequestId: number initialData?: VendorInfoFormValues | null + vendorCountry?: string } export function VendorInfoSheet({ @@ -69,10 +70,12 @@ export function VendorInfoSheet({ onSubmit, siteVisitRequestId, initialData, + vendorCountry = "", }: VendorInfoSheetProps) { const [isPending, setIsPending] = React.useState(false) const [selectedFiles, setSelectedFiles] = React.useState([]) const fileInputRef = React.useRef(null) + const isDomestic = vendorCountry === "KR" const form = useForm({ resolver: zodResolver(vendorInfoSchema), @@ -114,6 +117,36 @@ export function VendorInfoSheet({ } }, [isOpen, form, initialData]) + // 도로명 주소 검색 결과 수신 (내자만 적용) + React.useEffect(() => { + if (!isOpen || !isDomestic) return + + const handleMessage = (event: MessageEvent) => { + if (!event.data || event.data.type !== "JUSO_SELECTED") return + const { roadAddrPart1, roadAddrPart2, addrDetail } = event.data.payload || {} + const combinedRoad = [roadAddrPart1, roadAddrPart2].filter(Boolean).join(" ").trim() + + if (combinedRoad) { + form.setValue("factoryLocation", combinedRoad, { shouldDirty: true }) + } + if (addrDetail) { + form.setValue("factoryAddress", addrDetail, { shouldDirty: true }) + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [isOpen, isDomestic, form]) + + const handleJusoSearch = () => { + if (!isDomestic) return + window.open( + "/api/juso", + "jusoSearch", + "width=570,height=420,scrollbars=yes,resizable=yes" + ) + } + // 파일 업로드 핸들러 const handleFileUpload = (event: React.ChangeEvent) => { const files = event.target.files @@ -202,7 +235,25 @@ export function VendorInfoSheet({ 실사 지역 * - +
+ + {isDomestic && ( + + )} +
@@ -220,8 +271,9 @@ export function VendorInfoSheet({