diff --git a/src/apis/findCredential/postFindPassword.ts b/src/apis/findCredential/postFindPassword.ts index af161f87..12e6a135 100644 --- a/src/apis/findCredential/postFindPassword.ts +++ b/src/apis/findCredential/postFindPassword.ts @@ -1,4 +1,3 @@ -import { axiosInstance } from '@/apis/axios/axios'; import { cookieAxiosInstance } from '@/apis/axios/cookieAxios'; import type { SendMailRequest, @@ -14,7 +13,7 @@ import { useMutation } from '@tanstack/react-query'; export const postSendMail = async ( payload: SendMailRequest ): Promise => { - const { data } = await axiosInstance.post( + const { data } = await cookieAxiosInstance.post( '/api/find-credential/find-password/send-mail', payload ); diff --git a/src/assets/icons/accessories.svg b/src/assets/icons/accessories.svg deleted file mode 100644 index 42154299..00000000 --- a/src/assets/icons/accessories.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/assets/icons/chevron_right.svg b/src/assets/icons/chevron_right.svg deleted file mode 100644 index 782dd196..00000000 --- a/src/assets/icons/chevron_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icons/dropdown_up.svg b/src/assets/icons/dropdown_up.svg deleted file mode 100644 index 301d07fd..00000000 --- a/src/assets/icons/dropdown_up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Button/GoogleLoginButton.tsx b/src/components/Button/GoogleLoginButton.tsx index 1dd814de..fc6ada27 100644 --- a/src/components/Button/GoogleLoginButton.tsx +++ b/src/components/Button/GoogleLoginButton.tsx @@ -24,7 +24,7 @@ const GoogleLoginButton = ({ onClick, className }: GoogleLoginButtonProps) => { className={clsx( 'flex items-center gap-24', 'py-8 pl-0 pr-8', - 'bg-white cursor-pointer', + 'bg-white cursor-pointer hover:bg-gray-100', className )} style={{ boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.20)' }} diff --git a/src/components/Combination/CombinationDetailTag.tsx b/src/components/Combination/CombinationDetailTag.tsx deleted file mode 100644 index 7df9d187..00000000 --- a/src/components/Combination/CombinationDetailTag.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { COMBINATION_NAME_STYLE_MAP, type CombinationName } from '@/constants/combination'; - -type CombinationDetailTagProps = { - name: CombinationName; - info: string; - className?: string; -}; - -const CombinationDetailTag = ({ name, info, className = '' }: CombinationDetailTagProps) => { - const infoText = name === '라이프스타일' ? `#${info}` : info; - return ( -
- {infoText} -
- ); -}; - -export default CombinationDetailTag; diff --git a/src/components/Combination/CombinationTag.tsx b/src/components/Combination/CombinationTag.tsx index b1f4b0cd..1bf549c5 100644 --- a/src/components/Combination/CombinationTag.tsx +++ b/src/components/Combination/CombinationTag.tsx @@ -1,9 +1,4 @@ -import { - COMBINATION_NAME_STYLE_MAP, - COMBINATION_STATUS_STYLE_MAP, - type CombinationName, - type CombinationStatus, -} from '@/constants/combination'; +import { type CombinationName, type CombinationStatus } from '@/constants/combination'; type CombinationTagProps = { name: CombinationName; @@ -11,24 +6,34 @@ type CombinationTagProps = { className?: string; }; -const CombinationTag = ({ name, status, className = '' }: CombinationTagProps) => { - // status가 유효하지 않을 경우를 대비한 안전한 스타일 추출 - const statusStyle = COMBINATION_STATUS_STYLE_MAP[status] || COMBINATION_STATUS_STYLE_MAP['-']; - const displayStatus = status || '-'; +const NAME_STYLE_MAP: Record = { + 연동성: 'bg-blue-200 text-blue-700', + 편의성: 'bg-light-green text-dark-green', + 라이프스타일: 'bg-light-yellow text-dark-yellow', +}; +const STATUS_STYLE_MAP: Record = { + 최적: 'font-caption-sm text-optimal', + 양호: 'font-caption-sm text-good', + 보통: 'font-caption-sm text-normal', + 미흡: 'font-caption-sm text-poor', + '-': 'font-caption-sm text-optimal', +}; + +const CombinationTag = ({ name, status, className = '' }: CombinationTagProps) => { return ( {name}: - {displayStatus} + {status} ); }; diff --git a/src/components/MyPage/CombinationCard.tsx b/src/components/MyPage/CombinationCard.tsx index 59b7451a..8d7d6d0e 100644 --- a/src/components/MyPage/CombinationCard.tsx +++ b/src/components/MyPage/CombinationCard.tsx @@ -2,8 +2,11 @@ import { useState, memo } from 'react'; import StarIcon from '@/assets/icons/star.svg?react'; import StarXIcon from '@/assets/icons/starx.svg?react'; import StarHoverIcon from '@/assets/icons/starhover.svg?react'; +import CombinationTag from '@/components/Combination/CombinationTag'; +import { useComboEvaluation } from '@/apis/combo/getComboEvaluation'; import { formatDate } from '@/utils/format'; import type { ComboListItem } from '@/types/combo/combo'; +import type { CombinationStatus } from '@/constants/combination'; interface CombinationCardProps { combination: ComboListItem; @@ -30,6 +33,9 @@ const CombinationCard = ({ }: CombinationCardProps) => { const [hoveredStarComboId, setHoveredStarComboId] = useState(null); + // 조합 평가 캐시 구독 (staleTime: Infinity이므로 캐시에 있으면 API 호출 없이 바로 사용) + const { data: evaluation } = useComboEvaluation(combination.comboId); + // 그라데이션 로직 const gradientThreshold = columns === 4 ? 9 : 7; const shouldShowGradient = combination.devices.length >= gradientThreshold; @@ -90,44 +96,61 @@ const CombinationCard = ({ {nameError &&

{nameError}

} ) : ( - /* 일반 모드: 조합 번호 + 생성일 + 조합명 */ -
-
-

조합{index + 1}

-

- 생성일: {formatDate(combination.createdAt)} -

+ /* 일반 모드: 조합 번호 + 생성일 + 조합명 + 태그 */ +
+
+
+

조합{index + 1}

+

+ 생성일: {formatDate(combination.createdAt)} +

+
+
+

{combination.comboName}

+ {combination.isPinned ? ( + onTogglePin(e, combination.comboId)} + className={`!w-22 !h-22 -mt-3 cursor-pointer transition-opacity ${ + hoveredStarComboId === combination.comboId ? 'opacity-80' : '' + }`} + onMouseEnter={() => setHoveredStarComboId(combination.comboId)} + onMouseLeave={() => setHoveredStarComboId(null)} + /> + ) : ( + <> + {hoveredStarComboId === combination.comboId ? ( + onTogglePin(e, combination.comboId)} + className="!w-22 !h-22 -mt-3 cursor-pointer" + onMouseEnter={() => setHoveredStarComboId(combination.comboId)} + onMouseLeave={() => setHoveredStarComboId(null)} + /> + ) : ( + onTogglePin(e, combination.comboId)} + className="!w-22 !h-22 -mt-3 cursor-pointer" + onMouseEnter={() => setHoveredStarComboId(combination.comboId)} + onMouseLeave={() => setHoveredStarComboId(null)} + /> + )} + + )} +
-
-

{combination.comboName}

- {combination.isPinned ? ( - onTogglePin(e, combination.comboId)} - className={`!w-22 !h-22 -mt-3 cursor-pointer transition-opacity ${ - hoveredStarComboId === combination.comboId ? 'opacity-80' : '' - }`} - onMouseEnter={() => setHoveredStarComboId(combination.comboId)} - onMouseLeave={() => setHoveredStarComboId(null)} - /> - ) : ( - <> - {hoveredStarComboId === combination.comboId ? ( - onTogglePin(e, combination.comboId)} - className="!w-22 !h-22 -mt-3 cursor-pointer" - onMouseEnter={() => setHoveredStarComboId(combination.comboId)} - onMouseLeave={() => setHoveredStarComboId(null)} - /> - ) : ( - onTogglePin(e, combination.comboId)} - className="!w-22 !h-22 -mt-3 cursor-pointer" - onMouseEnter={() => setHoveredStarComboId(combination.comboId)} - onMouseLeave={() => setHoveredStarComboId(null)} - /> - )} - - )} + {/* 조합 평가 태그 - COMBO_EVALUATION 캐시에서 등급 읽기 */} +
+ + +
)} @@ -143,15 +166,7 @@ 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/components/ProductCard/ProductLife.tsx b/src/components/ProductCard/ProductLife.tsx deleted file mode 100644 index 45775024..00000000 --- a/src/components/ProductCard/ProductLife.tsx +++ /dev/null @@ -1,13 +0,0 @@ -type ProductLifeProps = { - label: string; -}; - -const ProductLife = ({ label }: ProductLifeProps) => { - return ( -
- #{label} -
- ); -}; - -export default ProductLife; diff --git a/src/constants/storageKeys.ts b/src/constants/storageKeys.ts deleted file mode 100644 index 76c4ce47..00000000 --- a/src/constants/storageKeys.ts +++ /dev/null @@ -1,3 +0,0 @@ -// localStorage 키 상수 -export const RECENTLY_VIEWED_DEVICES = 'RECENTLY_VIEWED_DEVICES'; -export const RECENTLY_VIEWED_MAX_COUNT = 10; diff --git a/src/hooks/useAddToCombination.ts b/src/hooks/useAddToCombination.ts index 995cb6d1..880b793c 100644 --- a/src/hooks/useAddToCombination.ts +++ b/src/hooks/useAddToCombination.ts @@ -5,9 +5,8 @@ import { ROUTES } from '@/constants/routes'; import { useGetCombos } from '@/apis/combo/getCombos'; import { useGetCombo } from '@/apis/combo/getComboId'; import { usePostComboDevice } from '@/apis/combo/postComboDevices'; -import { useGetUserProfile } from '@/apis/mypage/getUserProfile'; import { usePostRecentlyViewed } from '@/apis/recentlyViewed/postRecentlyViewed'; -import { hasAccessToken, hasCompletedOnboarding } from '@/utils/authStorage'; +import { useAuth } from '@/hooks/useAuth'; interface UseAddToCombinationParams { selectedProductId: string | null; @@ -20,14 +19,11 @@ export const useAddToCombination = ({ }: UseAddToCombinationParams) => { const navigate = useNavigate(); - // 로그인 상태 확인 - const isLoggedIn = hasAccessToken(); + // 인증 상태 확인 + const { isLoggedIn, hasCompletedOnboarding, isAuthLoading } = useAuth(); - // 사용자 프로필 조회 (로그인 시에만 자동 실행) - const { data: userProfile, isLoading: isProfileLoading } = useGetUserProfile(); - - // 온보딩 완료 여부 확인 (로딩 중에는 false로 기본 처리) - const hasOnboarding = isProfileLoading ? false : hasCompletedOnboarding(userProfile); + // 온보딩 완료 여부 (로딩 중에는 false로 기본 처리) + const hasOnboarding = isAuthLoading ? false : hasCompletedOnboarding; const [modalView, setModalView] = useState('device'); @@ -187,7 +183,7 @@ export const useAddToCombination = ({ isAlreadyInSelectedCombination, isAddingDevice, addToCombinationConfig, - isProfileLoading, + isProfileLoading: isAuthLoading, handleCloseModal, handleSelectCombination, handleAddDeviceToCombination, diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 0cde65aa..fc935992 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -6,7 +6,7 @@ import type { UserProfileResult } from '@/types/mypage/user'; export type UserProfile = UserProfileResult; /* -인증 상태를 관리하는 훅 +인증 상태를 관리하는 훅 (RootLayout에서 트리거한 user profile 쿼리 구독) 로그인 여부 판단 로직: - 토큰이 있고 userProfile이 있을 때만 → 로그인 상태 @@ -16,6 +16,7 @@ export type UserProfile = UserProfileResult; - isLoggedIn: 로그인 여부 (토큰 + user가 모두 있을 때만 true) - user: 유저 객체 (없으면 null) - isAuthLoading: 인증 로딩 상태 (토큰은 있는데 me를 아직 못 받아온 상태) + - hasCompletedOnboarding: 온보딩 완료 여부 (API의 isOnboardingCompleted 사용) */ export const useAuth = () => { @@ -32,11 +33,15 @@ export const useAuth = () => { // 토큰이 있고 userProfile이 있을 때만 로그인 상태 const isLoggedIn = hasToken && !!user; + // 온보딩 완료 여부 (API의 isOnboardingCompleted 사용) + const hasCompletedOnboarding = !!user?.isOnboardingCompleted; + return { isLoggedIn, user: user ?? null, isAuthLoading, hasToken, + hasCompletedOnboarding, refetchUserProfile: refetch, }; }; diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx index 52b95444..39c38c16 100644 --- a/src/layouts/RootLayout.tsx +++ b/src/layouts/RootLayout.tsx @@ -3,7 +3,7 @@ import { Outlet } from 'react-router-dom'; import { useGetUserProfile } from '@/apis/mypage/getUserProfile'; const RootLayout = () => { - // 토큰이 있을 때만 유저 정보 자동 조회 + // 레이아웃에서 유저 프로필 조회 트리거 (나머지 컴포넌트는 useAuth로 구독) useGetUserProfile(); return ( diff --git a/src/pages/auth/FindIdResultPage.tsx b/src/pages/auth/FindIdResultPage.tsx index 0506923d..aaec1180 100644 --- a/src/pages/auth/FindIdResultPage.tsx +++ b/src/pages/auth/FindIdResultPage.tsx @@ -55,7 +55,7 @@ const FindIdResultPage = () => { 비밀번호가 생각나지 않으신가요?