From 63eb2aa6993fc3836c42fe15f7b1979659965636 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 20:14:37 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B0=99=EC=9D=80=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=98=20=EA=B8=B0=EA=B8=B0=EC=9D=BC=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EB=8B=B4=EC=A7=80=20=EB=AA=BB=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useAddToCombination.ts | 24 +++++--- src/pages/devices/DeviceSearchPage.tsx | 16 ++++-- src/utils/devices/getBaseModelName.ts | 76 ++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 src/utils/devices/getBaseModelName.ts diff --git a/src/hooks/useAddToCombination.ts b/src/hooks/useAddToCombination.ts index a44c3f6..0886668 100644 --- a/src/hooks/useAddToCombination.ts +++ b/src/hooks/useAddToCombination.ts @@ -10,11 +10,13 @@ import { useAuth } from '@/hooks/useAuth'; interface UseAddToCombinationParams { selectedProductId: string | null; + selectedDeviceType?: string | null; onCloseModal: () => void; } export const useAddToCombination = ({ selectedProductId, + selectedDeviceType, onCloseModal, }: UseAddToCombinationParams) => { const navigate = useNavigate(); @@ -57,11 +59,7 @@ 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); - } + // 에러 처리 로직 필요시 추가 }; /* 내 조합에 담기 */ @@ -168,10 +166,18 @@ export const useAddToCombination = ({ /* 선택된 조합의 기기 리스트 (API에서 조회) */ const combinationDevices = comboDetail?.devices || []; - /* 선택된 조합에 이미 담긴 기기인지 확인 */ - const isAlreadyInSelectedCombination = selectedCombinationId && selectedProductId - ? combinationDevices.some(device => device.deviceId === Number(selectedProductId)) - : false; + /* 선택된 조합에 이미 같은 카테고리 기기가 있는지 확인 */ + const isAlreadyInSelectedCombination = (() => { + if (!selectedCombinationId || !selectedDeviceType) { + return false; + } + + const result = combinationDevices.some(device => { + return device.deviceType === selectedDeviceType; + }); + + return result; + })(); return { modalView, diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index 970eb7d..7576d19 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -34,33 +34,37 @@ 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, 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]); 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; +}; From ced635d6385d2ffc8df2709cbf65d14bfe16725c Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 20:32:26 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EA=B0=99=EC=9D=80=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=20=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EB=8B=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20disabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeviceSearch/CombinationDetailModal.tsx | 20 +++++- src/hooks/useAddToCombination.ts | 64 ++++++++++++++++--- src/pages/devices/DeviceSearchPage.tsx | 2 + 3 files changed, 77 insertions(+), 9 deletions(-) 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(); @@ -58,7 +61,7 @@ export const useAddToCombination = ({ }; // 에러 핸들러 (공통) - const handleComboError = (error: any) => { + const handleComboError = (_error: any) => { // 에러 처리 로직 필요시 추가 }; @@ -166,19 +169,63 @@ export const useAddToCombination = ({ /* 선택된 조합의 기기 리스트 (API에서 조회) */ const combinationDevices = comboDetail?.devices || []; - /* 선택된 조합에 이미 같은 카테고리 기기가 있는지 확인 */ - const isAlreadyInSelectedCombination = (() => { - if (!selectedCombinationId || !selectedDeviceType) { + /* 선택된 조합에 이미 담긴 기기인지 확인 */ + 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 }; + } } - const result = combinationDevices.some(device => { - return device.deviceType === selectedDeviceType; - }); + for (const device of combinationDevices) { + const isSameCategory = isSameDeviceType(device.deviceType, selectedDeviceType); - return result; + if (isSameCategory) { + return { isBlocked: true, reason: 'category' as const }; + } + } + + return { isBlocked: false, reason: null }; })(); + const isAlreadyInSelectedCombination = duplicateCheck.isBlocked; + const duplicateReason = duplicateCheck.reason; + return { modalView, setModalView, @@ -191,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 7576d19..76798bd 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -44,6 +44,7 @@ const DeviceSearchPage = () => { const combo = useAddToCombination({ selectedProductId, selectedDeviceType: selectedDevice?.deviceType ?? null, + selectedDeviceName: selectedDevice?.name ?? null, onCloseModal: () => { searchParams.delete('productId'); setSearchParams(searchParams); @@ -255,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')}