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;
+};