diff --git a/src/components/DeviceSearch/DeviceDetailModal.tsx b/src/components/DeviceSearch/DeviceDetailModal.tsx index b9153b4..2f1275c 100644 --- a/src/components/DeviceSearch/DeviceDetailModal.tsx +++ b/src/components/DeviceSearch/DeviceDetailModal.tsx @@ -8,7 +8,11 @@ import { getDeviceLifestyleTags } from '@/utils/tag/deviceLifestyleTags'; interface DeviceDetailModalProps { product: Product; device: SearchDevice; - addToCombinationConfig: { text: string; handler: () => void }; + addToCombinationConfig: { + text: string; + handler: () => void; + disabled?: boolean; + }; isProfileLoading: boolean; onClose: () => void; } @@ -136,8 +140,12 @@ const DeviceDetailModal = ({ diff --git a/src/components/MyPage/CombinationCard.tsx b/src/components/MyPage/CombinationCard.tsx index 8d7d6d0..5598fb7 100644 --- a/src/components/MyPage/CombinationCard.tsx +++ b/src/components/MyPage/CombinationCard.tsx @@ -166,7 +166,15 @@ const CombinationCard = ({ key={device.deviceId} className="bg-white rounded-card shadow-[0_0_4px_rgba(0,0,0,0.1)] p-12 w-244 flex items-center gap-12" > -
+
+ {device.imageUrl && ( + {device.name} + )} +

{device.name}

{device.brandName}

diff --git a/src/hooks/useAddToCombination.ts b/src/hooks/useAddToCombination.ts index 6e431ca..ef948ff 100644 --- a/src/hooks/useAddToCombination.ts +++ b/src/hooks/useAddToCombination.ts @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { type ModalView } from '@/types/devices'; +import { type ComboDevice } from '@/types/combo/combo'; import { ROUTES } from '@/constants/routes'; import { useGetCombos } from '@/apis/combo/getCombos'; import { useGetCombo } from '@/apis/combo/getComboId'; @@ -16,6 +17,17 @@ interface UseAddToCombinationParams { onCloseModal: () => void; } +interface AddToCombinationConfig { + text: string; + handler: () => void; + disabled?: boolean; +} + +interface DuplicateCheckResult { + isBlocked: boolean; + reason: 'model' | 'category' | null; +} + export const useAddToCombination = ({ selectedProductId, selectedDeviceType, @@ -37,6 +49,10 @@ export const useAddToCombination = ({ const { mutate: addDeviceToCombo, isPending: isAddingDevice } = usePostComboDevice(); const { mutate: recordRecentlyViewed } = usePostRecentlyViewed(); + // 조합이 1개일 때 해당 조합의 상세 정보 조회 + const singleComboId = combos.length === 1 ? combos[0].comboId : null; + const { data: singleComboDetail } = useGetCombo(singleComboId); + const [selectedCombinationId, setSelectedCombinationId] = useState(null); const [showAllDevices, setShowAllDevices] = useState(false); const [showSaveCompleteModal, setShowSaveCompleteModal] = useState(false); @@ -52,6 +68,57 @@ export const useAddToCombination = ({ // 선택된 조합의 상세 정보 조회 const { data: comboDetail } = useGetCombo(selectedCombinationId); + /* 중복 체크 유틸리티 */ + const DEVICE_TYPE_MAP: 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(DEVICE_TYPE_MAP)) { + if (types.includes(type1) && types.includes(type2)) { + return true; + } + } + + return false; + }; + + const checkDeviceDuplicate = ( + existingDevices: ComboDevice[], + targetDeviceType: string, + targetDeviceName: string + ): DuplicateCheckResult => { + if (!targetDeviceType || !targetDeviceName) { + return { isBlocked: false, reason: null }; + } + + const targetBaseName = getBaseModelName(targetDeviceName); + + // 우선순위 1: 같은 모델 + for (const device of existingDevices) { + const deviceBaseName = getBaseModelName(device.name); + if (deviceBaseName === targetBaseName) { + return { isBlocked: true, reason: 'model' }; + } + } + + // 우선순위 2: 같은 카테고리 + for (const device of existingDevices) { + if (isSameDeviceType(device.deviceType, targetDeviceType)) { + return { isBlocked: true, reason: 'category' }; + } + } + + return { isBlocked: false, reason: null }; + }; + /* 모달 닫기 */ const handleCloseModal = () => { onCloseModal(); @@ -65,6 +132,19 @@ export const useAddToCombination = ({ // 에러 처리 로직 필요시 추가 }; + /* 단일 조합일 때 중복 체크 */ + const singleComboDuplicateCheck = (() => { + if (combos.length !== 1 || !singleComboDetail) { + return { isBlocked: false, reason: null }; + } + + return checkDeviceDuplicate( + singleComboDetail.devices, + selectedDeviceType ?? '', + selectedDeviceName ?? '' + ); + })(); + /* 내 조합에 담기 */ const handleAddToCombination = () => { // 조합이 1개면 바로 저장 @@ -87,7 +167,7 @@ export const useAddToCombination = ({ }; /* 버튼 텍스트 및 핸들러 결정 */ - const getAddToCombinationConfig = () => { + const getAddToCombinationConfig = (): AddToCombinationConfig => { // Case 1: 로그아웃 상태 if (!isLoggedIn) { return { @@ -95,6 +175,7 @@ export const useAddToCombination = ({ handler: () => { navigate(ROUTES.auth.login); }, + disabled: false, }; } @@ -105,13 +186,34 @@ export const useAddToCombination = ({ handler: () => { navigate(ROUTES.onboarding.lifestyle); }, + disabled: false, }; } - // Case 3: 로그인 + 온보딩 완료 + // Case 3: 로그인 + 온보딩 완료 + 조합 1개 + if (combos.length === 1) { + const isBlocked = singleComboDuplicateCheck.isBlocked; + const reason = singleComboDuplicateCheck.reason; + + let text = '내 조합에 담기'; + if (isBlocked && reason === 'model') { + text = '이미 담은 기기입니다'; + } else if (isBlocked && reason === 'category') { + text = '이미 담은 타입입니다'; + } + + return { + text, + handler: handleAddToCombination, + disabled: isBlocked, + }; + } + + // Case 4: 로그인 + 온보딩 완료 + 조합 2개 이상 return { text: '내 조합에 담기', handler: handleAddToCombination, + disabled: false, }; }; @@ -175,52 +277,11 @@ export const useAddToCombination = ({ 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 }; + return checkDeviceDuplicate( + combinationDevices, + selectedDeviceType, + selectedDeviceName + ); })(); const isAlreadyInSelectedCombination = duplicateCheck.isBlocked;