diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 06:57:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 06:57:36 +0000 |
| commit | 02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch) | |
| tree | e932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /components/vendor-info/pq-simple-dialog.tsx | |
| parent | d78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff) | |
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리
설계메뉴 - 벤더 데이터
gtc 메뉴 업데이트
정보시스템 - 메뉴리스트 및 정보 업데이트
파일 라우트 업데이트
엑셀임포트 개선
기본계약 개선
벤더 가입과정 변경 및 개선
벤더 기본정보 - pq
돌체 오류 수정 및 개선
벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'components/vendor-info/pq-simple-dialog.tsx')
| -rw-r--r-- | components/vendor-info/pq-simple-dialog.tsx | 417 |
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> + ) +} |
