From 748bb1720fd81e97a84c3e92f89d606e976b52e3 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 28 May 2025 00:18:16 +0000
Subject: (대표님) 컴포넌트 파트 커밋
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/form-data/sedp-compare-dialog.tsx | 317 ++++++++++++++++-----------
 1 file changed, 195 insertions(+), 122 deletions(-)

(limited to 'components/form-data/sedp-compare-dialog.tsx')

diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx
index 37fe18ed..3107193a 100644
--- a/components/form-data/sedp-compare-dialog.tsx
+++ b/components/form-data/sedp-compare-dialog.tsx
@@ -3,12 +3,14 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
 import { Button } from "@/components/ui/button";
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { Loader, RefreshCw, AlertCircle, CheckCircle, Info, EyeOff } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import { Loader, RefreshCw, AlertCircle, CheckCircle, Info, EyeOff, ChevronDown, ChevronRight, Search } from "lucide-react";
 import { Badge } from "@/components/ui/badge";
 import { toast } from "sonner";
 import { DataTableColumnJSON } from "./form-data-table-columns";
 import { ExcelDownload } from "./sedp-excel-download";
 import { Switch } from "../ui/switch";
+import { Card, CardContent } from "@/components/ui/card";
 
 interface SEDPCompareDialogProps {
   isOpen: boolean;
@@ -37,7 +39,7 @@ interface ComparisonResult {
 // Component for formatting display value with UOM
 const DisplayValue = ({ value, uom, isSedp = false }: { value: any; uom?: string; isSedp?: boolean }) => {
   if (value === "" || value === null || value === undefined) {
-    return <span>(empty)</span>;
+    return <span className="text-muted-foreground italic">(empty)</span>;
   }
 
   // SEDP 값은 UOM을 표시하지 않음 (이미 포함되어 있다고 가정)
@@ -54,7 +56,7 @@ const DisplayValue = ({ value, uom, isSedp = false }: { value: any; uom?: string
   );
 };
 
-// 범례 컴포넌트 추가
+// 범례 컴포넌트
 const ColorLegend = () => {
   return (
     <div className="flex items-center gap-4 text-sm p-2 bg-muted/20 rounded">
@@ -76,6 +78,64 @@ const ColorLegend = () => {
   );
 };
 
+// 확장 가능한 차이점 표시 컴포넌트
+const DifferencesCard = ({ 
+  attributes, 
+  columnLabelMap, 
+  showOnlyDifferences 
+}: { 
+  attributes: ComparisonResult['attributes']; 
+  columnLabelMap: Record<string, string>;
+  showOnlyDifferences: boolean;
+}) => {
+  const attributesToShow = showOnlyDifferences 
+    ? attributes.filter(attr => !attr.isMatching)
+    : attributes;
+
+  if (attributesToShow.length === 0) {
+    return (
+      <div className="text-center text-muted-foreground py-4">
+        모든 속성이 일치합니다
+      </div>
+    );
+  }
+
+  return (
+    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 p-4">
+      {attributesToShow.map((attr) => (
+        <Card key={attr.key} className={`${!attr.isMatching ? 'border-red-200' : 'border-green-200'}`}>
+          <CardContent className="p-3">
+            <div className="font-medium text-sm mb-2 truncate" title={attr.label}>
+              {attr.label}
+              {attr.uom && <span className="text-xs text-muted-foreground ml-1">({attr.uom})</span>}
+            </div>
+            {attr.isMatching ? (
+              <div className="text-sm">
+                <DisplayValue value={attr.localValue} uom={attr.uom} />
+              </div>
+            ) : (
+              <div className="space-y-2">
+                <div className="flex items-center gap-2 text-sm">
+                  <span className="text-xs text-muted-foreground min-w-[35px]">로컬:</span>
+                  <span className="line-through text-red-500 flex-1">
+                    <DisplayValue value={attr.localValue} uom={attr.uom} isSedp={false} />
+                  </span>
+                </div>
+                <div className="flex items-center gap-2 text-sm">
+                  <span className="text-xs text-muted-foreground min-w-[35px]">SEDP:</span>
+                  <span className="text-green-500 flex-1">
+                    <DisplayValue value={attr.sedpValue} uom={attr.uom} isSedp={true} />
+                  </span>
+                </div>
+              </div>
+            )}
+          </CardContent>
+        </Card>
+      ))}
+    </div>
+  );
+};
+
 export function SEDPCompareDialog({
   isOpen,
   onClose,
@@ -92,11 +152,10 @@ export function SEDPCompareDialog({
   const [missingTags, setMissingTags] = React.useState<{
     localOnly: { tagNo: string; tagDesc: string }[];
     sedpOnly: { tagNo: string; tagDesc: string }[];
-  }>(
-    { localOnly: [], sedpOnly: [] }
-  );
-  // 추가: 차이점만 표시하는 옵션
+  }>({ localOnly: [], sedpOnly: [] });
   const [showOnlyDifferences, setShowOnlyDifferences] = React.useState(true);
+  const [searchTerm, setSearchTerm] = React.useState("");
+  const [expandedRows, setExpandedRows] = React.useState<Set<string>>(new Set());
 
   // Stats for summary
   const totalTags = comparisonResults.length;
@@ -119,41 +178,56 @@ export function SEDPCompareDialog({
     return { columnLabelMap: labelMap, columnUomMap: uomMap };
   }, [columnsJSON]);
 
-  // Filter results based on active tab
+  // Filter and search results
   const filteredResults = React.useMemo(() => {
+    let results = comparisonResults;
+    
+    // Filter by tab
     switch (activeTab) {
       case "matching":
-        return comparisonResults.filter(r => r.isMatching);
+        results = results.filter(r => r.isMatching);
+        break;
       case "differences":
-        return comparisonResults.filter(r => !r.isMatching);
+        results = results.filter(r => !r.isMatching);
+        break;
       case "all":
       default:
-        return comparisonResults;
+        break;
+    }
+
+    // Apply search filter
+    if (searchTerm.trim()) {
+      const search = searchTerm.toLowerCase();
+      results = results.filter(r => 
+        r.tagNo.toLowerCase().includes(search) || 
+        r.tagDesc.toLowerCase().includes(search)
+      );
     }
-  }, [comparisonResults, activeTab]);
 
-  // 변경: 표시할 컬럼 결정 (차이가 있는 컬럼만 or 모든 컬럼)
-  const columnsToDisplay = React.useMemo(() => {
-    // 기본 컬럼 (TAG_NO, TAG_DESC 제외)
-    const columns = columnsJSON.filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC");
+    return results;
+  }, [comparisonResults, activeTab, searchTerm]);
 
-    if (!showOnlyDifferences) {
-      return columns; // 모든 컬럼 표시
+  // Toggle row expansion
+  const toggleRowExpansion = (tagNo: string) => {
+    const newExpanded = new Set(expandedRows);
+    if (newExpanded.has(tagNo)) {
+      newExpanded.delete(tagNo);
+    } else {
+      newExpanded.add(tagNo);
     }
+    setExpandedRows(newExpanded);
+  };
 
-    // 하나라도 차이가 있는 속성만 필터링
-    const columnsWithDifferences = new Set<string>();
-    comparisonResults.forEach(result => {
-      result.attributes.forEach(attr => {
-        if (!attr.isMatching) {
-          columnsWithDifferences.add(attr.key);
-        }
+  // Auto-expand rows with differences when switching to differences tab
+  React.useEffect(() => {
+    if (activeTab === "differences") {
+      const newExpanded = new Set<string>();
+      filteredResults.filter(r => !r.isMatching).forEach(r => {
+        newExpanded.add(r.tagNo);
       });
-    });
-
-    // 차이가 있는 컬럼만 반환
-    return columns.filter(col => columnsWithDifferences.has(col.key));
-  }, [columnsJSON, comparisonResults, showOnlyDifferences]);
+      setExpandedRows(newExpanded);
+    }
+  }, [activeTab, filteredResults]);
 
   const fetchAndCompareData = React.useCallback(async () => {
     if (!projectCode || !formCode) {
@@ -294,20 +368,34 @@ export function SEDPCompareDialog({
 
   return (
     <Dialog open={isOpen} onOpenChange={onClose}>
-      <DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
+      <DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
         <DialogHeader>
           <DialogTitle className="mb-2">SEDP 데이터 비교</DialogTitle>
           <div className="flex items-center justify-between gap-2 pr-8">
-            <div className="flex items-center gap-2">
-              <Switch
-                checked={showOnlyDifferences}
-                onCheckedChange={setShowOnlyDifferences}
-                id="show-differences"
-              />
-              <label htmlFor="show-differences" className="text-sm cursor-pointer">
-                차이가 있는 항목만 표시
-              </label>
+            <div className="flex items-center gap-4">
+              <div className="flex items-center gap-2">
+                <Switch
+                  checked={showOnlyDifferences}
+                  onCheckedChange={setShowOnlyDifferences}
+                  id="show-differences"
+                />
+                <label htmlFor="show-differences" className="text-sm cursor-pointer">
+                  차이가 있는 항목만 표시
+                </label>
+              </div>
+              
+              {/* 검색 입력 */}
+              <div className="relative">
+                <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="태그 번호 또는 설명 검색..."
+                  value={searchTerm}
+                  onChange={(e) => setSearchTerm(e.target.value)}
+                  className="pl-8 w-64"
+                />
+              </div>
             </div>
+            
             <div className="flex items-center gap-2">
               <Badge variant={matchingTags === totalTags && totalMissingTags === 0 ? "default" : "destructive"}>
                 {matchingTags} / {totalTags} 일치 {totalMissingTags > 0 ? `(${totalMissingTags} 누락)` : ''}
@@ -329,7 +417,7 @@ export function SEDPCompareDialog({
           </div>
         </DialogHeader>
 
-        {/* 범례 추가 */}
+        {/* 범례 */}
         <div className="mb-4">
           <ColorLegend />
         </div>
@@ -357,7 +445,7 @@ export function SEDPCompareDialog({
                   <div>
                     <h3 className="text-sm font-medium mb-2">로컬에만 있는 태그 ({missingTags.localOnly.length})</h3>
                     <Table>
-                      <TableHeader>
+                      <TableHeader className="sticky top-0 bg-background z-10">
                         <TableRow>
                           <TableHead className="w-[180px]">Tag Number</TableHead>
                           <TableHead>Tag Description</TableHead>
@@ -379,7 +467,7 @@ export function SEDPCompareDialog({
                   <div>
                     <h3 className="text-sm font-medium mb-2">SEDP에만 있는 태그 ({missingTags.sedpOnly.length})</h3>
                     <Table>
-                      <TableHeader>
+                      <TableHeader className="sticky top-0 bg-background z-10">
                         <TableRow>
                           <TableHead className="w-[180px]">Tag Number</TableHead>
                           <TableHead>Tag Description</TableHead>
@@ -404,100 +492,85 @@ export function SEDPCompareDialog({
                 )}
               </div>
             ) : filteredResults.length > 0 ? (
-              // 개선된 테이블 구조
-              <div className="overflow-x-auto">
+              // 개선된 확장 가능한 테이블 구조
+              <div className="border rounded-md">
                 <Table>
-                  <TableHeader>
+                  <TableHeader className="sticky top-0 bg-muted/50 z-10">
                     <TableRow>
-                      <TableHead className="sticky left-0 z-10 bg-background w-[180px]">Tag Number</TableHead>
-                      <TableHead className="sticky left-[180px] z-10 bg-background w-[200px]">Tag Description</TableHead>
-                      <TableHead className="sticky left-[380px] z-10 bg-background w-[100px]">상태</TableHead>
-                      
-                      {/* 동적으로 속성 열 헤더 생성 */}
-                      {columnsToDisplay.length > 0 ? (
-                        columnsToDisplay.map(col => (
-                          <TableHead key={col.key} className="min-w-[150px]">
-                            {columnLabelMap[col.key] || col.key}
-                            {columnUomMap[col.key] && (
-                              <span className="text-xs text-muted-foreground ml-1">
-                                ({columnUomMap[col.key]})
-                              </span>
-                            )}
-                          </TableHead>
-                        ))
-                      ) : (
-                        <TableHead>
-                          <div className="flex items-center justify-center text-muted-foreground">
-                            <EyeOff className="h-4 w-4 mr-2" />
-                            <span>차이가 있는 항목이 없습니다</span>
-                          </div>
-                        </TableHead>
-                      )}
+                      <TableHead className="w-12"></TableHead>
+                      <TableHead className="w-[180px]">Tag Number</TableHead>
+                      <TableHead className="w-[250px]">Tag Description</TableHead>
+                      <TableHead className="w-[120px]">상태</TableHead>
+                      <TableHead>차이점 개수</TableHead>
                     </TableRow>
                   </TableHeader>
                   <TableBody>
                     {filteredResults.map((result) => (
-                      <TableRow key={result.tagNo} className={!result.isMatching ? "bg-muted/30" : ""}>
-                        <TableCell className="sticky left-0 z-10 bg-background font-medium">
-                          {result.tagNo}
-                        </TableCell>
-                        <TableCell className="sticky left-[180px] z-10 bg-background">
-                          {result.tagDesc}
-                        </TableCell>
-                        <TableCell className="sticky left-[380px] z-10 bg-background">
-                          {result.isMatching ? (
-                            <Badge variant="default" className="flex items-center gap-1">
-                              <CheckCircle className="h-3 w-3" />
-                              <span>일치</span>
-                            </Badge>
-                          ) : (
-                            <Badge variant="destructive" className="flex items-center gap-1">
-                              <AlertCircle className="h-3 w-3" />
-                              <span>차이 있음</span>
-                            </Badge>
-                          )}
-                        </TableCell>
-                        
-                        {/* 각 속성에 대한 셀 동적 생성 */}
-                        {columnsToDisplay.length > 0 ? (
-                          columnsToDisplay.map(col => {
-                            const attr = result.attributes.find(a => a.key === col.key);
-                            
-                            if (!attr) return <TableCell key={col.key}>-</TableCell>;
-                            
-                            return (
-                              <TableCell 
-                                key={col.key} 
-                                className={!attr.isMatching ? "bg-muted/50" : ""}
-                              >
-                                {attr.isMatching ? (
-                                  <DisplayValue value={attr.localValue} uom={attr.uom} />
-                                ) : (
-                                  <div className="flex flex-col gap-1">
-                                    <div className="line-through text-red-500">
-                                      <DisplayValue value={attr.localValue} uom={attr.uom} isSedp={false} />
-                                    </div>
-                                    <div className="text-green-500">
-                                      <DisplayValue value={attr.sedpValue} uom={attr.uom} isSedp={true} />
-                                    </div>
-                                  </div>
-                                )}
-                              </TableCell>
-                            );
-                          })
-                        ) : (
+                      <React.Fragment key={result.tagNo}>
+                        {/* 메인 행 */}
+                        <TableRow 
+                          className={`hover:bg-muted/20 cursor-pointer ${!result.isMatching ? "bg-muted/10" : ""}`}
+                          onClick={() => toggleRowExpansion(result.tagNo)}
+                        >
                           <TableCell>
-                            <span className="text-muted-foreground">모든 값이 일치합니다</span>
+                            {result.attributes.some(attr => !attr.isMatching) ? (
+                              expandedRows.has(result.tagNo) ? (
+                                <ChevronDown className="h-4 w-4" />
+                              ) : (
+                                <ChevronRight className="h-4 w-4" />
+                              )
+                            ) : null}
+                          </TableCell>
+                          <TableCell className="font-medium">
+                            {result.tagNo}
                           </TableCell>
+                          <TableCell title={result.tagDesc}>
+                            <div className="truncate">
+                              {result.tagDesc}
+                            </div>
+                          </TableCell>
+                          <TableCell>
+                            {result.isMatching ? (
+                              <Badge variant="default" className="flex items-center gap-1">
+                                <CheckCircle className="h-3 w-3" />
+                                <span>일치</span>
+                              </Badge>
+                            ) : (
+                              <Badge variant="destructive" className="flex items-center gap-1">
+                                <AlertCircle className="h-3 w-3" />
+                                <span>차이</span>
+                              </Badge>
+                            )}
+                          </TableCell>
+                          <TableCell>
+                            {!result.isMatching && (
+                              <span className="text-sm text-muted-foreground">
+                                {result.attributes.filter(attr => !attr.isMatching).length}개 속성이 다름
+                              </span>
+                            )}
+                          </TableCell>
+                        </TableRow>
+                        
+                        {/* 확장된 차이점 표시 행 */}
+                        {expandedRows.has(result.tagNo) && (
+                          <TableRow>
+                            <TableCell colSpan={5} className="p-0 bg-muted/5">
+                              <DifferencesCard 
+                                attributes={result.attributes}
+                                columnLabelMap={columnLabelMap}
+                                showOnlyDifferences={showOnlyDifferences}
+                              />
+                            </TableCell>
+                          </TableRow>
                         )}
-                      </TableRow>
+                      </React.Fragment>
                     ))}
                   </TableBody>
                 </Table>
               </div>
             ) : (
               <div className="flex items-center justify-center h-full text-muted-foreground">
-                현재 필터에 맞는 태그가 없습니다
+                {searchTerm ? "검색 결과가 없습니다" : "현재 필터에 맞는 태그가 없습니다"}
               </div>
             )}
           </TabsContent>
-- 
cgit v1.2.3

