summaryrefslogtreecommitdiff
path: root/components/vendor-info/pq-simple-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/vendor-info/pq-simple-dialog.tsx')
-rw-r--r--components/vendor-info/pq-simple-dialog.tsx417
1 files changed, 417 insertions, 0 deletions
diff --git a/components/vendor-info/pq-simple-dialog.tsx b/components/vendor-info/pq-simple-dialog.tsx
new file mode 100644
index 00000000..bb26685d
--- /dev/null
+++ b/components/vendor-info/pq-simple-dialog.tsx
@@ -0,0 +1,417 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Download, FileText, ChevronDown, ChevronUp, Search } from "lucide-react"
+import { Input } from "@/components/ui/input"
+import { toast } from "sonner"
+import { getPQProjectsByVendorId, ProjectPQ, getPQDataByVendorId, PQGroupData } from "@/lib/pq/service"
+import { downloadFile } from "@/lib/file-download"
+
+interface PQSimpleDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ vendorId: string
+}
+
+interface PQItemData {
+ groupName: string
+ code: string
+ checkPoint: string
+ description: string
+ answer: string | null
+ inputFormat: string
+ fileName?: string | null
+ filePath?: string | null
+}
+
+export function PQSimpleDialog({
+ open,
+ onOpenChange,
+ vendorId,
+}: PQSimpleDialogProps) {
+ const [projects, setProjects] = useState<ProjectPQ[]>([])
+ const [selectedProject, setSelectedProject] = useState<ProjectPQ | null>(null)
+ const [pqData, setPqData] = useState<PQGroupData[]>([])
+ const [loading, setLoading] = useState(false)
+ const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set())
+ const [searchTerm, setSearchTerm] = useState("")
+
+ // vendorId를 숫자로 변환
+ const numericVendorId = parseInt(vendorId)
+
+ useEffect(() => {
+ if (open && !isNaN(numericVendorId)) {
+ loadProjects()
+ }
+ }, [open, numericVendorId])
+
+ const loadProjects = async () => {
+ try {
+ setLoading(true)
+ const projectList = await getPQProjectsByVendorId(numericVendorId)
+ setProjects(projectList)
+
+ if (projectList.length > 0) {
+ setSelectedProject(projectList[0])
+ await loadPQData(projectList[0].projectId)
+ }
+ } catch (error) {
+ console.error("프로젝트 목록 로드 실패:", error)
+ toast.error("PQ 프로젝트 목록을 불러오는데 실패했습니다.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const loadPQData = async (projectId: number | null) => {
+ if (projectId === null) return
+
+ try {
+ setLoading(true)
+ const data = await getPQDataByVendorId(numericVendorId, projectId)
+ setPqData(data)
+ } catch (error) {
+ console.error("PQ 데이터 로드 실패:", error)
+ toast.error("PQ 데이터를 불러오는데 실패했습니다.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleProjectChange = async (project: ProjectPQ) => {
+ setSelectedProject(project)
+ await loadPQData(project.projectId)
+ }
+
+ const handleFileDownload = async (filePath: string, fileName: string) => {
+ try {
+ const result = await downloadFile(filePath, fileName)
+ if (result.success) {
+ toast.success(`${fileName} 파일이 다운로드되었습니다.`)
+ } else {
+ toast.error(result.error || "파일 다운로드에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("파일 다운로드 오류:", error)
+ toast.error("파일 다운로드에 실패했습니다.")
+ }
+ }
+
+ // 코드 순서로 정렬하는 함수 (1-1-1, 1-1-2, 1-2-1 순서)
+ const sortByCode = (items: any[]) => {
+ return [...items].sort((a, b) => {
+ const parseCode = (code: string) => {
+ return code.split('-').map(part => parseInt(part, 10))
+ }
+
+ const aCode = parseCode(a.code)
+ const bCode = parseCode(b.code)
+
+ for (let i = 0; i < Math.max(aCode.length, bCode.length); i++) {
+ const aPart = aCode[i] || 0
+ const bPart = bCode[i] || 0
+ if (aPart !== bPart) {
+ return aPart - bPart
+ }
+ }
+ return 0
+ })
+ }
+
+ // 검색 필터링 함수
+ const filterItems = (items: any[], searchTerm: string) => {
+ if (!searchTerm.trim()) return items
+
+ const search = searchTerm.toLowerCase()
+ return items.filter(item =>
+ item.checkPoint?.toLowerCase().includes(search) ||
+ item.description?.toLowerCase().includes(search) ||
+ item.code?.toLowerCase().includes(search)
+ )
+ }
+
+ // 그룹별로 정렬 및 필터링된 데이터 계산
+ const processedPQData = pqData.map(group => ({
+ ...group,
+ items: filterItems(sortByCode(group.items), searchTerm)
+ })).filter(group => group.items.length > 0) // 검색 결과가 없는 그룹은 제외
+
+ const toggleGroup = (groupName: string) => {
+ setExpandedGroups(prev => {
+ const newSet = new Set(prev)
+ if (newSet.has(groupName)) {
+ newSet.delete(groupName)
+ } else {
+ newSet.add(groupName)
+ }
+ return newSet
+ })
+ }
+
+ const renderPQContent = (groupData: PQGroupData) => {
+ const isExpanded = expandedGroups.has(groupData.groupName)
+ const itemCount = groupData.items.length
+
+ return (
+ <Card key={groupData.groupName} className="mb-4">
+ <CardHeader
+ className="cursor-pointer hover:bg-muted/50 transition-colors"
+ onClick={() => toggleGroup(groupData.groupName)}
+ >
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <CardTitle className="text-base font-medium">
+ {groupData.groupName}
+ </CardTitle>
+ <Badge variant="secondary" className="text-xs">
+ {itemCount}개 항목
+ </Badge>
+ </div>
+ {isExpanded ? (
+ <ChevronUp className="w-4 h-4 text-muted-foreground" />
+ ) : (
+ <ChevronDown className="w-4 h-4 text-muted-foreground" />
+ )}
+ </div>
+ </CardHeader>
+
+ {isExpanded && (
+ <CardContent className="pt-0">
+ <div className="space-y-3">
+ {groupData.items.map((item, index) => (
+ <div
+ key={`${groupData.groupName}-${index}`}
+ className="border rounded-lg p-4 hover:bg-muted/30 transition-colors"
+ >
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <div className="flex items-center gap-2">
+ <Badge variant="outline" className="font-mono text-xs">
+ {item.code}
+ </Badge>
+ <Badge variant="secondary" className="text-xs">
+ {item.inputFormat}
+ </Badge>
+ </div>
+ <h4 className="font-medium text-sm">
+ {item.checkPoint}
+ </h4>
+ {item.description && (
+ <p className="text-sm text-muted-foreground">
+ {item.description}
+ </p>
+ )}
+ </div>
+
+ <div className="space-y-2">
+ <div>
+ <label className="text-xs font-medium text-muted-foreground">답변</label>
+ <p className="text-sm mt-1 p-2 bg-muted/50 rounded border min-h-[2.5rem]">
+ {item.answer || "답변 없음"}
+ </p>
+ </div>
+
+ {item.attachments && item.attachments.length > 0 && (
+ <div>
+ <label className="text-xs font-medium text-muted-foreground">첨부파일</label>
+ <div className="mt-1 space-y-1">
+ {item.attachments.map((attachment, idx) => (
+ <Button
+ key={idx}
+ variant="outline"
+ size="sm"
+ onClick={() => handleFileDownload(attachment.filePath, attachment.fileName)}
+ className="h-8 w-full justify-start text-xs"
+ >
+ <FileText className="w-3 h-3 mr-2" />
+ <span className="truncate flex-1 text-left">
+ {attachment.fileName}
+ </span>
+ <Download className="w-3 h-3 ml-2" />
+ </Button>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ )}
+ </Card>
+ )
+ }
+
+ if (projects.length === 0 && !loading) {
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>PQ 조회</DialogTitle>
+ </DialogHeader>
+ <div className="text-center py-8">
+ <p className="text-muted-foreground">제출된 PQ가 없습니다.</p>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>PQ 조회</DialogTitle>
+ </DialogHeader>
+
+ {loading ? (
+ <div className="text-center py-8">
+ <p className="text-muted-foreground">로딩 중...</p>
+ </div>
+ ) : selectedProject ? (
+ <div className="space-y-4">
+ {/* 프로젝트 선택 */}
+ {projects.length > 1 && (
+ <div className="space-y-2">
+ <label className="text-sm font-medium">프로젝트 선택</label>
+ <Select
+ value={selectedProject.id.toString()}
+ onValueChange={(value) => {
+ const project = projects.find(p => p.id.toString() === value)
+ if (project) handleProjectChange(project)
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="프로젝트를 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ {projects.map((project) => (
+ <SelectItem key={project.id} value={project.id.toString()}>
+ <div className="flex flex-col items-start">
+ <span className="font-medium">{project.projectCode}</span>
+ <span className="text-xs text-muted-foreground">
+ {project.projectName}
+ </span>
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+
+ {/* 프로젝트 정보 카드 */}
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <CardTitle className="text-lg">{selectedProject.projectName}</CardTitle>
+ <Badge
+ variant={selectedProject.status === 'APPROVED' ? 'default' : 'secondary'}
+ className="text-xs"
+ >
+ {selectedProject.status}
+ </Badge>
+ </div>
+ </div>
+ <div className="text-sm text-muted-foreground">
+ <span className="font-medium">프로젝트 코드:</span> {selectedProject.projectCode} •
+ <span className="font-medium">제출일:</span> {selectedProject.submittedAt ? new Date(selectedProject.submittedAt).toLocaleDateString('ko-KR') : '-'}
+ </div>
+ </CardHeader>
+ </Card>
+
+ {/* 검색 및 PQ 그룹 데이터 */}
+ <div className="space-y-4">
+ <div className="flex items-center justify-between">
+ <h3 className="text-lg font-semibold">PQ 항목</h3>
+ <div className="flex gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setExpandedGroups(new Set(processedPQData.map(g => g.groupName)))}
+ >
+ 모두 펼치기
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setExpandedGroups(new Set())}
+ >
+ 모두 접기
+ </Button>
+ </div>
+ </div>
+
+ {/* 검색 박스 */}
+ <div className="relative">
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
+ <Input
+ placeholder="항목 검색 (체크포인트, 세부내용, 코드)"
+ value={searchTerm}
+ onChange={(e) => setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+ {searchTerm && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => setSearchTerm("")}
+ className="absolute right-2 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0"
+ >
+ ×
+ </Button>
+ )}
+ </div>
+
+ {/* 검색 결과 카운트 */}
+ {searchTerm && (
+ <div className="text-sm text-muted-foreground">
+ 검색 결과: {processedPQData.reduce((total, group) => total + group.items.length, 0)}개 항목
+ ({processedPQData.length}개 그룹)
+ </div>
+ )}
+
+ {/* PQ 그룹 목록 */}
+ {processedPQData.length > 0 ? (
+ processedPQData.map((groupData) => renderPQContent(groupData))
+ ) : (
+ <div className="text-center py-8">
+ <p className="text-muted-foreground">
+ {searchTerm ? "검색 결과가 없습니다." : "PQ 데이터가 없습니다."}
+ </p>
+ </div>
+ )}
+ </div>
+ </div>
+ ) : null}
+ </DialogContent>
+ </Dialog>
+ )
+}