diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-03 10:15:45 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-03 10:15:45 +0000 |
| commit | f2fafe555b65f9207c2c6e216b7d7b2ff83af866 (patch) | |
| tree | 4a230e4bde10a612150a299922bc04cb15b0930f /lib/site-visit/vendor-info-view-dialog.tsx | |
| parent | 1e857a0b1443ad2124caf3d180b7195651fe33e4 (diff) | |
(최겸) 구매 PQ/실사 수정
Diffstat (limited to 'lib/site-visit/vendor-info-view-dialog.tsx')
| -rw-r--r-- | lib/site-visit/vendor-info-view-dialog.tsx | 582 |
1 files changed, 368 insertions, 214 deletions
diff --git a/lib/site-visit/vendor-info-view-dialog.tsx b/lib/site-visit/vendor-info-view-dialog.tsx index 48aefeb0..fb2b0dfe 100644 --- a/lib/site-visit/vendor-info-view-dialog.tsx +++ b/lib/site-visit/vendor-info-view-dialog.tsx @@ -1,9 +1,7 @@ "use client"
import * as React from "react"
-import { format } from "date-fns"
-import { ko } from "date-fns/locale"
-import { Building2, User, Phone, Mail, FileText, Calendar } from "lucide-react"
+import { Building2, User, Phone, Mail, FileText, Calendar, ChevronRight } from "lucide-react"
import { formatDate } from "../utils"
import {
@@ -50,6 +48,19 @@ interface Attachment { updatedAt: Date
}
+interface SiteVisitRequest {
+ id: number
+ status: string
+ inspectionDuration: number
+ requestedStartDate: Date
+ requestedEndDate: Date
+ additionalRequests: string | null
+ createdAt: Date
+ updatedAt: Date
+ vendorInfo: VendorInfo | null
+ attachments: Attachment[]
+}
+
interface VendorInfoViewDialogProps {
isOpen: boolean
onClose: () => void
@@ -58,6 +69,223 @@ interface VendorInfoViewDialogProps { isReinspection?: boolean // 재실사 모드 플래그
}
+// 상세 정보를 표시하는 내부 컴포넌트
+function VendorDetailView({
+ vendorInfo,
+ attachments,
+ siteVisitRequest
+}: {
+ vendorInfo: VendorInfo | null
+ attachments: Attachment[]
+ siteVisitRequest?: SiteVisitRequest
+}) {
+ if (!vendorInfo) {
+ return (
+ <div className="text-center py-8">
+ <div className="text-muted-foreground">
+ <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
+ <p>협력업체가 아직 정보를 입력하지 않았습니다.</p>
+ </div>
+ </div>
+ )
+ }
+
+ return (
+ <div className="space-y-6">
+ {/* 협력업체 공장 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Building2 className="h-5 w-5" />
+ 협력업체 공장 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+ <div className="space-y-4">
+ <div>
+ <h4 className="font-semibold mb-2">공장 기본 정보</h4>
+ <div className="space-y-2 text-sm">
+ <div><span className="font-medium">공장명:</span> {vendorInfo.factoryName}</div>
+ <div><span className="font-medium">공장위치:</span> {vendorInfo.factoryLocation}</div>
+ <div><span className="font-medium">공장주소:</span> {vendorInfo.factoryAddress}</div>
+ </div>
+ </div>
+
+ <div>
+ <h4 className="font-semibold mb-2">공장 담당자 정보</h4>
+ <div className="space-y-2 text-sm">
+ <div className="flex items-center gap-2">
+ <User className="h-4 w-4" />
+ <span>{vendorInfo.factoryPicName}</span>
+ </div>
+ <div className="flex items-center gap-2">
+ <Phone className="h-4 w-4" />
+ <span>{vendorInfo.factoryPicPhone}</span>
+ </div>
+ <div className="flex items-center gap-2">
+ <Mail className="h-4 w-4" />
+ <span>{vendorInfo.factoryPicEmail}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div className="space-y-4">
+ {vendorInfo.factoryDirections && (
+ <div>
+ <h4 className="font-semibold mb-2">공장 가는 법</h4>
+ <div className="bg-muted p-3 rounded-md">
+ <p className="text-sm whitespace-pre-wrap">{vendorInfo.factoryDirections}</p>
+ </div>
+ </div>
+ )}
+
+ {vendorInfo.accessProcedure && (
+ <div>
+ <h4 className="font-semibold mb-2">공장 출입절차</h4>
+ <div className="bg-muted p-3 rounded-md">
+ <p className="text-sm whitespace-pre-wrap">{vendorInfo.accessProcedure}</p>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 첨부파일 */}
+ {attachments.length > 0 && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ 협력업체 첨부파일 ({attachments.length}개)
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-2">
+ {attachments.map((attachment) => (
+ <div key={attachment.id} className="flex items-center justify-between p-2 border rounded-md">
+ <div className="flex items-center space-x-2 flex-1 min-w-0">
+ <FileText className="h-4 w-4 text-muted-foreground" />
+ <span className="text-sm truncate">{attachment.originalFileName}</span>
+ <span className="text-xs text-muted-foreground">
+ ({Math.round((attachment.fileSize || 0) / 1024)}KB)
+ </span>
+ </div>
+ <Button
+ type="button"
+ variant="outline"
+ size="sm"
+ onClick={async () => {
+ try {
+ const { downloadFile } = await import('@/lib/file-download')
+ await downloadFile(attachment.filePath, attachment.originalFileName || '', {
+ showToast: true,
+ onError: (error) => {
+ console.error('다운로드 오류:', error)
+ toast.error(error)
+ },
+ onSuccess: (fileName, fileSize) => {
+ console.log(`다운로드 성공: ${fileName} (${fileSize} bytes)`)
+ }
+ })
+ } catch (error) {
+ console.error('다운로드 오류:', error)
+ toast.error('파일 다운로드 중 오류가 발생했습니다.')
+ }
+ }}
+ >
+ 다운로드
+ </Button>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 실사 정보 */}
+ {siteVisitRequest && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Calendar className="h-5 w-5" />
+ 실사 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-2 gap-4 text-sm">
+ <div>
+ <span className="font-medium">실사 기간:</span> {siteVisitRequest.inspectionDuration}일
+ </div>
+ <div>
+ <span className="font-medium">요청 시작일:</span>
+ {siteVisitRequest.requestedStartDate ? formatDate(siteVisitRequest.requestedStartDate, "kr") : "미정"}
+ </div>
+ <div>
+ <span className="font-medium">요청 종료일:</span>
+ {siteVisitRequest.requestedEndDate ? formatDate(siteVisitRequest.requestedEndDate, "kr") : "미정"}
+ </div>
+ <div>
+ <span className="font-medium">상태:</span>
+ <Badge variant={siteVisitRequest.status === "VENDOR_SUBMITTED" ? "default" : "secondary"} className="ml-2">
+ {siteVisitRequest.status === "VENDOR_SUBMITTED" ? "제출완료" : siteVisitRequest.status === "SENT" ? "발송완료" : "요청됨"}
+ </Badge>
+ </div>
+ {siteVisitRequest.additionalRequests && (
+ <div className="col-span-2">
+ <span className="font-medium">추가 요청사항:</span>
+ <div className="bg-muted p-2 rounded mt-1 text-sm whitespace-pre-wrap">{siteVisitRequest.additionalRequests}</div>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 기타 정보 */}
+ {vendorInfo.otherInfo && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ 기타 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <p className="text-sm whitespace-pre-wrap">{vendorInfo.otherInfo}</p>
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 제출 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Calendar className="h-5 w-5" />
+ 제출 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <div className="space-y-2 text-sm">
+ <div>
+ <span className="font-medium">제출일:</span>{" "}
+ {vendorInfo.submittedAt ? formatDate(vendorInfo.submittedAt, "kr") : "-"}
+ </div>
+ <div><span className="font-medium">첨부파일:</span> {vendorInfo.hasAttachments ? "있음" : "없음"}</div>
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ )
+}
+
export function VendorInfoViewDialog({
isOpen,
onClose,
@@ -66,13 +294,22 @@ export function VendorInfoViewDialog({ }: VendorInfoViewDialogProps) {
const [data, setData] = React.useState<VendorInfo | null>(null)
const [attachments, setAttachments] = React.useState<Attachment[]>([])
- const [allConfirmations, setAllConfirmations] = React.useState<any[]>([]) // 여러 확정정보
+ const [siteVisitRequests, setSiteVisitRequests] = React.useState<SiteVisitRequest[]>([])
const [isLoading, setIsLoading] = React.useState(false)
+ const [selectedRequest, setSelectedRequest] = React.useState<SiteVisitRequest | null>(null)
+ const [detailDialogOpen, setDetailDialogOpen] = React.useState(false)
// 데이터 로드
React.useEffect(() => {
if (isOpen && (siteVisitRequestId || investigationId)) {
loadData()
+ } else {
+ // Dialog가 닫힐 때 상태 초기화
+ setData(null)
+ setAttachments([])
+ setSiteVisitRequests([])
+ setSelectedRequest(null)
+ setDetailDialogOpen(false)
}
}, [isOpen, siteVisitRequestId, investigationId])
@@ -81,7 +318,7 @@ export function VendorInfoViewDialog({ setIsLoading(true)
try {
- // 단일 확정정보 조회 (기존)
+ // 단일 확정정보 조회 (기존 방식 - 하위 호환성 유지)
if (siteVisitRequestId) {
const { getVendorSiteVisitInfoAction } = await import("./service")
const result = await getVendorSiteVisitInfoAction(siteVisitRequestId)
@@ -90,16 +327,20 @@ export function VendorInfoViewDialog({ setData(result.data.vendorInfo)
setAttachments(result.data.attachments || [])
} else {
- toast.error("협력업체 정보를 불러올 수 없습니다.")
+ setData(null)
+ setAttachments([])
}
}
- // 여러 확정정보 조회 (신규 - 실사 ID로 모든 siteVisitRequests 조회)
+ // 여러 확정정보 조회 (investigationId 기준)
if (investigationId) {
const { getAllSiteVisitRequestsForInvestigationAction } = await import("./service")
const result = await getAllSiteVisitRequestsForInvestigationAction(investigationId)
if (result.success) {
- setAllConfirmations(result.confirmations || [])
+ setSiteVisitRequests(result.requests || [])
+ } else {
+ setSiteVisitRequests([])
+ toast.error(result.error || "방문실사 정보를 불러올 수 없습니다.")
}
}
} catch (error) {
@@ -110,7 +351,124 @@ export function VendorInfoViewDialog({ }
}
+ const handleListItemClick = (request: SiteVisitRequest) => {
+ setSelectedRequest(request)
+ setDetailDialogOpen(true)
+ }
+
+ const handleCloseDetail = () => {
+ setDetailDialogOpen(false)
+ setSelectedRequest(null)
+ }
+
+ // investigationId가 있는 경우: 리스트 형태 표시
+ if (investigationId) {
+ return (
+ <>
+ <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>협력업체 방문실사 정보</DialogTitle>
+ <DialogDescription>
+ 협력업체가 입력한 방문실사 관련 정보를 확인할 수 있습니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ {isLoading ? (
+ <div className="flex items-center justify-center py-8">
+ <div className="text-center">
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
+ <p className="text-muted-foreground">협력업체 정보를 불러오는 중...</p>
+ </div>
+ </div>
+ ) : siteVisitRequests.length > 0 ? (
+ <div className="space-y-3">
+ {siteVisitRequests.map((request, index) => (
+ <Card
+ key={request.id}
+ className="cursor-pointer hover:bg-muted/50 transition-colors"
+ onClick={() => handleListItemClick(request)}
+ >
+ <CardContent className="p-4">
+ <div className="flex items-center justify-between">
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center gap-3 mb-2">
+ <h4 className="font-semibold text-base">
+ 방문실사 정보 #{index + 1}
+ </h4>
+ <Badge
+ variant={
+ request.vendorInfo
+ ? (request.status === "VENDOR_SUBMITTED" ? "default" : "secondary")
+ : "outline"
+ }
+ >
+ {request.vendorInfo
+ ? (request.status === "VENDOR_SUBMITTED" ? "제출완료" : "발송완료")
+ : "미제출"
+ }
+ </Badge>
+ </div>
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-muted-foreground">
+ <div>
+ <span className="font-medium">공장명:</span>{" "}
+ {request.vendorInfo?.factoryName || "미입력"}
+ </div>
+ <div>
+ <span className="font-medium">제출일:</span>{" "}
+ {request.vendorInfo?.submittedAt
+ ? formatDate(request.vendorInfo.submittedAt, "kr")
+ : "-"
+ }
+ </div>
+ <div>
+ <span className="font-medium">실사기간:</span> {request.inspectionDuration}일
+ </div>
+ <div>
+ <span className="font-medium">생성일:</span> {formatDate(request.createdAt, "kr")}
+ </div>
+ </div>
+ </div>
+ <ChevronRight className="h-5 w-5 text-muted-foreground ml-4 flex-shrink-0" />
+ </div>
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ ) : (
+ <div className="text-center py-8">
+ <div className="text-muted-foreground">
+ <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
+ <p>협력업체 방문실사 정보가 없습니다.</p>
+ </div>
+ </div>
+ )}
+ </DialogContent>
+ </Dialog>
+
+ {/* 상세 정보 Dialog */}
+ <Dialog open={detailDialogOpen} onOpenChange={(open) => !open && handleCloseDetail()}>
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>협력업체 방문실사 상세 정보</DialogTitle>
+ <DialogDescription>
+ 협력업체가 입력한 방문실사 관련 상세 정보를 확인할 수 있습니다.
+ </DialogDescription>
+ </DialogHeader>
+ {selectedRequest && (
+ <VendorDetailView
+ vendorInfo={selectedRequest.vendorInfo}
+ attachments={selectedRequest.attachments || []}
+ siteVisitRequest={selectedRequest}
+ />
+ )}
+ </DialogContent>
+ </Dialog>
+ </>
+ )
+ }
+ // siteVisitRequestId가 있는 경우: 기존 방식 (단일 상세 정보 표시)
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
@@ -128,214 +486,10 @@ export function VendorInfoViewDialog({ <p className="text-muted-foreground">협력업체 정보를 불러오는 중...</p>
</div>
</div>
- ) : (data || allConfirmations.length > 0) ? (
- <div className="space-y-6">
- {/* 협력업체 정보 - 단일 확정정보 조회 시에만 표시 */}
- {data && (
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Building2 className="h-5 w-5" />
- 협력업체 공장 정보
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-4">
- <div>
- <h4 className="font-semibold mb-2">공장 기본 정보</h4>
- <div className="space-y-2 text-sm">
- <div><span className="font-medium">공장명:</span> {data.factoryName}</div>
- <div><span className="font-medium">공장위치:</span> {data.factoryLocation}</div>
- <div><span className="font-medium">공장주소:</span> {data.factoryAddress}</div>
- </div>
- </div>
-
- <div>
- <h4 className="font-semibold mb-2">공장 담당자 정보</h4>
- <div className="space-y-2 text-sm">
- <div className="flex items-center gap-2">
- <User className="h-4 w-4" />
- <span>{data.factoryPicName}</span>
- </div>
- <div className="flex items-center gap-2">
- <Phone className="h-4 w-4" />
- <span>{data.factoryPicPhone}</span>
- </div>
- <div className="flex items-center gap-2">
- <Mail className="h-4 w-4" />
- <span>{data.factoryPicEmail}</span>
- </div>
- </div>
- </div>
- </div>
-
- <div className="space-y-4">
- {data.factoryDirections && (
- <div>
- <h4 className="font-semibold mb-2">공장 가는 법</h4>
- <div className="bg-muted p-3 rounded-md">
- <p className="text-sm whitespace-pre-wrap">{data.factoryDirections}</p>
- </div>
- </div>
- )}
-
- {data.accessProcedure && (
- <div>
- <h4 className="font-semibold mb-2">공장 출입절차</h4>
- <div className="bg-muted p-3 rounded-md">
- <p className="text-sm whitespace-pre-wrap">{data.accessProcedure}</p>
- </div>
- </div>
- )}
- </div>
- </div>
- </CardContent>
- </Card>
- )}
-
- {/* 첨부파일 */}
- {attachments.length > 0 && (
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <FileText className="h-5 w-5" />
- 협력업체 첨부파일 ({attachments.length}개)
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="space-y-2">
- {attachments.map((attachment) => (
- <div key={attachment.id} className="flex items-center justify-between p-2 border rounded-md">
- <div className="flex items-center space-x-2 flex-1 min-w-0">
- <FileText className="h-4 w-4 text-muted-foreground" />
- <span className="text-sm truncate">{attachment.originalFileName}</span>
- <span className="text-xs text-muted-foreground">
- ({Math.round((attachment.fileSize || 0) / 1024)}KB)
- </span>
- </div>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={async () => {
- try {
- const { downloadFile } = await import('@/lib/file-download')
- await downloadFile(attachment.filePath, attachment.originalFileName || '', {
- showToast: true,
- onError: (error) => {
- console.error('다운로드 오류:', error)
- toast.error(error)
- },
- onSuccess: (fileName, fileSize) => {
- console.log(`다운로드 성공: ${fileName} (${fileSize} bytes)`)
- }
- })
- } catch (error) {
- console.error('다운로드 오류:', error)
- toast.error('파일 다운로드 중 오류가 발생했습니다.')
- }
- }}
- >
- 다운로드
- </Button>
- </div>
- ))}
- </div>
- </CardContent>
- </Card>
- )}
-
- {/* 실사 실시 확정정보 (복수 지원) */}
- {allConfirmations.length > 0 && (
- <div className="space-y-4">
- <h3 className="text-lg font-semibold">실사 실시 확정정보</h3>
- {allConfirmations.map((confirmation, index) => (
- <Card key={confirmation.id}>
- <CardHeader>
- <CardTitle className="flex items-center justify-between">
- <span className="flex items-center gap-2">
- <Calendar className="h-5 w-5" />
- 실사 확정정보 #{index + 1}
- </span>
- <Badge variant={confirmation.status === "COMPLETED" ? "default" : "secondary"}>
- {confirmation.status === "COMPLETED" ? "완료" : "진행중"}
- </Badge>
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-2 gap-4 text-sm">
- <div>
- <span className="font-medium">실사 기간:</span> {confirmation.inspectionDuration}일
- </div>
- <div>
- <span className="font-medium">요청 시작일:</span>
- {confirmation.requestedStartDate ? formatDate(confirmation.requestedStartDate, "kr") : "미정"}
- </div>
- <div>
- <span className="font-medium">요청 종료일:</span>
- {confirmation.requestedEndDate ? formatDate(confirmation.requestedEndDate, "kr") : "미정"}
- </div>
- <div>
- <span className="font-medium">생성일:</span> {formatDate(confirmation.createdAt, "kr")}
- </div>
- {confirmation.additionalRequests && (
- <div className="col-span-2">
- <span className="font-medium">추가 요청사항:</span>
- <div className="bg-muted p-2 rounded mt-1">{confirmation.additionalRequests}</div>
- </div>
- )}
- </div>
- </CardContent>
- </Card>
- ))}
- </div>
- )}
-
- {/* 기타 정보 */}
- {data?.otherInfo && (
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <FileText className="h-5 w-5" />
- 기타 정보
- </CardTitle>
- </CardHeader>
- <CardContent>
- <p className="text-sm whitespace-pre-wrap">{data?.otherInfo}</p>
- </CardContent>
- </Card>
- )}
-
- {/* 제출 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Calendar className="h-5 w-5" />
- 제출 정보
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-2 gap-4">
- <div>
- <div className="space-y-2 text-sm">
- <div><span className="font-medium">제출일:</span> {formatDate(data?.submittedAt, "kr")}</div>
- <div><span className="font-medium">첨부파일:</span> {data?.hasAttachments ? "있음" : "없음"}</div>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
) : (
- <div className="text-center py-8">
- <div className="text-muted-foreground">
- <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
- <p>협력업체가 아직 정보를 입력하지 않았습니다.</p>
- </div>
- </div>
+ <VendorDetailView vendorInfo={data} attachments={attachments} />
)}
</DialogContent>
</Dialog>
)
-}
\ No newline at end of file +}
\ No newline at end of file |
