diff --git a/src/components/DeviceSearch/CombinationDetailModal.tsx b/src/components/DeviceSearch/CombinationDetailModal.tsx index 4f86496..0dbfbe7 100644 --- a/src/components/DeviceSearch/CombinationDetailModal.tsx +++ b/src/components/DeviceSearch/CombinationDetailModal.tsx @@ -11,6 +11,7 @@ interface CombinationDetailModalProps { showAllDevices: boolean; onExpandChange: (expanded: boolean) => void; isAlreadyInCombination: boolean | 0 | null; + duplicateReason: 'model' | 'category' | null; isAddingDevice: boolean; onAddDevice: () => void; onBack: () => void; @@ -24,11 +25,28 @@ const CombinationDetailModal = ({ showAllDevices, onExpandChange, isAlreadyInCombination, + duplicateReason, isAddingDevice, onAddDevice, onBack, onClose, }: CombinationDetailModalProps) => { + // 중복 이유에 따라 다른 메시지 표시 + const getButtonText = () => { + if (!isAlreadyInCombination) { + return `${combination.comboName}에 담기`; + } + + if (duplicateReason === 'model') { + return '이미 담은 기기입니다.'; + } + + if (duplicateReason === 'category') { + return '이미 담은 타입입니다.'; + } + + return '이미 담은 타입입니다.'; + }; return (
void; } export const useAddToCombination = ({ selectedProductId, + selectedDeviceType, + selectedDeviceName, onCloseModal, }: UseAddToCombinationParams) => { const navigate = useNavigate(); @@ -56,12 +61,8 @@ export const useAddToCombination = ({ }; // 에러 핸들러 (공통) - const handleComboError = (error: any) => { - console.error('기기 추가 실패 상세 정보:', error.response?.data || error.message); - if (error.response?.data) { - console.log('Error Code:', error.response.data.errorCode || error.response.data.code); - console.log('Error Message:', error.response.data.message); - } + const handleComboError = (_error: any) => { + // 에러 처리 로직 필요시 추가 }; /* 내 조합에 담기 */ @@ -169,9 +170,61 @@ export const useAddToCombination = ({ const combinationDevices = comboDetail?.devices || []; /* 선택된 조합에 이미 담긴 기기인지 확인 */ - const isAlreadyInSelectedCombination = selectedCombinationId && selectedProductId - ? combinationDevices.some(device => device.deviceId === Number(selectedProductId)) - : false; + const duplicateCheck = (() => { + if (!selectedCombinationId || !selectedDeviceType || !selectedDeviceName) { + return { isBlocked: false, reason: null }; + } + + // deviceType 매핑 (영어 ↔ 한글) + const deviceTypeMap: Record = { + 'SMARTPHONE': ['SMARTPHONE', 'PHONE', '스마트폰', '폰'], + 'LAPTOP': ['LAPTOP', '노트북'], + 'TABLET': ['TABLET', '태블릿'], + 'CHARGER': ['CHARGER', '충전기'], + 'EARBUDS': ['EARBUDS', '이어버드'], + 'WATCH': ['WATCH', '워치', '시계'], + }; + + // 같은 카테고리인지 확인하는 함수 + const isSameDeviceType = (type1: string, type2: string): boolean => { + // 정확히 일치 + if (type1 === type2) return true; + + // 매핑 테이블에서 확인 + for (const types of Object.values(deviceTypeMap)) { + if (types.includes(type1) && types.includes(type2)) { + return true; + } + } + + return false; + }; + + const selectedBaseName = getBaseModelName(selectedDeviceName); + + // 우선순위: 같은 모델 > 같은 카테고리 + for (const device of combinationDevices) { + const deviceBaseName = getBaseModelName(device.name); + const isSameModel = deviceBaseName === selectedBaseName; + + if (isSameModel) { + return { isBlocked: true, reason: 'model' as const }; + } + } + + for (const device of combinationDevices) { + const isSameCategory = isSameDeviceType(device.deviceType, selectedDeviceType); + + if (isSameCategory) { + return { isBlocked: true, reason: 'category' as const }; + } + } + + return { isBlocked: false, reason: null }; + })(); + + const isAlreadyInSelectedCombination = duplicateCheck.isBlocked; + const duplicateReason = duplicateCheck.reason; return { modalView, @@ -185,6 +238,7 @@ export const useAddToCombination = ({ showAllDevices, setShowAllDevices, isAlreadyInSelectedCombination, + duplicateReason, isAddingDevice, addToCombinationConfig, isProfileLoading: isAuthLoading, diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index 970eb7d..76798bd 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -34,33 +34,38 @@ const DeviceSearchPage = () => { const productGridRef = useRef(null); const scroll = useScrollState(productGridRef); + /* 선택된 제품 찾기 */ + const selectedDevice = selectedProductId + ? search.allDevices.find(d => d.deviceId === Number(selectedProductId)) + : null; + const selectedProduct = selectedDevice ? mapSearchDeviceToProduct(selectedDevice) : null; + // 조합 담기 + 모달 상태 const combo = useAddToCombination({ selectedProductId, + selectedDeviceType: selectedDevice?.deviceType ?? null, + selectedDeviceName: selectedDevice?.name ?? null, onCloseModal: () => { searchParams.delete('productId'); setSearchParams(searchParams); }, }); - /* 선택된 제품 찾기 */ - const selectedDevice = selectedProductId - ? search.allDevices.find(d => d.deviceId === Number(selectedProductId)) - : null; - const selectedProduct = selectedDevice ? mapSearchDeviceToProduct(selectedDevice) : null; - /* 모달 열림 상태 확인 및 스크롤 잠금 (회색 배경이 보일 때와 동일한 조건) */ const isModalOpen = (!!selectedProduct && !combo.showSaveCompleteModal) || combo.showSaveCompleteModal; useEffect(() => { if (isModalOpen) { document.documentElement.style.overflow = 'hidden'; + document.body.style.overflow = 'hidden'; } else { document.documentElement.style.overflow = ''; + document.body.style.overflow = ''; } return () => { document.documentElement.style.overflow = ''; + document.body.style.overflow = ''; }; }, [isModalOpen]); @@ -251,6 +256,7 @@ const DeviceSearchPage = () => { showAllDevices={combo.showAllDevices} onExpandChange={combo.setShowAllDevices} isAlreadyInCombination={combo.isAlreadyInSelectedCombination} + duplicateReason={combo.duplicateReason} isAddingDevice={combo.isAddingDevice} onAddDevice={combo.handleAddDeviceToCombination} onBack={() => combo.setModalView('combination')} diff --git a/src/utils/devices/getBaseModelName.ts b/src/utils/devices/getBaseModelName.ts new file mode 100644 index 0000000..117ab2a --- /dev/null +++ b/src/utils/devices/getBaseModelName.ts @@ -0,0 +1,76 @@ +/** + * 기기명에서 용량과 색상 정보를 제거하여 베이스 모델명을 반환합니다. + * 같은 모델의 다른 용량/색상 기기를 동일한 모델로 인식하기 위해 사용됩니다. + * + * @param name - 전체 기기명 (예: "iPhone 15 Pro 블랙 512GB") + * @returns 베이스 모델명 (예: "iPhone 15 Pro") + * + * @example + * getBaseModelName("iPhone 15 Pro 블랙 512GB") // "iPhone 15 Pro" + * getBaseModelName("Samsung Galaxy S24 Ultra 화이트 256GB") // "Samsung Galaxy S24 Ultra" + * getBaseModelName("MacBook Pro 14 1TB") // "MacBook Pro 14" + */ +export const getBaseModelName = (name: string): string => { + if (!name) return ''; + + let baseName = name; + + // 1. 용량 정보 제거 (512GB, 1TB, 256MB 등) + baseName = baseName.replace(/\s*\d+\s*(GB|TB|MB)\s*/gi, ' '); + + // 2. 색상 정보 제거 + const colorPatterns = [ + // 한글 색상 + /\s*블랙\s*/gi, + /\s*화이트\s*/gi, + /\s*실버\s*/gi, + /\s*골드\s*/gi, + /\s*그레이\s*/gi, + /\s*그린\s*/gi, + /\s*블루\s*/gi, + /\s*레드\s*/gi, + /\s*핑크\s*/gi, + /\s*퍼플\s*/gi, + /\s*옐로우\s*/gi, + /\s*오렌지\s*/gi, + /\s*브라운\s*/gi, + /\s*네이비\s*/gi, + /\s*스페이스\s*그레이\s*/gi, + /\s*미드나이트\s*/gi, + /\s*스타라이트\s*/gi, + + // 영어 색상 + /\s*Black\s*/gi, + /\s*White\s*/gi, + /\s*Silver\s*/gi, + /\s*Gold\s*/gi, + /\s*Gray\s*/gi, + /\s*Grey\s*/gi, + /\s*Green\s*/gi, + /\s*Blue\s*/gi, + /\s*Red\s*/gi, + /\s*Pink\s*/gi, + /\s*Purple\s*/gi, + /\s*Yellow\s*/gi, + /\s*Orange\s*/gi, + /\s*Brown\s*/gi, + /\s*Navy\s*/gi, + /\s*Space\s*Gray\s*/gi, + /\s*Midnight\s*/gi, + /\s*Starlight\s*/gi, + /\s*Rose\s*Gold\s*/gi, + /\s*로즈\s*골드\s*/gi, + + // 괄호로 감싸진 색상 (예: "(블랙)", "(Black)") + /\s*\([^)]*\)\s*/g, + ]; + + colorPatterns.forEach(pattern => { + baseName = baseName.replace(pattern, ' '); + }); + + // 3. 양 끝 공백 제거 및 연속된 공백을 하나로 + baseName = baseName.trim().replace(/\s+/g, ' '); + + return baseName; +};