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.tsx244
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>