diff options
Diffstat (limited to 'lib/techsales-rfq/table/create-rfq-dialog.tsx')
| -rw-r--r-- | lib/techsales-rfq/table/create-rfq-dialog.tsx | 244 |
1 files changed, 200 insertions, 44 deletions
diff --git a/lib/techsales-rfq/table/create-rfq-dialog.tsx b/lib/techsales-rfq/table/create-rfq-dialog.tsx index cc652b44..5faa3a0b 100644 --- a/lib/techsales-rfq/table/create-rfq-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-dialog.tsx @@ -197,20 +197,60 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { throw new Error("로그인이 필요합니다") } - // 자재코드(item_code) 배열을 materialGroupCodes로 전달 - const result = await createTechSalesRfq({ - biddingProjectId: data.biddingProjectId, - materialGroupCodes: data.materialCodes, // item_code를 자재코드로 사용 - createdBy: Number(session.user.id), - dueDate: data.dueDate, + // 선택된 아이템들을 아이템명(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, + materialGroupCodes: [group.joinedItemCodes], // 그룹화된 자재코드들 + createdBy: Number(session.user.id), + dueDate: data.dueDate, + }) + ) + + const results = await Promise.all(createPromises) - if (result.error) { - throw new Error(result.error) + // 오류 확인 + const errors = results.filter(result => result.error) + if (errors.length > 0) { + throw new Error(errors.map(e => e.error).join(', ')) } // 성공적으로 생성되면 다이얼로그 닫기 및 메시지 표시 - toast.success(`${result.data?.length || 0}개의 RFQ가 성공적으로 생성되었습니다`) + const totalRfqs = results.reduce((sum, result) => sum + (result.data?.length || 0), 0) + toast.success(`${rfqGroups.length}개 아이템 그룹으로 총 ${totalRfqs}개의 RFQ가 성공적으로 생성되었습니다`) setIsDialogOpen(false) form.reset({ biddingProjectId: undefined, @@ -423,38 +463,45 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { 아이템을 불러오는 중... </div> ) : availableItems.length > 0 ? ( - availableItems.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" /> + [...availableItems] + .sort((a, b) => { + // itemList 기준으로 정렬 (없는 경우 itemName 사용, 둘 다 없으면 맨 뒤로) + const aName = a.itemList || a.itemName || 'zzz' + const bName = b.itemList || b.itemName || '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" )} - <div className="flex-1"> - <div className="font-medium"> - {item.itemList || item.itemName} - </div> - <div className="text-sm text-muted-foreground"> - {item.itemCode} • {item.description || '설명 없음'} - </div> - <div className="text-xs text-muted-foreground"> - 공종: {item.workType} • 선종: {item.shipTypes} + 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 || item.itemName || '아이템명 없음'} + </div> + <div className="text-sm text-muted-foreground"> + {item.itemCode} • {item.description || '설명 없음'} + </div> + <div className="text-xs text-muted-foreground"> + 공종: {item.workType} • 선종: {item.shipTypes} + </div> </div> </div> </div> - </div> - ) - }) + ) + }) ) : ( <div className="text-center py-8 text-muted-foreground"> {itemSearchQuery ? "검색 결과가 없습니다" : "아이템이 없습니다"} @@ -480,7 +527,7 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { variant="secondary" className="flex items-center gap-1" > - {item.itemList || item.itemName} ({item.itemCode}) + {item.itemList || item.itemName || '아이템명 없음'} ({item.itemCode}) <X className="h-3 w-3 cursor-pointer hover:text-destructive" onClick={() => handleRemoveItem(item.id)} @@ -498,19 +545,93 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { </FormItem> )} /> + + {/* RFQ 그룹핑 미리보기 */} + {selectedItems.length > 0 && ( + <div className="space-y-3"> + <FormLabel>생성될 RFQ 그룹 미리보기</FormLabel> + <div className="border rounded-md p-3 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"> + 총 {rfqGroups.length}개의 RFQ가 생성됩니다 (아이템명별로 그룹핑) + </div> + {rfqGroups.map((group, index) => ( + <div + key={group.actualItemName} + className={cn( + "p-3 border rounded-md space-y-2", + group.isOverLimit && "border-destructive bg-destructive/5" + )} + > + <div className="flex items-center justify-between"> + <div className="font-medium"> + RFQ #{index + 1}: {group.actualItemName} + </div> + <Badge variant={group.isOverLimit ? "destructive" : "secondary"}> + {group.itemCodes.length}개 자재코드 ({group.codeLength}/255자) + </Badge> + </div> + <div className="text-sm text-muted-foreground"> + 자재코드: {group.joinedItemCodes} + </div> + {group.isOverLimit && ( + <div className="text-sm text-destructive"> + ⚠️ 자재코드 길이가 255자를 초과합니다. 일부 아이템을 제거해주세요. + </div> + )} + <div className="text-xs text-muted-foreground"> + 포함된 아이템: {group.items.map(item => `${item.itemCode}`).join(', ')} + </div> + </div> + ))} + </div> + ) + })()} + </div> + </div> + )} </div> </div> )} {/* 안내 메시지 */} - {selectedProject && ( + {/* {selectedProject && ( <div className="text-sm text-muted-foreground bg-muted p-3 rounded-md"> <p>• 공종별 조선 아이템을 선택하세요.</p> - <p>• 선택된 아이템의 자재코드(item_code)별로 개별 RFQ가 생성됩니다.</p> - <p>• 아이템 코드가 자재 그룹 코드로 사용됩니다.</p> + <p>• <strong>같은 아이템명의 다른 자재코드들은 하나의 RFQ로 그룹핑됩니다.</strong></p> + <p>• 그룹핑된 자재코드들은 comma로 구분되어 저장됩니다.</p> + <p>• 자재코드 길이는 최대 255자까지 가능합니다.</p> <p>• 마감일은 벤더가 견적을 제출해야 하는 날짜입니다.</p> </div> - )} + )} */} <div className="flex justify-end space-x-2 pt-4"> <Button @@ -523,9 +644,44 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { </Button> <Button type="submit" - disabled={isProcessing || !selectedProject || selectedItems.length === 0} + 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 ? "처리 중..." : `${selectedItems.length}개 자재코드로 생성하기`} + {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> </form> |
