From 2fdce8d7a57c792bba0ac36fa554dca9c9cc31e3 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 30 May 2025 03:36:16 +0000 Subject: (김준회) 기술영업 조선 RFQ 오류 수정 및 선종 필터 기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/techsales-rfq/table/create-rfq-dialog.tsx | 749 +++++++++++++++++--------- 1 file changed, 487 insertions(+), 262 deletions(-) (limited to 'lib/techsales-rfq/table/create-rfq-dialog.tsx') diff --git a/lib/techsales-rfq/table/create-rfq-dialog.tsx b/lib/techsales-rfq/table/create-rfq-dialog.tsx index 5faa3a0b..bacec02e 100644 --- a/lib/techsales-rfq/table/create-rfq-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-dialog.tsx @@ -44,12 +44,20 @@ import { } 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 { - getShipbuildingItemsByWorkType, - searchShipbuildingItems, getWorkTypes, + getAllShipbuildingItemsForCache, + getShipTypes, type ShipbuildingItem, type WorkType } from "@/lib/items-tech/service" @@ -81,7 +89,7 @@ interface CreateRfqDialogProps { onCreated?: () => void; } -export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { +export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { const { data: session } = useSession() const [isProcessing, setIsProcessing] = React.useState(false) const [isDialogOpen, setIsDialogOpen] = React.useState(false) @@ -90,13 +98,115 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { // 검색 및 필터링 상태 const [itemSearchQuery, setItemSearchQuery] = React.useState("") const [selectedWorkType, setSelectedWorkType] = React.useState(null) + const [selectedShipType, setSelectedShipType] = React.useState(null) const [selectedItems, setSelectedItems] = React.useState([]) - const [isSearchingItems, setIsSearchingItems] = React.useState(false) // 데이터 상태 const [workTypes, setWorkTypes] = React.useState([]) - const [availableItems, setAvailableItems] = React.useState([]) + const [allItems, setAllItems] = React.useState([]) + const [shipTypes, setShipTypes] = React.useState([]) const [isLoadingItems, setIsLoadingItems] = React.useState(false) + const [dataLoadError, setDataLoadError] = React.useState(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({ @@ -104,55 +214,42 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { defaultValues: { biddingProjectId: undefined, materialCodes: [], - dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14일 후 + dueDate: undefined, // 기본값 제거 } }) - // 공종 목록 로드 - React.useEffect(() => { - const loadWorkTypes = async () => { - const types = await getWorkTypes() - setWorkTypes(types) + // 필터링된 아이템 목록 가져오기 + const availableItems = React.useMemo(() => { + let filtered = [...allItems] + + // 선종 필터 + if (selectedShipType) { + filtered = filtered.filter(item => item.shipTypes === selectedShipType) } - loadWorkTypes() - }, []) - // 아이템 데이터 로드 - const loadItems = React.useCallback(async () => { - setIsLoadingItems(true) - try { - let result - if (itemSearchQuery.trim()) { - result = await searchShipbuildingItems(itemSearchQuery, selectedWorkType || undefined) - } else { - result = await getShipbuildingItemsByWorkType(selectedWorkType || undefined) - } - - if (result.error) { - toast.error(`아이템 로드 오류: ${result.error}`) - setAvailableItems([]) - } else { - setAvailableItems(result.data || []) - } - } catch (error) { - console.error("아이템 로드 오류:", error) - toast.error("아이템을 불러오는 중 오류가 발생했습니다") - setAvailableItems([]) - } finally { - setIsLoadingItems(false) + // 공종 필터 + if (selectedWorkType) { + filtered = filtered.filter(item => item.workType === selectedWorkType) } - }, [itemSearchQuery, selectedWorkType]) - // 아이템 검색 디바운스 - React.useEffect(() => { - setIsSearchingItems(true) - const timer = setTimeout(() => { - loadItems() - setIsSearchingItems(false) - }, 300) + // 검색어 필터 + if (itemSearchQuery && itemSearchQuery.trim()) { + const query = itemSearchQuery.toLowerCase().trim() + filtered = filtered.filter(item => + item.itemCode.toLowerCase().includes(query) || + item.itemName.toLowerCase().includes(query) || + (item.description && item.description.toLowerCase().includes(query)) || + (item.itemList && item.itemList.toLowerCase().includes(query)) + ) + } + + return filtered + }, [allItems, itemSearchQuery, selectedWorkType, selectedShipType]) - return () => clearTimeout(timer) - }, [loadItems]) + // 사용 가능한 선종 목록 가져오기 + const availableShipTypes = React.useMemo(() => { + return shipTypes + }, [shipTypes]) // 프로젝트 선택 처리 const handleProjectSelect = (project: Project) => { @@ -160,6 +257,9 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { form.setValue("biddingProjectId", project.id) // 선택 초기화 setSelectedItems([]) + setSelectedShipType(null) + setSelectedWorkType(null) + setItemSearchQuery("") form.setValue("materialCodes", []) } @@ -255,13 +355,16 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { form.reset({ biddingProjectId: undefined, materialCodes: [], - dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14일 후로 재설정 + dueDate: undefined, // 기본값 제거 }) setSelectedProject(null) setItemSearchQuery("") setSelectedWorkType(null) + setSelectedShipType(null) setSelectedItems([]) - setAvailableItems([]) + // 에러 상태 및 재시도 카운터 초기화 + setDataLoadError(null) + setRetryCount(0) // 생성 후 콜백 실행 if (onCreated) { @@ -277,7 +380,7 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { } return ( - { setIsDialogOpen(open) @@ -285,13 +388,16 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { form.reset({ biddingProjectId: undefined, materialCodes: [], - dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14일 후로 재설정 + dueDate: undefined, // 기본값 제거 }) setSelectedProject(null) setItemSearchQuery("") setSelectedWorkType(null) + setSelectedShipType(null) setSelectedItems([]) - setAvailableItems([]) + // 에러 상태 및 재시도 카운터 초기화 + setDataLoadError(null) + setRetryCount(0) } }} > @@ -306,95 +412,177 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { RFQ 생성 - - + + RFQ 생성 -
+
- + {/* 프로젝트 선택 */} - ( - - 입찰 프로젝트 - - - - - - )} - /> - - - - {/* 마감일 설정 */} - ( - - 마감일 - - - - - - - - - date < new Date() || date < new Date("1900-01-01") - } - initialFocus +
+ ( + + 입찰 프로젝트 + + - - - - 벤더가 견적을 제출해야 하는 마감일입니다. - - - - )} - /> - - - - {!selectedProject ? ( -
- 먼저 프로젝트를 선택해주세요 + + + + )} + /> + + + + {/* 선종 선택 */} +
+
+ 선종 선택 +
+ + {/* 데이터 로딩 에러 표시 */} + {dataLoadError && ( +
+
+
+ + {dataLoadError} +
+ +
+
+ )} + + + + + + + { + setSelectedShipType(null) + setSelectedItems([]) + form.setValue("materialCodes", []) + }} + > + 전체 선종 + + {availableShipTypes.map(shipType => ( + { + setSelectedShipType(shipType) + setSelectedItems([]) + form.setValue("materialCodes", []) + }} + > + {shipType} + + ))} + +
- ) : ( + + + + {/* 마감일 설정 */} + ( + + 마감일 + + + + + + + + + date < new Date() || date < new Date("1900-01-01") + } + initialFocus + /> + + + + + )} + /> + + +
{/* 아이템 선택 영역 */}
조선 아이템 선택 - 공종별 아이템을 선택하세요 + {selectedShipType ? `선종 ${selectedShipType}의 공종별 아이템을 선택하세요` : "먼저 선종을 선택해주세요"}
@@ -408,6 +596,7 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { value={itemSearchQuery} onChange={(e) => setItemSearchQuery(e.target.value)} className="pl-8 pr-8" + disabled={!selectedShipType || isLoadingItems || dataLoadError !== null} /> {itemSearchQuery && ( )} - {isSearchingItems && ( - - )}
{/* 공종 필터 */} - @@ -457,10 +648,41 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) {
- {isLoadingItems ? ( + {dataLoadError ? ( +
+
+
+ +
+

데이터 로딩에 실패했습니다

+

{dataLoadError}

+
+ +
+
+
+ ) : isLoadingItems ? (
아이템을 불러오는 중... + {retryCount > 0 && ( +

재시도 {retryCount}회

+ )}
) : availableItems.length > 0 ? ( [...availableItems] @@ -547,146 +769,149 @@ export function CreateRfqDialog({ onCreated }: CreateRfqDialogProps) { /> {/* RFQ 그룹핑 미리보기 */} - {selectedItems.length > 0 && ( -
- 생성될 RFQ 그룹 미리보기 -
- {(() => { - // 아이템명(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) +
+ 생성될 RFQ 그룹 미리보기 +
+ {(() => { + // 아이템명(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) - 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 - } - }) + 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 ( -
-
- 총 {rfqGroups.length}개의 RFQ가 생성됩니다 (아이템명별로 그룹핑) -
- {rfqGroups.map((group, index) => ( -
-
-
- RFQ #{index + 1}: {group.actualItemName} -
- - {group.itemCodes.length}개 자재코드 ({group.codeLength}/255자) - -
-
- 자재코드: {group.joinedItemCodes} -
- {group.isOverLimit && ( -
- ⚠️ 자재코드 길이가 255자를 초과합니다. 일부 아이템을 제거해주세요. -
- )} -
- 포함된 아이템: {group.items.map(item => `${item.itemCode}`).join(', ')} -
-
- ))} + return ( +
+
+ 총 {rfqGroups.length}개의 RFQ가 생성됩니다 (아이템명별로 그룹핑)
- ) - })()} -
+ + + + + RFQ # + 아이템명 + 자재그룹코드 개수 + 길이 + 상태 + + + + {rfqGroups.map((group, index) => ( + + #{index + 1} + +
+ {group.actualItemName} +
+
+ {group.itemCodes.length}개 + + + {group.codeLength}/255자 + + + + {group.isOverLimit ? ( + 초과 + ) : ( + 정상 + )} + +
+ ))} +
+
+
+
+ ) + })()}
- )} +
- )} - - {/* 안내 메시지 */} - {/* {selectedProject && ( -
-

• 공종별 조선 아이템을 선택하세요.

-

같은 아이템명의 다른 자재코드들은 하나의 RFQ로 그룹핑됩니다.

-

• 그룹핑된 자재코드들은 comma로 구분되어 저장됩니다.

-

• 자재코드 길이는 최대 255자까지 가능합니다.

-

• 마감일은 벤더가 견적을 제출해야 하는 날짜입니다.

-
- )} */} - -
- -
+ + {/* Footer - Sticky 버튼 영역 */} +
+
+ + +
+
) -- cgit v1.2.3