From 7433eea5b4bbc0899e255b88e1a7e91f26e9d95b Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Wed, 17 Sep 2025 18:04:10 +0900
Subject: (김준회) 자재그룹 선택기 오류수정, 공용사용을 위한 다이얼로그
 컴포넌트 구현, data-table 오류 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../material-group-selector-dialog-multi.tsx       | 299 +++++++++++++++++++++
 1 file changed, 299 insertions(+)
 create mode 100644 components/common/material/material-group-selector-dialog-multi.tsx

(limited to 'components/common/material/material-group-selector-dialog-multi.tsx')

diff --git a/components/common/material/material-group-selector-dialog-multi.tsx b/components/common/material/material-group-selector-dialog-multi.tsx
new file mode 100644
index 00000000..22a2ef25
--- /dev/null
+++ b/components/common/material/material-group-selector-dialog-multi.tsx
@@ -0,0 +1,299 @@
+"use client";
+
+import React, { useState, useCallback } from "react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from "@/components/ui/dialog";
+import { MaterialGroupSelector } from "./material-group-selector";
+import { MaterialSearchItem } from "@/lib/material/material-group-service";
+import { X } from "lucide-react";
+
+/**
+ * 자재그룹 다중 선택 Dialog 컴포넌트
+ * 
+ * @description
+ * - MaterialGroupSelector를 Dialog로 래핑한 다중 선택 컴포넌트
+ * - 버튼 클릭 시 Dialog가 열리고, 여러 자재를 선택한 후 확인 버튼으로 완료
+ * - 최대 선택 개수 제한 가능
+ * 
+ * @MaterialSearchItem_Structure
+ * 상태에서 관리되는 자재그룹 객체의 형태:
+ * ```typescript
+ * interface MaterialSearchItem {
+ *   materialGroupCode: string;        // 자재그룹코드 (예: "BG2001")
+ *   materialGroupDescription: string; // 자재그룹명 (예: "DOUBLE WETDOOR HINGE")
+ *   materialGroupUom?: string;        // 단위 (예: "EA", "KG") - 선택적
+ *   displayText: string;              // 표시용 텍스트 (code + " - " + description)
+ * }
+ * ```
+ * 
+ * @state
+ * - open: Dialog 열림/닫힘 상태
+ * - selectedMaterials: 현재 선택된 자재들 (배열)
+ * - tempSelectedMaterials: Dialog 내에서 임시로 선택된 자재들 (확인 버튼 클릭 전까지)
+ * 
+ * @callback
+ * - onMaterialsSelect: 자재 선택 완료 시 호출되는 콜백
+ *   - 매개변수: MaterialSearchItem[]
+ *   - 선택된 자재들의 배열 (빈 배열일 수도 있음)
+ * 
+ * @usage
+ * ```tsx
+ * <MaterialGroupSelectorDialogMulti
+ *   triggerLabel="자재 선택 (다중)"
+ *   selectedMaterials={selectedMaterials}
+ *   onMaterialsSelect={(materials) => {
+ *     setSelectedMaterials(materials);
+ *     console.log('선택된 자재들:', materials);
+ *   }}
+ *   maxSelections={5}
+ *   placeholder="자재를 검색하세요..."
+ * />
+ * ```
+ */
+
+interface MaterialGroupSelectorDialogMultiProps {
+  /** Dialog를 여는 트리거 버튼 텍스트 */
+  triggerLabel?: string;
+  /** 현재 선택된 자재들 */
+  selectedMaterials?: MaterialSearchItem[];
+  /** 자재 선택 완료 시 호출되는 콜백 */
+  onMaterialsSelect?: (materials: MaterialSearchItem[]) => void;
+  /** 최대 선택 가능한 자재 개수 */
+  maxSelections?: number;
+  /** 검색 입력창 placeholder */
+  placeholder?: string;
+  /** Dialog 제목 */
+  title?: string;
+  /** Dialog 설명 */
+  description?: string;
+  /** 트리거 버튼 비활성화 여부 */
+  disabled?: boolean;
+  /** 트리거 버튼 variant */
+  triggerVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
+  /** 제외할 자재그룹코드들 */
+  excludeMaterialCodes?: Set<string>;
+  /** 초기 데이터 표시 여부 */
+  showInitialData?: boolean;
+  /** 트리거 버튼에서 선택된 자재들을 표시할지 여부 */
+  showSelectedInTrigger?: boolean;
+}
+
+export function MaterialGroupSelectorDialogMulti({
+  triggerLabel = "자재 선택",
+  selectedMaterials = [],
+  onMaterialsSelect,
+  maxSelections,
+  placeholder = "자재를 검색하세요...",
+  title = "자재 선택 (다중)",
+  description,
+  disabled = false,
+  triggerVariant = "outline",
+  excludeMaterialCodes,
+  showInitialData = true,
+  showSelectedInTrigger = true,
+}: MaterialGroupSelectorDialogMultiProps) {
+  // Dialog 열림/닫힘 상태
+  const [open, setOpen] = useState(false);
+  
+  // Dialog 내에서 임시로 선택된 자재들 (확인 버튼 클릭 전까지)
+  const [tempSelectedMaterials, setTempSelectedMaterials] = useState<MaterialSearchItem[]>([]);
+
+  // Dialog 설명 동적 생성
+  const dialogDescription = description || 
+    (maxSelections 
+      ? `원하는 자재를 검색하고 선택해주세요. (최대 ${maxSelections}개)`
+      : "원하는 자재를 검색하고 선택해주세요."
+    );
+
+  // Dialog 열림 시 현재 선택된 자재들로 임시 선택 초기화
+  const handleOpenChange = useCallback((newOpen: boolean) => {
+    setOpen(newOpen);
+    if (newOpen) {
+      setTempSelectedMaterials([...selectedMaterials]);
+    }
+  }, [selectedMaterials]);
+
+  // 자재 선택 처리 (Dialog 내에서)
+  const handleMaterialsChange = useCallback((materials: MaterialSearchItem[]) => {
+    setTempSelectedMaterials(materials);
+  }, []);
+
+  // 확인 버튼 클릭 시 선택 완료
+  const handleConfirm = useCallback(() => {
+    onMaterialsSelect?.(tempSelectedMaterials);
+    setOpen(false);
+  }, [tempSelectedMaterials, onMaterialsSelect]);
+
+  // 취소 버튼 클릭 시
+  const handleCancel = useCallback(() => {
+    setTempSelectedMaterials([...selectedMaterials]);
+    setOpen(false);
+  }, [selectedMaterials]);
+
+  // 전체 선택 해제
+  const handleClearAll = useCallback(() => {
+    setTempSelectedMaterials([]);
+  }, []);
+
+  // 개별 자재 제거 (트리거 버튼에서)
+  const handleRemoveMaterial = useCallback((materialToRemove: MaterialSearchItem, e: React.MouseEvent) => {
+    e.preventDefault();
+    e.stopPropagation();
+    const newMaterials = selectedMaterials.filter(
+      (material) => !(material.materialGroupCode === materialToRemove.materialGroupCode && 
+                     material.materialGroupDescription === materialToRemove.materialGroupDescription)
+    );
+    onMaterialsSelect?.(newMaterials);
+  }, [selectedMaterials, onMaterialsSelect]);
+
+  // 트리거 버튼 렌더링
+  const renderTriggerContent = () => {
+    if (selectedMaterials.length === 0) {
+      return triggerLabel;
+    }
+
+    if (!showSelectedInTrigger) {
+      return `${triggerLabel} (${selectedMaterials.length}개)`;
+    }
+
+    return (
+      <div className="flex flex-wrap gap-1 items-center max-w-full">
+        <span className="shrink-0">{triggerLabel}:</span>
+        {selectedMaterials.slice(0, 2).map((material) => (
+          <Badge
+            key={`${material.materialGroupCode}-${material.materialGroupDescription}`}
+            variant="secondary"
+            className="gap-1 pr-1 max-w-[120px]"
+          >
+            <span className="truncate text-xs">
+              {material.materialGroupDescription}
+            </span>
+            {!disabled && (
+              <button
+                type="button"
+                className="ml-1 h-3 w-3 rounded-sm hover:bg-red-100 flex items-center justify-center"
+                onClick={(e) => handleRemoveMaterial(material, e)}
+              >
+                <X className="h-3 w-3 hover:text-red-500" />
+              </button>
+            )}
+          </Badge>
+        ))}
+        {selectedMaterials.length > 2 && (
+          <Badge variant="outline" className="text-xs">
+            +{selectedMaterials.length - 2}개
+          </Badge>
+        )}
+      </div>
+    );
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={handleOpenChange}>
+      <DialogTrigger asChild>
+        <Button 
+          variant={triggerVariant} 
+          disabled={disabled}
+          className="min-h-[2.5rem] h-auto justify-start"
+        >
+          {renderTriggerContent()}
+        </Button>
+      </DialogTrigger>
+      
+      <DialogContent className="max-w-md">
+        <DialogHeader>
+          <DialogTitle>{title}</DialogTitle>
+          <DialogDescription>{dialogDescription}</DialogDescription>
+        </DialogHeader>
+        
+        <div className="py-4">
+          <MaterialGroupSelector
+            selectedMaterials={tempSelectedMaterials}
+            onMaterialsChange={handleMaterialsChange}
+            singleSelect={false}
+            maxSelections={maxSelections}
+            placeholder={placeholder}
+            noValuePlaceHolder="자재를 선택해주세요"
+            closeOnSelect={false}
+            excludeMaterialCodes={excludeMaterialCodes}
+            showInitialData={showInitialData}
+          />
+          
+          {/* 선택된 자재 개수 표시 */}
+          <div className="mt-2 text-sm text-muted-foreground">
+            {maxSelections ? (
+              `선택됨: ${tempSelectedMaterials.length}/${maxSelections}개`
+            ) : (
+              `선택됨: ${tempSelectedMaterials.length}개`
+            )}
+          </div>
+        </div>
+
+        <DialogFooter className="gap-2">
+          <Button variant="outline" onClick={handleCancel}>
+            취소
+          </Button>
+          {tempSelectedMaterials.length > 0 && (
+            <Button variant="ghost" onClick={handleClearAll}>
+              전체 해제
+            </Button>
+          )}
+          <Button onClick={handleConfirm}>
+            확인 ({tempSelectedMaterials.length}개)
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}
+
+/**
+ * 사용 예시:
+ * 
+ * ```tsx
+ * const [selectedMaterials, setSelectedMaterials] = useState<MaterialSearchItem[]>([]);
+ * 
+ * return (
+ *   <MaterialGroupSelectorDialogMulti
+ *     triggerLabel="자재 선택"
+ *     selectedMaterials={selectedMaterials}
+ *     onMaterialsSelect={(materials) => {
+ *       setSelectedMaterials(materials);
+ *       console.log('선택된 자재들:', materials.map(m => ({
+ *         code: m.materialGroupCode,
+ *         description: m.materialGroupDescription,
+ *         uom: m.materialGroupUom
+ *       })));
+ *     }}
+ *     maxSelections={5}
+ *     title="필수 자재 선택"
+ *     description="프로젝트에 필요한 자재들을 선택해주세요."
+ *     showSelectedInTrigger={true}
+ *   />
+ * );
+ * ```
+ * 
+ * @advanced_usage
+ * ```tsx
+ * // 제외할 자재가 있는 경우
+ * const excludedCodes = new Set(['MAT001', 'MAT002']);
+ * 
+ * <MaterialGroupSelectorDialogMulti
+ *   selectedMaterials={selectedMaterials}
+ *   onMaterialsSelect={setSelectedMaterials}
+ *   excludeMaterialCodes={excludedCodes}
+ *   maxSelections={3}
+ *   triggerVariant="default"
+ *   showSelectedInTrigger={false}
+ * />
+ * ```
+ */
-- 
cgit v1.2.3

