diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-15 18:58:07 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-15 18:58:07 +0900 |
| commit | 2b490956c9752c1b756780a3461bc1c37b6fe0a7 (patch) | |
| tree | b0b8a03c8de5dfce4b6c7373a9d608306e9147c0 /lib/avl/components/avl-history-modal.tsx | |
| parent | e7818a457371849e29519497ebf046f385f05ab6 (diff) | |
(김준회) AVL 관리 및 상세 - 기능 구현 1차
+ docker compose 내 오류 수정
Diffstat (limited to 'lib/avl/components/avl-history-modal.tsx')
| -rw-r--r-- | lib/avl/components/avl-history-modal.tsx | 297 |
1 files changed, 297 insertions, 0 deletions
diff --git a/lib/avl/components/avl-history-modal.tsx b/lib/avl/components/avl-history-modal.tsx new file mode 100644 index 00000000..4f0c354b --- /dev/null +++ b/lib/avl/components/avl-history-modal.tsx @@ -0,0 +1,297 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Calendar, Users, FileText, ChevronDown, ChevronRight } from "lucide-react" +import type { AvlListItem } from "@/lib/avl/types" + +interface AvlHistoryModalProps { + isOpen: boolean + onClose: () => void + avlItem: AvlListItem | null + historyData?: AvlHistoryRecord[] + onLoadHistory?: (avlItem: AvlListItem) => Promise<AvlHistoryRecord[]> +} + +export interface VendorSnapshot { + id: number + vendorName?: string + avlVendorName?: string + vendorCode?: string + disciplineName?: string + materialNameCustomerSide?: string + materialGroupCode?: string + materialGroupName?: string + tier?: string + hasAvl?: boolean + faTarget?: boolean + headquarterLocation?: string + ownerSuggestion?: boolean + shiSuggestion?: boolean + [key: string]: unknown // 다른 모든 속성들 +} + +export interface AvlHistoryRecord { + id: number + rev: number + createdAt: string + createdBy: string + vendorInfoSnapshot: VendorSnapshot[] // JSON 데이터 + changeDescription?: string +} + +// 스냅샷 테이블 컴포넌트 +interface SnapshotTableProps { + snapshot: VendorSnapshot[] + isOpen: boolean + onToggle: () => void +} + +function SnapshotTable({ snapshot, isOpen, onToggle }: SnapshotTableProps) { + if (!snapshot || snapshot.length === 0) { + return ( + <div className="text-sm text-muted-foreground"> + 스냅샷 데이터가 없습니다. + </div> + ) + } + + return ( + <Collapsible open={isOpen} onOpenChange={onToggle}> + <CollapsibleTrigger asChild> + <Button variant="outline" size="sm" className="w-full justify-between"> + <span>벤더 상세 정보 ({snapshot.length}개)</span> + {isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />} + </Button> + </CollapsibleTrigger> + <CollapsibleContent className="mt-3"> + <div className="border rounded-lg"> + <div className="overflow-auto max-h-[400px]"> + <Table> + <TableHeader className="sticky top-0 bg-background z-10"> + <TableRow> + <TableHead className="w-[60px]">No.</TableHead> + <TableHead className="w-[100px]">설계공종</TableHead> + <TableHead>고객사 AVL 자재명</TableHead> + <TableHead className="w-[120px]">자재그룹 코드</TableHead> + <TableHead className="w-[130px]">자재그룹 명</TableHead> + <TableHead>AVL 등재업체명</TableHead> + <TableHead className="w-[120px]">협력업체 코드</TableHead> + <TableHead className="w-[130px]">협력업체 명</TableHead> + <TableHead className="w-[80px]">선주제안</TableHead> + <TableHead className="w-[80px]">SHI 제안</TableHead> + <TableHead className="w-[100px]">본사 위치</TableHead> + <TableHead className="w-[80px]">등급</TableHead> + <TableHead className="w-[60px]">AVL</TableHead> + <TableHead className="w-[80px]">FA대상</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {snapshot.map((item, index) => ( + <TableRow key={item.id || index}> + <TableCell className="font-mono text-xs text-center">{index + 1}</TableCell> + <TableCell className="text-sm">{item.disciplineName || '-'}</TableCell> + <TableCell className="text-sm">{item.materialNameCustomerSide || '-'}</TableCell> + <TableCell className="font-mono text-xs">{item.materialGroupCode || '-'}</TableCell> + <TableCell className="text-sm">{item.materialGroupName || '-'}</TableCell> + <TableCell className="font-medium text-sm">{item.avlVendorName || '-'}</TableCell> + <TableCell className="font-mono text-xs">{item.vendorCode || '-'}</TableCell> + <TableCell className="font-medium text-sm">{item.vendorName || '-'}</TableCell> + <TableCell> + <Badge variant={item.ownerSuggestion ? "default" : "secondary"} className="text-xs"> + {item.ownerSuggestion ? "예" : "아니오"} + </Badge> + </TableCell> + <TableCell> + <Badge variant={item.shiSuggestion ? "default" : "secondary"} className="text-xs"> + {item.shiSuggestion ? "예" : "아니오"} + </Badge> + </TableCell> + <TableCell className="text-xs">{item.headquarterLocation || '-'}</TableCell> + <TableCell> + {item.tier ? ( + <Badge variant="outline" className="text-xs"> + {item.tier} + </Badge> + ) : '-'} + </TableCell> + <TableCell> + <Badge variant={item.hasAvl ? "default" : "secondary"} className="text-xs"> + {item.hasAvl ? "Y" : "N"} + </Badge> + </TableCell> + <TableCell> + <Badge variant={item.faTarget ? "default" : "secondary"} className="text-xs"> + {item.faTarget ? "Y" : "N"} + </Badge> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + </div> + </CollapsibleContent> + </Collapsible> + ) +} + +export function AvlHistoryModal({ + isOpen, + onClose, + avlItem, + historyData, + onLoadHistory +}: AvlHistoryModalProps) { + const [loading, setLoading] = React.useState(false) + const [history, setHistory] = React.useState<AvlHistoryRecord[]>([]) + const [openSnapshots, setOpenSnapshots] = React.useState<Record<number, boolean>>({}) + + // 히스토리 데이터 로드 + React.useEffect(() => { + if (isOpen && avlItem && onLoadHistory) { + setLoading(true) + onLoadHistory(avlItem) + .then(setHistory) + .catch(console.error) + .finally(() => setLoading(false)) + } else if (historyData) { + setHistory(historyData) + } + }, [isOpen, avlItem, onLoadHistory, historyData]) + + // 스냅샷 테이블 토글 함수 + const toggleSnapshot = (recordId: number) => { + setOpenSnapshots(prev => ({ + ...prev, + [recordId]: !prev[recordId] + })) + } + + if (!avlItem) return null + + return ( + <Dialog open={isOpen} onOpenChange={onClose}> + <DialogContent className="max-w-7xl h-[90vh] flex flex-col"> + <DialogHeader className="flex-shrink-0"> + <DialogTitle className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + AVL 리비전 히스토리 + </DialogTitle> + <div className="text-sm text-muted-foreground"> + {avlItem.isTemplate ? "표준 AVL" : "프로젝트 AVL"} - {avlItem.avlKind} + {avlItem.projectCode && ` (${avlItem.projectCode})`} + </div> + </DialogHeader> + + <div className="flex-1 overflow-auto min-h-0"> + <div className="pr-4"> + {loading ? ( + <div className="flex items-center justify-center h-[300px]"> + <div className="text-muted-foreground">히스토리를 불러오는 중...</div> + </div> + ) : history.length === 0 ? ( + <div className="flex items-center justify-center h-[300px]"> + <div className="text-muted-foreground">히스토리 데이터가 없습니다.</div> + </div> + ) : ( + <div className="space-y-4 py-4"> + {history.map((record, index) => ( + <div + key={record.id} + className={`p-4 border rounded-lg ${ + index === 0 ? "border-primary bg-primary/5" : "border-border" + }`} + > + {/* 리비전 헤더 */} + <div className="flex items-center justify-between mb-3"> + <div className="flex items-center gap-2"> + <Badge + variant={index === 0 ? "default" : "outline"} + className="font-mono" + > + Rev {record.rev} + </Badge> + {index === 0 && ( + <Badge variant="secondary" className="text-xs"> + 현재 + </Badge> + )} + </div> + <div className="flex items-center gap-4 text-sm text-muted-foreground"> + <div className="flex items-center gap-1"> + <Calendar className="h-4 w-4" /> + {new Date(record.createdAt).toLocaleDateString('ko-KR')} + </div> + </div> + </div> + + {/* 변경 설명 */} + {record.changeDescription && ( + <div className="mb-3 p-2 bg-muted/50 rounded text-sm"> + {record.changeDescription} + </div> + )} + + {/* Vendor Info 요약 */} + <div className="grid grid-cols-3 gap-4 text-sm"> + <div className="text-center"> + <div className="font-medium text-lg"> + {record.vendorInfoSnapshot?.length || 0} + </div> + <div className="text-muted-foreground">총 협력업체</div> + </div> + <div className="text-center"> + <div className="font-medium text-lg"> + {record.vendorInfoSnapshot?.filter(v => v.hasAvl).length || 0} + </div> + <div className="text-muted-foreground">AVL 등재</div> + </div> + <div className="text-center"> + <div className="font-medium text-lg"> + {record.vendorInfoSnapshot?.filter(v => v.faTarget).length || 0} + </div> + <div className="text-muted-foreground">FA 대상</div> + </div> + </div> + + {/* 스냅샷 테이블 */} + <div className="mt-3 pt-3 border-t"> + <SnapshotTable + snapshot={record.vendorInfoSnapshot || []} + isOpen={openSnapshots[record.id] || false} + onToggle={() => toggleSnapshot(record.id)} + /> + </div> + </div> + ))} + </div> + )} + </div> + </div> + + <div className="flex justify-end pt-4 border-t flex-shrink-0 mt-4"> + <Button variant="outline" onClick={onClose}> + 닫기 + </Button> + </div> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
