Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/components/DeviceSearch/DeviceDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -136,8 +140,12 @@ const DeviceDetailModal = ({
<PrimaryButton
text={addToCombinationConfig.text}
onClick={addToCombinationConfig.handler}
disabled={isProfileLoading}
className="w-full bg-blue-500 hover:bg-blue-400 transition-colors"
disabled={isProfileLoading || addToCombinationConfig.disabled}
className={`w-full ${
(isProfileLoading || addToCombinationConfig.disabled)
? ''
: 'bg-blue-500 hover:bg-blue-400'
} transition-colors`}
/>
</div>
</div>
Expand Down
10 changes: 9 additions & 1 deletion src/components/MyPage/CombinationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<div className="w-64 h-64 bg-gray-200 flex-shrink-0" />
<div className="w-64 h-64 bg-gray-200 flex-shrink-0 relative overflow-hidden">
{device.imageUrl && (
<img
src={device.imageUrl}
alt={device.name}
className="absolute inset-0 w-full h-full object-cover"
/>
)}
</div>
<div className="flex flex-col gap-4 flex-1">
<p className="font-body-3-sm text-black truncate w-120">{device.name}</p>
<p className="font-body-4-r text-gray-300">{device.brandName}</p>
Expand Down
157 changes: 109 additions & 48 deletions src/hooks/useAddToCombination.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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<number | null>(null);
const [showAllDevices, setShowAllDevices] = useState(false);
const [showSaveCompleteModal, setShowSaveCompleteModal] = useState(false);
Expand All @@ -52,6 +68,57 @@ export const useAddToCombination = ({
// 선택된 조합의 상세 정보 조회
const { data: comboDetail } = useGetCombo(selectedCombinationId);

/* 중복 체크 유틸리티 */
const DEVICE_TYPE_MAP: Record<string, string[]> = {
'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();
Expand All @@ -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개면 바로 저장
Expand All @@ -87,14 +167,15 @@ export const useAddToCombination = ({
};

/* 버튼 텍스트 및 핸들러 결정 */
const getAddToCombinationConfig = () => {
const getAddToCombinationConfig = (): AddToCombinationConfig => {
// Case 1: 로그아웃 상태
if (!isLoggedIn) {
return {
text: '로그인하고 내 조합에 담기',
handler: () => {
navigate(ROUTES.auth.login);
},
disabled: false,
};
}

Expand All @@ -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,
};
};

Expand Down Expand Up @@ -175,52 +277,11 @@ export const useAddToCombination = ({
return { isBlocked: false, reason: null };
}

// deviceType 매핑 (영어 ↔ 한글)
const deviceTypeMap: Record<string, string[]> = {
'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;
Expand Down