Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
3 changes: 1 addition & 2 deletions src/apis/findCredential/postFindPassword.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { axiosInstance } from '@/apis/axios/axios';
import { cookieAxiosInstance } from '@/apis/axios/cookieAxios';
import type {
SendMailRequest,
Expand All @@ -14,7 +13,7 @@ import { useMutation } from '@tanstack/react-query';
export const postSendMail = async (
payload: SendMailRequest
): Promise<SendMailResponse> => {
const { data } = await axiosInstance.post<SendMailResponse>(
const { data } = await cookieAxiosInstance.post<SendMailResponse>(
'/api/find-credential/find-password/send-mail',
payload
);
Expand Down
11 changes: 0 additions & 11 deletions src/assets/icons/accessories.svg

This file was deleted.

3 changes: 0 additions & 3 deletions src/assets/icons/chevron_right.svg

This file was deleted.

3 changes: 0 additions & 3 deletions src/assets/icons/dropdown_up.svg

This file was deleted.

1 change: 0 additions & 1 deletion src/assets/react.svg

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/Button/GoogleLoginButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)' }}
Expand Down
30 changes: 0 additions & 30 deletions src/components/Combination/CombinationDetailTag.tsx

This file was deleted.

31 changes: 18 additions & 13 deletions src/components/Combination/CombinationTag.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
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;
status: CombinationStatus;
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<CombinationName, string> = {
연동성: 'bg-blue-200 text-blue-700',
편의성: 'bg-light-green text-dark-green',
라이프스타일: 'bg-light-yellow text-dark-yellow',
};

const STATUS_STYLE_MAP: Record<CombinationStatus, string> = {
최적: '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 (
<span
className={`
inline-flex items-center gap-8
px-12 py-8 h-30
rounded-tag
font-caption-sm
${COMBINATION_NAME_STYLE_MAP[name]}
font-caption-r
${NAME_STYLE_MAP[name]}
${className}
`}
>
<span>{name}:</span>
<span className={statusStyle}>{displayStatus}</span>
<span className={STATUS_STYLE_MAP[status]}>{status}</span>
</span>
);
};
Expand Down
107 changes: 61 additions & 46 deletions src/components/MyPage/CombinationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +33,9 @@ const CombinationCard = ({
}: CombinationCardProps) => {
const [hoveredStarComboId, setHoveredStarComboId] = useState<number | null>(null);

// 조합 평가 캐시 구독 (staleTime: Infinity이므로 캐시에 있으면 API 호출 없이 바로 사용)
const { data: evaluation } = useComboEvaluation(combination.comboId);

// 그라데이션 로직
const gradientThreshold = columns === 4 ? 9 : 7;
const shouldShowGradient = combination.devices.length >= gradientThreshold;
Expand Down Expand Up @@ -90,44 +96,61 @@ const CombinationCard = ({
{nameError && <p className="pl-12 font-body-4-r text-warning">{nameError}</p>}
</div>
) : (
/* 일반 모드: 조합 번호 + 생성일 + 조합명 */
<div className="flex flex-col gap-8">
<div className="flex items-center gap-16">
<p className="font-body-3-r text-gray-400">조합{index + 1}</p>
<p className="font-body-3-r text-gray-400">
생성일: {formatDate(combination.createdAt)}
</p>
/* 일반 모드: 조합 번호 + 생성일 + 조합명 + 태그 */
<div className="flex flex-col gap-16">
<div className="flex flex-col gap-8">
<div className="flex items-center gap-16">
<p className="font-body-3-r text-gray-400">조합{index + 1}</p>
<p className="font-body-3-r text-gray-400">
생성일: {formatDate(combination.createdAt)}
</p>
</div>
<div className="flex items-center gap-8">
<p className="font-body-1-sm text-black">{combination.comboName}</p>
{combination.isPinned ? (
<StarIcon
onClick={(e) => 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 ? (
<StarHoverIcon
onClick={(e) => onTogglePin(e, combination.comboId)}
className="!w-22 !h-22 -mt-3 cursor-pointer"
onMouseEnter={() => setHoveredStarComboId(combination.comboId)}
onMouseLeave={() => setHoveredStarComboId(null)}
/>
) : (
<StarXIcon
onClick={(e) => onTogglePin(e, combination.comboId)}
className="!w-22 !h-22 -mt-3 cursor-pointer"
onMouseEnter={() => setHoveredStarComboId(combination.comboId)}
onMouseLeave={() => setHoveredStarComboId(null)}
/>
)}
</>
)}
</div>
</div>
<div className="flex items-center gap-8">
<p className="font-body-1-sm text-black">{combination.comboName}</p>
{combination.isPinned ? (
<StarIcon
onClick={(e) => 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 ? (
<StarHoverIcon
onClick={(e) => onTogglePin(e, combination.comboId)}
className="!w-22 !h-22 -mt-3 cursor-pointer"
onMouseEnter={() => setHoveredStarComboId(combination.comboId)}
onMouseLeave={() => setHoveredStarComboId(null)}
/>
) : (
<StarXIcon
onClick={(e) => onTogglePin(e, combination.comboId)}
className="!w-22 !h-22 -mt-3 cursor-pointer"
onMouseEnter={() => setHoveredStarComboId(combination.comboId)}
onMouseLeave={() => setHoveredStarComboId(null)}
/>
)}
</>
)}
{/* 조합 평가 태그 - COMBO_EVALUATION 캐시에서 등급 읽기 */}
<div className="flex gap-12">
<CombinationTag
name="연동성"
status={(evaluation?.connectivityGrade as CombinationStatus) || '-'}
/>
<CombinationTag
name="편의성"
status={(evaluation?.convenienceGrade as CombinationStatus) || '-'}
/>
<CombinationTag
name="라이프스타일"
status={(evaluation?.lifestyleGrade as CombinationStatus) || '-'}
/>
</div>
</div>
)}
Expand All @@ -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"
>
<div className="w-64 h-64 bg-gray-200 flex-shrink-0 overflow-hidden relative">
{device.imageUrl && (
<img
src={device.imageUrl}
alt={device.name}
className="absolute inset-0 w-full h-full object-cover"
/>
)}
</div>
<div className="w-64 h-64 bg-gray-200 flex-shrink-0" />
<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
13 changes: 0 additions & 13 deletions src/components/ProductCard/ProductLife.tsx

This file was deleted.

3 changes: 0 additions & 3 deletions src/constants/storageKeys.ts

This file was deleted.

16 changes: 6 additions & 10 deletions src/hooks/useAddToCombination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ModalView>('device');

Expand Down Expand Up @@ -187,7 +183,7 @@ export const useAddToCombination = ({
isAlreadyInSelectedCombination,
isAddingDevice,
addToCombinationConfig,
isProfileLoading,
isProfileLoading: isAuthLoading,
handleCloseModal,
handleSelectCombination,
handleAddDeviceToCombination,
Expand Down
7 changes: 6 additions & 1 deletion src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { UserProfileResult } from '@/types/mypage/user';
export type UserProfile = UserProfileResult;

/*
인증 상태를 관리하는 훅
인증 상태를 관리하는 훅 (RootLayout에서 트리거한 user profile 쿼리 구독)

로그인 여부 판단 로직:
- 토큰이 있고 userProfile이 있을 때만 → 로그인 상태
Expand All @@ -16,6 +16,7 @@ export type UserProfile = UserProfileResult;
- isLoggedIn: 로그인 여부 (토큰 + user가 모두 있을 때만 true)
- user: 유저 객체 (없으면 null)
- isAuthLoading: 인증 로딩 상태 (토큰은 있는데 me를 아직 못 받아온 상태)
- hasCompletedOnboarding: 온보딩 완료 여부 (API의 isOnboardingCompleted 사용)
*/

export const useAuth = () => {
Expand All @@ -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,
};
};
2 changes: 1 addition & 1 deletion src/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Outlet } from 'react-router-dom';
import { useGetUserProfile } from '@/apis/mypage/getUserProfile';

const RootLayout = () => {
// 토큰이 있을 때만 유저 정보 자동 조회
// 레이아웃에서 유저 프로필 조회 트리거 (나머지 컴포넌트는 useAuth로 구독)
useGetUserProfile();

return (
Expand Down
Loading