summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/table/create-rfq-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/table/create-rfq-dialog.tsx')
-rw-r--r--lib/techsales-rfq/table/create-rfq-dialog.tsx917
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