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
36 changes: 36 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@tanstack/react-virtual": "^3.13.12",
"@tosspayments/payment-widget-sdk": "^0.12.0",
"@tosspayments/tosspayments-sdk": "^2.4.0",
"@vercel/speed-insights": "^1.2.0",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Suspense } from 'react';
import { pretendard } from '@/lib/fonts';
import { SpeedInsights } from '@vercel/speed-insights/next';
import ClientLayout from '@/components/common/ClientLayout';
import './globals.css';

Expand Down Expand Up @@ -70,6 +71,7 @@ export default function RootLayout({
<Suspense>
<ClientLayout>{children}</ClientLayout>
</Suspense>
<SpeedInsights />
</body>
</html>
);
Expand Down
164 changes: 81 additions & 83 deletions src/store/likeStore.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,99 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { Activity } from '@/types/activities.type';

interface FavoritesState {
favorites: Activity[];
currentUserId: number | null;
favoritesByUser: Record<number, Activity[]>; // 모든 사용자의 찜 목록을 저장
currentUserId: number | null; // 현재 로그인한 사용자 ID (세션 상태)
favorites: Activity[]; // 현재 사용자의 찜 목록 (UI용)

initializeUser: (userId: number) => void;
isFavorite: (activityId: number) => boolean;
removeFavorite: (activityId: number) => void;
toggleFavorite: (activity: Activity) => void;
clearFavorites: () => void;
}
export const useFavoritesStore = create<FavoritesState>()(
persist(
(set, get) => ({
favoritesByUser: {},
currentUserId: null,
favorites: [],

// 사용자별 로컬 키 생성
const getUserStorageKey = (userId: number) => `favorites-${userId}`;
initializeUser: (userId) => {
const { currentUserId, favoritesByUser } = get();
// 이미 같은 사용자로 초기화된 경우 중복 실행 방지
if (currentUserId === userId) return;

// 사용자별 찜 목록 불러오기
const loadUserFavorites = (userId: number): Activity[] => {
try {
const key = getUserStorageKey(userId);
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : [];
} catch (error) {
console.error('찜 목록 불러오기 실패:', error);
return [];
}
};
// `favoritesByUser`에서 현재 사용자의 찜 목록을 가져와 UI용 상태에 설정
const userFavorites = favoritesByUser[userId] || [];
set({
currentUserId: userId,
favorites: userFavorites,
});
},

// 사용자별 찜 목록 저장
const saveUserFavorites = (userId: number, favorites: Activity[]) => {
try {
const key = getUserStorageKey(userId);
localStorage.setItem(key, JSON.stringify(favorites));
} catch (error) {
console.error('찜 목록 저장 실패:', error);
}
};
isFavorite: (activityId) => {
// 현재 사용자의 찜 목록에서 확인
return get().favorites.some((fav) => fav.id === activityId);
},

export const useFavoritesStore = create<FavoritesState>()((set, get) => ({
favorites: [],
currentUserId: null,
// 사용자 판별
initializeUser: (userId) => {
const currentUserId = get().currentUserId;
// 이미 같은 사용자면 리턴
if (currentUserId === userId) return;
// 새 사용자의 찜 목록 로드
const userFavorites = loadUserFavorites(userId);
set({
currentUserId: userId,
favorites: userFavorites,
});
},
// 찜되어 있는지 확인
isFavorite: (activityId) => {
const state = get();
return state.favorites.some((fav) => fav.id === activityId);
},
// 찜목록에 제거
removeFavorite: (activityId) => {
const { currentUserId } = get();
removeFavorite: (activityId) => {
const { currentUserId, favorites } = get();
if (!currentUserId) {
console.warn('사용자가 로그인되지 않았습니다.');
return;
}

if (!currentUserId) {
console.warn('사용자가 로그인되지 않았습니다.');
return;
}
const newFavorites = favorites.filter((fav) => fav.id !== activityId);

set((state) => {
const newFavorites = state.favorites.filter((fav) => fav.id !== activityId);
saveUserFavorites(currentUserId, newFavorites);
return { favorites: newFavorites };
});
},
// 찜목록 토글 기능
toggleFavorite: (activity) => {
const { currentUserId } = get();
set((state) => ({
favorites: newFavorites, // UI 상태 업데이트
favoritesByUser: {
...state.favoritesByUser,
[currentUserId]: newFavorites, // 영속성 상태 업데이트
},
}));
},

if (!currentUserId) {
console.warn('사용자가 로그인되지 않았습니다.');
return;
}
toggleFavorite: (activity) => {
const { currentUserId, favorites } = get();
if (!currentUserId) {
console.warn('사용자가 로그인되지 않았습니다.');
return;
}

let newFavorites;
set((state) => {
const isAlreadyFavorite = get().isFavorite(activity.id);
if (isAlreadyFavorite) {
newFavorites = state.favorites.filter((fav) => fav.id !== activity.id);
} else {
newFavorites = [...state.favorites, activity];
}
saveUserFavorites(currentUserId, newFavorites);
return { favorites: newFavorites };
});
},
clearFavorites: () => {
set({
favorites: [],
currentUserId: null,
});
},
}));
const isAlreadyFavorite = get().isFavorite(activity.id);
const newFavorites = isAlreadyFavorite
? favorites.filter((fav) => fav.id !== activity.id)
: [...favorites, activity];

set((state) => ({
favorites: newFavorites, // UI 상태 업데이트
favoritesByUser: {
...state.favoritesByUser,
[currentUserId]: newFavorites, // 영속성 상태 업데이트
},
}));
},

clearFavorites: () => {
// 로그아웃 시 현재 사용자 정보만 초기화
set({
currentUserId: null,
favorites: [],
});
},
}),
{
name: 'favorites-storage', // localStorage에 저장될 고유한 키
storage: createJSONStorage(() => localStorage),
// `favoritesByUser` 상태만 localStorage에 저장
partialize: (state) => ({ favoritesByUser: state.favoritesByUser }),
// 스토리지에서 데이터를 성공적으로 불러왔을 때 실행
onRehydrateStorage: () => () => {
console.log('찜 목록을 성공적으로 불러왔습니다.');
},
},
),
);
94 changes: 43 additions & 51 deletions src/store/recentlyWatched.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { Activity } from '@/types/activities.type';
import { persist, createJSONStorage } from 'zustand/middleware';

interface ViewedActivity extends Activity {
viewedAt: string; // ISO string
Expand All @@ -16,23 +17,6 @@ interface RecentViewedState {
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
const STORAGE_KEY = 'recent-viewed';

const loadRecent = (): ViewedActivity[] => {
try {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : [];
} catch {
return [];
}
};

const saveRecent = (items: ViewedActivity[]) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
} catch (e) {
console.error('최근 본 목록 저장 실패:', e);
}
};

const formatDateLabel = (date: Date) => {
const today = new Date();
const yesterday = new Date(Date.now() - 86400000);
Expand Down Expand Up @@ -64,38 +48,46 @@ const groupByLabel = (items: ViewedActivity[]) => {
return groups;
};

export const useRecentViewedStore = create<RecentViewedState>((set) => {
const initial = loadRecent();
return {
recentViewed: initial,
grouped: groupByLabel(initial),

addViewed: (activity) => {
const now = new Date();
const weekAgo = now.getTime() - WEEK_MS;

set((state) => {
const filtered = state.recentViewed.filter(
(a) => a.id !== activity.id && new Date(a.viewedAt).getTime() >= weekAgo,
);

const newItems = [{ ...activity, viewedAt: now.toISOString() }, ...filtered];

saveRecent(newItems);
return { recentViewed: newItems, grouped: groupByLabel(newItems) };
});
},

removeViewed: (activityId) =>
set((state) => {
const newItems = state.recentViewed.filter((a) => a.id !== activityId);
saveRecent(newItems);
return { recentViewed: newItems, grouped: groupByLabel(newItems) };
}),

clearViewed: () => {
saveRecent([]);
set({ recentViewed: [], grouped: {} });
export const useRecentViewedStore = create<RecentViewedState>()(
persist(
(set) => ({
recentViewed: [],
grouped: {},

addViewed: (activity) => {
const now = new Date();
const weekAgo = now.getTime() - WEEK_MS;

set((state) => {
const filtered = state.recentViewed.filter(
(a) => a.id !== activity.id && new Date(a.viewedAt).getTime() >= weekAgo,
);

const newItems = [{ ...activity, viewedAt: now.toISOString() }, ...filtered];

return { recentViewed: newItems, grouped: groupByLabel(newItems) };
});
},

removeViewed: (activityId) =>
set((state) => {
const newItems = state.recentViewed.filter((a) => a.id !== activityId);
return { recentViewed: newItems, grouped: groupByLabel(newItems) };
}),

clearViewed: () => {
set({ recentViewed: [], grouped: {} });
},
}),
{
name: STORAGE_KEY,
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ recentViewed: state.recentViewed }),
onRehydrateStorage: () => (state) => {
if (state) {
state.grouped = groupByLabel(state.recentViewed);
}
},
},
};
});
),
);