From 28f3360c8c2194c82d98653c76c29972ce35122a Mon Sep 17 00:00:00 2001 From: hun Date: Fri, 20 Feb 2026 12:26:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EA=B8=B0=EA=B8=B0=203=EA=B0=9C=20?= =?UTF-8?q?=EC=A0=84=EB=B6=80=20=EB=8B=B4=EC=9D=84=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingRecommendationPage.tsx | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/src/pages/onboarding/OnboardingRecommendationPage.tsx b/src/pages/onboarding/OnboardingRecommendationPage.tsx index e30ba8c..704efd6 100644 --- a/src/pages/onboarding/OnboardingRecommendationPage.tsx +++ b/src/pages/onboarding/OnboardingRecommendationPage.tsx @@ -39,11 +39,23 @@ const OnboardingRecommendationPage = () => { const { mutateAsync: addDevice } = usePostComboDevice(); const [isAdding, setIsAdding] = useState(false); - // 선택된 기기 ID (단일 선택) - const [selectedDeviceId, setSelectedDeviceId] = useState(null); - - const selectDevice = (deviceId: number) => { - setSelectedDeviceId(deviceId); + // 선택된 기기 Map (slot을 key로, deviceId를 value로 - 다른 타입 기기는 모두 선택 가능) + const [selectedDevices, setSelectedDevices] = useState>(new Map()); + + const selectDevice = (deviceId: number, slot: number) => { + setSelectedDevices((prev) => { + const newMap = new Map(prev); + + if (newMap.get(slot) === deviceId) { + // 이미 선택된 기기를 다시 클릭하면 선택 해제 + newMap.delete(slot); + } else { + // 새로운 기기 선택 (같은 slot의 기존 선택은 자동 대체) + newMap.set(slot, deviceId); + } + + return newMap; + }); }; // 유저명 @@ -52,8 +64,11 @@ const OnboardingRecommendationPage = () => { // 타이틀 표시 여부 const hasTitleData = lifestyleTagLabel && userName; - // API 응답을 RecentlyViewedDevice 형태로 변환 (3개만) - const recommendedDevices: RecentlyViewedDevice[] = (lifestyleData?.result?.devices ?? []) + // slot 정보를 포함한 확장 타입 + type RecommendedDevice = RecentlyViewedDevice & { slot: number }; + + // API 응답을 RecentlyViewedDevice 형태로 변환 (3개만, slot 정보 보존) + const recommendedDevices: RecommendedDevice[] = (lifestyleData?.result?.devices ?? []) .slice(0, 3) .map((device) => ({ deviceId: device.deviceId, @@ -66,16 +81,23 @@ const OnboardingRecommendationPage = () => { priceKrw: device.price, imageUrl: device.imageUrl, viewedAt: new Date().toISOString(), + slot: device.slot, })); - // 내 조합에 담기 핸들러 + // 내 조합에 담기 핸들러 (다중 기기 순차 추가) const handleAddToCombo = async () => { - if (!comboId || !selectedDeviceId || isAdding) return; + if (!comboId || selectedDevices.size === 0 || isAdding) return; setIsAdding(true); try { - await addDevice({ comboId, deviceId: selectedDeviceId }); - alert('선택한 기기가 내 조합에 담겼습니다!'); + const deviceIds = Array.from(selectedDevices.values()); + + // 선택된 모든 기기를 순차적으로 추가 + for (const deviceId of deviceIds) { + await addDevice({ comboId, deviceId }); + } + + alert(`선택한 ${deviceIds.length}개의 기기가 내 조합에 담겼습니다!`); navigate(ROUTES.home, { replace: true }); } catch (error) { const { message } = parseApiError(error); @@ -121,7 +143,7 @@ const OnboardingRecommendationPage = () => { ) : recommendedDevices.length > 0 ? ( recommendedDevices.map((device) => { - const isSelected = selectedDeviceId === device.deviceId; + const isSelected = selectedDevices.get(device.slot) === device.deviceId; return ( { 'rounded-8 transition-all', isSelected && 'border-shadow-blue' )} - onClick={() => selectDevice(device.deviceId)} + onClick={() => selectDevice(device.deviceId, device.slot)} /> ); }) @@ -142,8 +164,14 @@ const OnboardingRecommendationPage = () => {
{/* 내 조합에 담기 버튼 */} 0 + ? `내 조합에 담기 (${selectedDevices.size}개)` + : '내 조합에 담기' + } + disabled={!comboId || selectedDevices.size === 0 || isAdding} className="w-280 bg-blue-500 hover:bg-blue-400" onClick={handleAddToCombo} /> From 00d62b52b4c15c0433e3cbaa744e89eec67b6009 Mon Sep 17 00:00:00 2001 From: hun Date: Fri, 20 Feb 2026 12:33:25 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=AA=A8=EB=8B=AC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingRecommendationPage.tsx | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/pages/onboarding/OnboardingRecommendationPage.tsx b/src/pages/onboarding/OnboardingRecommendationPage.tsx index 704efd6..cd0353b 100644 --- a/src/pages/onboarding/OnboardingRecommendationPage.tsx +++ b/src/pages/onboarding/OnboardingRecommendationPage.tsx @@ -1,9 +1,10 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import clsx from 'clsx'; import PrimaryButton from '@/components/Button/PrimaryButton'; import RecentlyViewedCard from '@/components/RecentlyViewed/RecentlyViewedCard'; import LoadingSpinner from '@/components/LoadingSpinner'; +import SaveCompleteModal from '@/components/DeviceSearch/SaveCompleteModal'; import type { RecentlyViewedDevice } from '@/types/recentlyViewed/recentlyViewed'; import type { LifestyleTagKey } from '@/types/lifestyle/lifestyle'; import { useAuth } from '@/hooks/useAuth'; @@ -42,6 +43,29 @@ const OnboardingRecommendationPage = () => { // 선택된 기기 Map (slot을 key로, deviceId를 value로 - 다른 타입 기기는 모두 선택 가능) const [selectedDevices, setSelectedDevices] = useState>(new Map()); + // 저장 완료 모달 상태 + const [showSaveSuccessModal, setShowSaveSuccessModal] = useState(false); + const [isSaveFadingOut, setIsSaveFadingOut] = useState(false); + + // 저장 완료 팝업 자동 닫기 (0.8초 유지 후 0.2초 fade-out → 홈으로 이동) + useEffect(() => { + if (showSaveSuccessModal) { + const holdTimer = setTimeout(() => { + setIsSaveFadingOut(true); + + const closeTimer = setTimeout(() => { + setShowSaveSuccessModal(false); + setIsSaveFadingOut(false); + navigate(ROUTES.home, { replace: true }); + }, 200); + + return () => clearTimeout(closeTimer); + }, 800); + + return () => clearTimeout(holdTimer); + } + }, [showSaveSuccessModal, navigate]); + const selectDevice = (deviceId: number, slot: number) => { setSelectedDevices((prev) => { const newMap = new Map(prev); @@ -97,8 +121,7 @@ const OnboardingRecommendationPage = () => { await addDevice({ comboId, deviceId }); } - alert(`선택한 ${deviceIds.length}개의 기기가 내 조합에 담겼습니다!`); - navigate(ROUTES.home, { replace: true }); + setShowSaveSuccessModal(true); } catch (error) { const { message } = parseApiError(error); alert(message || '조합에 기기를 담는데 실패했습니다. 잠시 후 다시 시도해주세요.'); @@ -108,6 +131,7 @@ const OnboardingRecommendationPage = () => { }; return ( + <>
{/* 메인 컨테이너 */}
@@ -185,6 +209,11 @@ const OnboardingRecommendationPage = () => {
+ + {showSaveSuccessModal && ( + + )} + ); };