From 2b490956c9752c1b756780a3461bc1c37b6fe0a7 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Mon, 15 Sep 2025 18:58:07 +0900
Subject: (김준회) AVL 관리 및 상세 - 기능 구현 1차 + docker compose 내 오류
 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/avl/components/avl-history-modal.tsx | 297 +++++++++++++++++++++++++++++++
 1 file changed, 297 insertions(+)
 create mode 100644 lib/avl/components/avl-history-modal.tsx

(limited to 'lib/avl/components/avl-history-modal.tsx')

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
-- 
cgit v1.2.3

