diff options
Diffstat (limited to 'lib/techsales-rfq/table/create-rfq-dialog.tsx')
| -rw-r--r-- | lib/techsales-rfq/table/create-rfq-dialog.tsx | 917 |
1 files changed, 0 insertions, 917 deletions
diff --git a/lib/techsales-rfq/table/create-rfq-dialog.tsx b/lib/techsales-rfq/table/create-rfq-dialog.tsx deleted file mode 100644 index 81c85649..00000000 --- a/lib/techsales-rfq/table/create-rfq-dialog.tsx +++ /dev/null @@ -1,917 +0,0 @@ -"use client" - -import * as React from "react" -import { toast } from "sonner" -import { ArrowUpDown, CheckSquare, Plus, Search, Square, X, Loader2 } from "lucide-react" -import { Input } from "@/components/ui/input" -import { Calendar } from "@/components/ui/calendar" -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" -import { CalendarIcon } from "lucide-react" -import { format } from "date-fns" -import { ko } from "date-fns/locale" - -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - FormDescription, -} from "@/components/ui/form" -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import * as z from "zod" -import { EstimateProjectSelector } from "@/components/BidProjectSelector" -import { type Project } from "@/lib/rfqs/service" -import { createTechSalesRfq } from "@/lib/techsales-rfq/service" -import { useSession } from "next-auth/react" -import { Separator } from "@/components/ui/separator" -import { Badge } from "@/components/ui/badge" -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { cn } from "@/lib/utils" -import { ScrollArea } from "@/components/ui/scroll-area" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" - -// 실제 데이터 서비스 import -import { - getWorkTypes, - getAllShipbuildingItemsForCache, - getShipTypes, - type ShipbuildingItem, - type WorkType -} from "@/lib/items-tech/service" - -// 유효성 검증 스키마 - 자재코드(item_code) 배열로 변경 -const createRfqSchema = z.object({ - biddingProjectId: z.number({ - required_error: "프로젝트를 선택해주세요.", - }), - materialCodes: z.array(z.string()).min(1, { - message: "적어도 하나의 자재코드를 선택해야 합니다.", - }), - dueDate: z.date({ - required_error: "마감일을 선택해주세요.", - }), -}) - -// 폼 데이터 타입 -type CreateRfqFormValues = z.infer<typeof createRfqSchema> - -// 공종 타입 정의 -interface WorkTypeOption { - code: WorkType - name: string - description: string -} - -interface CreateRfqDialogProps { - onCreated?: () => void; -} - -export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { - const { data: session } = useSession() - const [isProcessing, setIsProcessing] = React.useState(false) - const [isDialogOpen, setIsDialogOpen] = React.useState(false) - const [selectedProject, setSelectedProject] = React.useState<Project | null>(null) - - // 검색 및 필터링 상태 - const [itemSearchQuery, setItemSearchQuery] = React.useState("") - const [selectedWorkType, setSelectedWorkType] = React.useState<WorkType | null>(null) - const [selectedShipType, setSelectedShipType] = React.useState<string | null>(null) - const [selectedItems, setSelectedItems] = React.useState<ShipbuildingItem[]>([]) - - // 데이터 상태 - const [workTypes, setWorkTypes] = React.useState<WorkTypeOption[]>([]) - const [allItems, setAllItems] = React.useState<ShipbuildingItem[]>([]) - const [shipTypes, setShipTypes] = React.useState<string[]>([]) - const [isLoadingItems, setIsLoadingItems] = React.useState(false) - const [dataLoadError, setDataLoadError] = React.useState<string | null>(null) - const [retryCount, setRetryCount] = React.useState(0) - - // 데이터 로딩 함수를 useCallback으로 메모이제이션 - const loadData = React.useCallback(async (isRetry = false) => { - try { - if (!isRetry) { - setIsLoadingItems(true) - setDataLoadError(null) - } - - console.log(`데이터 로딩 시작... ${isRetry ? `(재시도 ${retryCount + 1}회)` : ''}`) - - const [workTypesResult, itemsResult, shipTypesResult] = await Promise.all([ - getWorkTypes(), - getAllShipbuildingItemsForCache(), - getShipTypes() - ]) - - console.log("WorkTypes 결과:", workTypesResult) - console.log("Items 결과:", itemsResult) - console.log("ShipTypes 결과:", shipTypesResult) - - // WorkTypes 설정 - if (Array.isArray(workTypesResult)) { - setWorkTypes(workTypesResult) - } else { - console.error("WorkTypes 데이터 형식 오류:", workTypesResult) - throw new Error("공종 데이터를 불러올 수 없습니다.") - } - - // Items 설정 - if (!itemsResult.error && itemsResult.data && Array.isArray(itemsResult.data)) { - setAllItems(itemsResult.data) - console.log("아이템 설정 완료:", itemsResult.data.length, "개") - } else { - console.error("아이템 로딩 실패:", itemsResult.error) - throw new Error(itemsResult.error || "아이템 데이터를 불러올 수 없습니다.") - } - - // ShipTypes 설정 - if (!shipTypesResult.error && shipTypesResult.data && Array.isArray(shipTypesResult.data)) { - setShipTypes(shipTypesResult.data) - console.log("선종 설정 완료:", shipTypesResult.data) - } else { - console.error("선종 로딩 실패:", shipTypesResult.error) - throw new Error(shipTypesResult.error || "선종 데이터를 불러올 수 없습니다.") - } - - // 성공 시 재시도 카운터 리셋 - setRetryCount(0) - setDataLoadError(null) - console.log("데이터 로딩 완료") - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' - console.error("데이터 로딩 오류:", errorMessage) - - setDataLoadError(errorMessage) - - // 3회까지 자동 재시도 (500ms 간격) - if (retryCount < 2) { - console.log(`${500 * (retryCount + 1)}ms 후 재시도...`) - setTimeout(() => { - setRetryCount(prev => prev + 1) - loadData(true) - }, 500 * (retryCount + 1)) - } else { - // 재시도 실패 시 사용자에게 알림 - toast.error(`데이터 로딩에 실패했습니다: ${errorMessage}`) - } - } finally { - if (!isRetry) { - setIsLoadingItems(false) - } - } - }, [retryCount]) - - // 다이얼로그가 열릴 때마다 데이터 로딩 - React.useEffect(() => { - if (isDialogOpen) { - // 다이얼로그가 열릴 때마다 데이터 상태 초기화 및 로딩 - setDataLoadError(null) - setRetryCount(0) - - // 이미 데이터가 있고 에러가 없다면 로딩하지 않음 (성능 최적화) - if (allItems.length > 0 && workTypes.length > 0 && shipTypes.length > 0 && !dataLoadError) { - console.log("기존 데이터 사용 (캐시)") - return - } - - loadData() - } - }, [isDialogOpen, loadData, allItems.length, workTypes.length, shipTypes.length, dataLoadError]) - - // 수동 새로고침 함수 - const handleRefreshData = React.useCallback(() => { - setDataLoadError(null) - setRetryCount(0) - loadData() - }, [loadData]) - - // RFQ 생성 폼 - const form = useForm<CreateRfqFormValues>({ - resolver: zodResolver(createRfqSchema), - defaultValues: { - biddingProjectId: undefined, - materialCodes: [], - dueDate: undefined, // 기본값 제거 - } - }) - - // 필터링된 아이템 목록 가져오기 - const availableItems = React.useMemo(() => { - let filtered = [...allItems] - - // 선종 필터 - if (selectedShipType) { - filtered = filtered.filter(item => item.shipTypes === selectedShipType) - } - - // 공종 필터 - if (selectedWorkType) { - filtered = filtered.filter(item => item.workType === selectedWorkType) - } - - // 검색어 필터 - if (itemSearchQuery && itemSearchQuery.trim()) { - const query = itemSearchQuery.toLowerCase().trim() - filtered = filtered.filter(item => - item.itemCode.toLowerCase().includes(query) || - (item.itemList && item.itemList.toLowerCase().includes(query)) - ) - } - - return filtered - }, [allItems, itemSearchQuery, selectedWorkType, selectedShipType]) - - // 사용 가능한 선종 목록 가져오기 - const availableShipTypes = React.useMemo(() => { - return shipTypes - }, [shipTypes]) - - // 프로젝트 선택 처리 - const handleProjectSelect = (project: Project) => { - setSelectedProject(project) - form.setValue("biddingProjectId", project.id) - // 선택 초기화 - setSelectedItems([]) - setSelectedShipType(null) - setSelectedWorkType(null) - setItemSearchQuery("") - form.setValue("materialCodes", []) - } - - // 아이템 선택/해제 처리 - const handleItemToggle = (item: ShipbuildingItem) => { - const isSelected = selectedItems.some(selected => selected.id === item.id) - - if (isSelected) { - // 아이템 선택 해제 - const newSelectedItems = selectedItems.filter(selected => selected.id !== item.id) - setSelectedItems(newSelectedItems) - form.setValue("materialCodes", newSelectedItems.map(item => item.itemCode)) - } else { - // 아이템 선택 추가 - const newSelectedItems = [...selectedItems, item] - setSelectedItems(newSelectedItems) - form.setValue("materialCodes", newSelectedItems.map(item => item.itemCode)) - } - } - - // 아이템 제거 처리 - const handleRemoveItem = (itemId: number) => { - const newSelectedItems = selectedItems.filter(item => item.id !== itemId) - setSelectedItems(newSelectedItems) - form.setValue("materialCodes", newSelectedItems.map(item => item.itemCode)) - } - - // RFQ 생성 함수 - const handleCreateRfq = async (data: CreateRfqFormValues) => { - try { - setIsProcessing(true) - - // 사용자 인증 확인 - if (!session?.user?.id) { - throw new Error("로그인이 필요합니다") - } - - // 선택된 아이템들을 아이템명(itemList)으로 그룹핑 - const groupedItems = selectedItems.reduce((groups, item) => { - const actualItemName = item.itemList // 실제 조선 아이템명 - if (!actualItemName) { - throw new Error(`아이템 "${item.itemCode}"의 아이템명(itemList)이 없습니다.`) - } - if (!groups[actualItemName]) { - groups[actualItemName] = [] - } - groups[actualItemName].push(item) - return groups - }, {} as Record<string, typeof selectedItems>) - - const rfqGroups = Object.entries(groupedItems).map(([actualItemName, items]) => { - const itemCodes = items.map(item => item.itemCode) // 자재그룹코드들 - const joinedItemCodes = itemCodes.join(',') - return { - actualItemName, - items, - itemCodes, - joinedItemCodes, - codeLength: joinedItemCodes.length, - isOverLimit: joinedItemCodes.length > 255 - } - }) - - // 255자 초과 그룹 확인 - const overLimitGroups = rfqGroups.filter(group => group.isOverLimit) - if (overLimitGroups.length > 0) { - const groupNames = overLimitGroups.map(g => `"${g.actualItemName}" (${g.codeLength}자)`).join(', ') - throw new Error(`다음 아이템 그룹의 자재코드가 255자를 초과합니다: ${groupNames}`) - } - - // 각 그룹별로 RFQ 생성 - const createPromises = rfqGroups.map(group => - createTechSalesRfq({ - biddingProjectId: data.biddingProjectId, - itemShipbuildingId: group.items[0].id, // 그룹의 첫 번째 아이템의 shipbuilding ID 사용 - materialGroupCodes: group.itemCodes, // 해당 그룹의 자재코드들 - createdBy: Number(session.user.id), - dueDate: data.dueDate, - }) - ) - - const results = await Promise.all(createPromises) - - // 오류 확인 - const errors = results.filter(result => result.error) - if (errors.length > 0) { - throw new Error(errors.map(e => e.error).join(', ')) - } - - // 성공적으로 생성되면 다이얼로그 닫기 및 메시지 표시 - const totalRfqs = results.reduce((sum, result) => sum + (result.data?.length || 0), 0) - toast.success(`${rfqGroups.length}개 아이템 그룹으로 총 ${totalRfqs}개의 RFQ가 성공적으로 생성되었습니다`) - setIsDialogOpen(false) - form.reset({ - biddingProjectId: undefined, - materialCodes: [], - dueDate: undefined, // 기본값 제거 - }) - setSelectedProject(null) - setItemSearchQuery("") - setSelectedWorkType(null) - setSelectedShipType(null) - setSelectedItems([]) - // 에러 상태 및 재시도 카운터 초기화 - setDataLoadError(null) - setRetryCount(0) - - // 생성 후 콜백 실행 - if (onCreated) { - onCreated() - } - - } catch (error) { - console.error("RFQ 생성 오류:", error) - toast.error(`RFQ 생성 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) - } finally { - setIsProcessing(false) - } - } - - return ( - <Dialog - open={isDialogOpen} - onOpenChange={(open) => { - setIsDialogOpen(open) - if (!open) { - form.reset({ - biddingProjectId: undefined, - materialCodes: [], - dueDate: undefined, // 기본값 제거 - }) - setSelectedProject(null) - setItemSearchQuery("") - setSelectedWorkType(null) - setSelectedShipType(null) - setSelectedItems([]) - // 에러 상태 및 재시도 카운터 초기화 - setDataLoadError(null) - setRetryCount(0) - } - }} - > - <DialogTrigger asChild> - <Button - variant="default" - size="sm" - className="gap-2" - disabled={isProcessing} - > - <Plus className="size-4" aria-hidden="true" /> - <span className="hidden sm:inline">RFQ 생성</span> - </Button> - </DialogTrigger> - <DialogContent - className="max-w-none h-[90vh] overflow-y-auto flex flex-col" - style={{ width: '1200px' }} - > - <DialogHeader className="border-b pb-4"> - <DialogTitle>RFQ 생성</DialogTitle> - </DialogHeader> - - <div className="space-y-6 p-1 overflow-y-auto"> - <Form {...form}> - <form onSubmit={form.handleSubmit(handleCreateRfq)} className="space-y-6"> - {/* 프로젝트 선택 */} - <div className="space-y-4"> - <FormField - control={form.control} - name="biddingProjectId" - render={({ field }) => ( - <FormItem> - <FormLabel>입찰 프로젝트</FormLabel> - <FormControl> - <EstimateProjectSelector - selectedProjectId={field.value} - onProjectSelect={handleProjectSelect} - placeholder="입찰 프로젝트를 선택하세요" - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <Separator className="my-4" /> - - {/* 선종 선택 */} - <div className="space-y-4"> - <div> - <FormLabel>선종 선택</FormLabel> - </div> - - {/* 데이터 로딩 에러 표시 */} - {dataLoadError && ( - <div className="p-3 bg-destructive/10 border border-destructive/20 rounded-md"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <X className="h-4 w-4 text-destructive" /> - <span className="text-sm text-destructive">{dataLoadError}</span> - </div> - <Button - variant="outline" - size="sm" - onClick={handleRefreshData} - disabled={isLoadingItems} - className="h-8 text-xs" - > - {isLoadingItems ? ( - <> - <Loader2 className="h-3 w-3 animate-spin mr-1" /> - 재시도 중... - </> - ) : ( - "다시 시도" - )} - </Button> - </div> - </div> - )} - - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - variant="outline" - className="w-full justify-between" - disabled={!selectedProject || isLoadingItems || dataLoadError !== null} - > - {isLoadingItems ? ( - <> - <Loader2 className="h-4 w-4 animate-spin mr-2" /> - 데이터 로딩 중... - </> - ) : dataLoadError ? ( - "데이터 로딩 실패" - ) : selectedShipType ? ( - selectedShipType - ) : ( - "전체조회: 선종을 선택해야 생성가능합니다." - )} - <ArrowUpDown className="ml-2 h-4 w-4 opacity-50" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-full max-h-60 overflow-y-auto"> - <DropdownMenuCheckboxItem - checked={selectedShipType === null} - onCheckedChange={() => { - setSelectedShipType(null) - setSelectedItems([]) - form.setValue("materialCodes", []) - }} - > - 전체 선종 - </DropdownMenuCheckboxItem> - {availableShipTypes.map(shipType => ( - <DropdownMenuCheckboxItem - key={shipType} - checked={selectedShipType === shipType} - onCheckedChange={() => { - setSelectedShipType(shipType) - setSelectedItems([]) - form.setValue("materialCodes", []) - }} - > - {shipType} - </DropdownMenuCheckboxItem> - ))} - </DropdownMenuContent> - </DropdownMenu> - </div> - - <Separator className="my-4" /> - - {/* 마감일 설정 */} - <FormField - control={form.control} - name="dueDate" - render={({ field }) => ( - <FormItem className="flex flex-col"> - <FormLabel>마감일</FormLabel> - <Popover> - <PopoverTrigger asChild> - <FormControl> - <Button - variant="outline" - className={cn( - "w-full pl-3 text-left font-normal", - !field.value && "text-muted-foreground" - )} - > - {field.value ? ( - format(field.value, "PPP", { locale: ko }) - ) : ( - <span>마감일을 선택하세요</span> - )} - <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> - </Button> - </FormControl> - </PopoverTrigger> - <PopoverContent className="w-auto p-0" align="start"> - <Calendar - mode="single" - selected={field.value} - onSelect={field.onChange} - disabled={(date) => - date < new Date() || date < new Date("1900-01-01") - } - initialFocus - /> - </PopoverContent> - </Popover> - <FormMessage /> - </FormItem> - )} - /> - - <Separator className="my-4" /> - - <div className="space-y-6"> - {/* 아이템 선택 영역 */} - <div className="space-y-4"> - <div> - <FormLabel>조선 아이템 선택</FormLabel> - <FormDescription> - {selectedShipType ? `선종 ${selectedShipType}의 공종별 아이템을 선택하세요` : "먼저 선종을 선택해주세요"} - </FormDescription> - </div> - - {/* 아이템 검색 및 필터 */} - <div className="space-y-2"> - <div className="flex space-x-2"> - <div className="relative flex-1"> - <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> - <Input - placeholder="아이템 검색..." - value={itemSearchQuery} - onChange={(e) => setItemSearchQuery(e.target.value)} - className="pl-8 pr-8" - disabled={!selectedShipType || isLoadingItems || dataLoadError !== null} - /> - {itemSearchQuery && ( - <Button - variant="ghost" - size="sm" - className="absolute right-0 top-0 h-full px-3" - onClick={() => setItemSearchQuery("")} - disabled={!selectedShipType || isLoadingItems || dataLoadError !== null} - > - <X className="h-4 w-4" /> - </Button> - )} - </div> - - {/* 공종 필터 */} - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - variant="outline" - className="gap-1" - disabled={!selectedShipType || isLoadingItems || dataLoadError !== null} - > - {selectedWorkType ? workTypes.find(wt => wt.code === selectedWorkType)?.name : "전체 공종"} - <ArrowUpDown className="ml-2 h-4 w-4 opacity-50" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuCheckboxItem - checked={selectedWorkType === null} - onCheckedChange={() => setSelectedWorkType(null)} - > - 전체 공종 - </DropdownMenuCheckboxItem> - {workTypes.map(workType => ( - <DropdownMenuCheckboxItem - key={workType.code} - checked={selectedWorkType === workType.code} - onCheckedChange={() => setSelectedWorkType(workType.code)} - > - {workType.name} - </DropdownMenuCheckboxItem> - ))} - </DropdownMenuContent> - </DropdownMenu> - </div> - </div> - - {/* 아이템 목록 */} - <div className="border rounded-md"> - <ScrollArea className="h-[300px]"> - <div className="p-2 space-y-1"> - {dataLoadError ? ( - <div className="text-center py-8"> - <div className="p-4 bg-destructive/10 border border-destructive/20 rounded-md mx-4"> - <div className="flex flex-col items-center gap-3"> - <X className="h-8 w-8 text-destructive" /> - <div className="text-center"> - <p className="text-sm text-destructive font-medium">데이터 로딩에 실패했습니다</p> - <p className="text-xs text-muted-foreground mt-1">{dataLoadError}</p> - </div> - <Button - variant="outline" - size="sm" - onClick={handleRefreshData} - disabled={isLoadingItems} - className="h-8" - > - {isLoadingItems ? ( - <> - <Loader2 className="h-3 w-3 animate-spin mr-1" /> - 재시도 중... - </> - ) : ( - "다시 시도" - )} - </Button> - </div> - </div> - </div> - ) : isLoadingItems ? ( - <div className="text-center py-8 text-muted-foreground"> - <Loader2 className="h-6 w-6 animate-spin mx-auto mb-2" /> - 아이템을 불러오는 중... - {retryCount > 0 && ( - <p className="text-xs mt-1">재시도 {retryCount}회</p> - )} - </div> - ) : availableItems.length > 0 ? ( - [...availableItems] - .sort((a, b) => { - // itemList 기준으로 정렬 (없는 경우 itemName 사용, 둘 다 없으면 맨 뒤로) - const aName = a.itemList || 'zzz' - const bName = b.itemList || 'zzz' - return aName.localeCompare(bName, 'ko', { numeric: true }) - }) - .map((item) => { - const isSelected = selectedItems.some(selected => selected.id === item.id) - return ( - <div - key={item.id} - className={cn( - "flex items-center space-x-2 p-2 rounded-md cursor-pointer hover:bg-muted", - isSelected && "bg-muted" - )} - onClick={() => handleItemToggle(item)} - > - <div className="flex items-center space-x-2 flex-1"> - {isSelected ? ( - <CheckSquare className="h-4 w-4" /> - ) : ( - <Square className="h-4 w-4" /> - )} - <div className="flex-1"> - <div className="font-medium"> - {item.itemList || '아이템명 없음'} - </div> - <div className="text-sm text-muted-foreground"> - {item.itemCode || '자재그룹코드 없음'} - </div> - <div className="text-xs text-muted-foreground"> - 공종: {item.workType} • 선종: {item.shipTypes} - </div> - </div> - </div> - </div> - ) - }) - ) : ( - <div className="text-center py-8 text-muted-foreground"> - {itemSearchQuery ? "검색 결과가 없습니다" : "아이템이 없습니다"} - </div> - )} - </div> - </ScrollArea> - </div> - - {/* 선택된 아이템 목록 */} - <FormField - control={form.control} - name="materialCodes" - render={() => ( - <FormItem> - <FormLabel>선택된 아이템 ({selectedItems.length}개)</FormLabel> - <div className="min-h-[80px] p-3 border rounded-md bg-muted/50"> - {selectedItems.length > 0 ? ( - <div className="flex flex-wrap gap-2"> - {selectedItems.map((item) => ( - <Badge - key={item.id} - variant="secondary" - className="flex items-center gap-1" - > - {item.itemList || '아이템명 없음'} ({item.itemCode}) - <X - className="h-3 w-3 cursor-pointer hover:text-destructive" - onClick={() => handleRemoveItem(item.id)} - /> - </Badge> - ))} - </div> - ) : ( - <div className="flex items-center justify-center h-full text-sm text-muted-foreground"> - 선택된 아이템이 없습니다 - </div> - )} - </div> - <FormMessage /> - </FormItem> - )} - /> - - {/* RFQ 그룹핑 미리보기 */} - <div className="space-y-3"> - <FormLabel>생성될 RFQ 그룹 미리보기</FormLabel> - <div className="border rounded-md bg-background"> - {(() => { - // 아이템명(itemList)으로 그룹핑 - const groupedItems = selectedItems.reduce((groups, item) => { - const actualItemName = item.itemList // 실제 조선 아이템명 - if (!actualItemName) { - return groups // itemList가 없는 경우 제외 - } - if (!groups[actualItemName]) { - groups[actualItemName] = [] - } - groups[actualItemName].push(item) - return groups - }, {} as Record<string, typeof selectedItems>) - - const rfqGroups = Object.entries(groupedItems).map(([actualItemName, items]) => { - const itemCodes = items.map(item => item.itemCode) // 자재그룹코드들 - const joinedItemCodes = itemCodes.join(',') - return { - actualItemName, - items, - itemCodes, - joinedItemCodes, - codeLength: joinedItemCodes.length, - isOverLimit: joinedItemCodes.length > 255 - } - }) - - return ( - <div className="space-y-3"> - <div className="text-sm text-muted-foreground p-3 border-b"> - 총 {rfqGroups.length}개의 RFQ가 생성됩니다 (아이템명별로 그룹핑) - </div> - <ScrollArea className="h-[200px]"> - <Table> - <TableHeader className="sticky top-0 bg-background"> - <TableRow> - <TableHead className="w-[80px]">RFQ #</TableHead> - <TableHead>아이템명</TableHead> - <TableHead className="w-[120px]">자재그룹코드 개수</TableHead> - <TableHead className="w-[100px]">길이</TableHead> - <TableHead className="w-[80px]">상태</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {rfqGroups.map((group, index) => ( - <TableRow - key={group.actualItemName} - className={group.isOverLimit ? "bg-destructive/5" : ""} - > - <TableCell className="font-medium">#{index + 1}</TableCell> - <TableCell> - <div className="max-w-[200px] truncate" title={group.actualItemName}> - {group.actualItemName} - </div> - </TableCell> - <TableCell>{group.itemCodes.length}개</TableCell> - <TableCell> - <span className={group.isOverLimit ? "text-destructive font-medium" : ""}> - {group.codeLength}/255자 - </span> - </TableCell> - <TableCell> - {group.isOverLimit ? ( - <Badge variant="destructive" className="text-xs">초과</Badge> - ) : ( - <Badge variant="secondary" className="text-xs">정상</Badge> - )} - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </ScrollArea> - </div> - ) - })()} - </div> - </div> - </div> - </div> - </div> - </form> - </Form> - </div> - - {/* Footer - Sticky 버튼 영역 */} - <div className="sticky bottom-0 left-0 z-20 bg-background border-t pt-4 mt-4"> - <div className="flex justify-end space-x-2"> - <Button - type="button" - variant="outline" - onClick={() => setIsDialogOpen(false)} - disabled={isProcessing} - > - 취소 - </Button> - <Button - type="button" - onClick={form.handleSubmit(handleCreateRfq)} - disabled={ - isProcessing || - !selectedProject || - selectedItems.length === 0 || - // 255자 초과 그룹이 있는지 확인 - (() => { - const groupedItems = selectedItems.reduce((groups, item) => { - const actualItemName = item.itemList // 실제 조선 아이템명 - if (!actualItemName) { - return groups // itemList가 없는 경우 제외 - } - if (!groups[actualItemName]) { - groups[actualItemName] = [] - } - groups[actualItemName].push(item.itemCode) - return groups - }, {} as Record<string, string[]>) - - return Object.values(groupedItems).some(itemCodes => itemCodes.join(',').length > 255) - })() - } - > - {isProcessing ? "처리 중..." : (() => { - const groupedItems = selectedItems.reduce((groups, item) => { - const actualItemName = item.itemList // 실제 조선 아이템명 - if (!actualItemName) { - return groups // itemList가 없는 경우 제외 - } - if (!groups[actualItemName]) { - groups[actualItemName] = [] - } - groups[actualItemName].push(item.itemCode) - return groups - }, {} as Record<string, string[]>) - - const groupCount = Object.keys(groupedItems).length - return `${groupCount}개 아이템 그룹으로 생성하기` - })()} - </Button> - </div> - </div> - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file |
