+ {/* 1. ์ฌ์ด๋๋ฐ (๋ฐ์ํ) */}
+
+
+ {/* ์ฌ์ด๋๋ฐ ์ธ๋ถ ์์ญ ํด๋ฆญ ์ ๋ซ๊ธฐ (๋ชจ๋ฐ์ผ) */}
+ {isSidebarOpen &&
setIsSidebarOpen(false)} />}
+
+
+ {/* 2. ํค๋ */}
+
+
+ {/* 3. ๋ฉ์ธ ์ฝํ
์ธ */}
+
+ {children}
+
+
+ {/* 4. ํ๋กํ
๋ฒํผ (+) */}
+
+
+
+ );
+};
+
+export default Layout;
\ No newline at end of file
diff --git a/week8/client/src/components/LpCard.tsx b/week8/client/src/components/LpCard.tsx
new file mode 100644
index 00000000..d26c7654
--- /dev/null
+++ b/week8/client/src/components/LpCard.tsx
@@ -0,0 +1,36 @@
+import { useNavigate } from 'react-router-dom';
+
+const LpCard = ({ lp }: { lp: any }) => {
+ const navigate = useNavigate();
+
+ return (
+
navigate(`/lps/${lp.id}`)}
+ className="group relative cursor-pointer rounded-[24px] border border-white/10 bg-white/5 p-3 backdrop-blur-xl transition-all duration-500 hover:bg-white/10 hover:border-white/30"
+ >
+ {/* ์นด๋ ์ด๋ฏธ์ง - ์ ๋ฆฌ ์ ์ฌ๋ฌผ ๋๋ */}
+
+

+
+ {/* ๋ง์ฐ์ค ํธ๋ฒ ์ ์๋จ์์ ๋ด๋ ค์ค๋ ๋ฐ์ง์ด๋ ๋น ํจ๊ณผ */}
+
+
+
+ {/* ์ ๋ณด ํ
์คํธ */}
+
+
{lp.title}
+
+
+ {new Date(lp.createdAt).toLocaleDateString()}
+
+ ๐ค {lp.likes?.length || 0}
+
+
+
+ {/* ์ ๋ฆฌ ์ง๊ฐ์ ํต์ฌ: ๋ฏธ์ธํ ์ธ๊ณฝ์ ๋น */}
+
+
+ );
+};
+
+export default LpCard;
\ No newline at end of file
diff --git a/week8/client/src/components/ProtectedRoute.tsx b/week8/client/src/components/ProtectedRoute.tsx
new file mode 100644
index 00000000..c86b3e83
--- /dev/null
+++ b/week8/client/src/components/ProtectedRoute.tsx
@@ -0,0 +1,13 @@
+import { Navigate, Outlet } from 'react-router-dom';
+
+const ProtectedRoute = () => {
+
+ const isLogin = !!localStorage.getItem('accessToken');
+ if (!isLogin) {
+ alert('๋ก๊ทธ์ธ์ด ํ์ํ ํ์ด์ง์
๋๋ค!');
+ return
;
+ }
+ return
;
+};
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/week4/client/src/hooks/useBallAnimation.ts b/week8/client/src/hooks/useBallAnimation.ts
similarity index 100%
rename from week4/client/src/hooks/useBallAnimation.ts
rename to week8/client/src/hooks/useBallAnimation.ts
diff --git a/week8/client/src/hooks/useDebounce.ts b/week8/client/src/hooks/useDebounce.ts
new file mode 100644
index 00000000..d4963ec6
--- /dev/null
+++ b/week8/client/src/hooks/useDebounce.ts
@@ -0,0 +1,30 @@
+import { useState, useEffect } from 'react';
+
+/**
+ * ๊ฐ์ ์ผ์ ์๊ฐ ์ง์ฐ์์ผ ๋ฐํํ๋ useDebounce ํ
+ * @param value ๋๋ฐ์ด์ฑํ ์ค์๊ฐ ์
๋ ฅ๊ฐ
+ * @param delay ์ง์ฐ ์๊ฐ (ms)
+ */
+
+
+export function useDebounce
(value: T, delay: number = 300): T {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ // 1. ํ์ด๋จธ ์ค์ ๋ก๊ทธ (์์ ๊ธฐ์ค)
+ console.log(`์
๋ ฅ: ${value} ... debounce ํ์ด๋จธ ์ค์ `);
+
+ const timer = setTimeout(() => {
+ setDebouncedValue(value);
+ console.log(`๐ ๋๋ฐ์ด์ค ์๋ฃ! ์ต์ข
๊ฐ ๋ณ๊ฒฝ ->`, value);
+ }, delay);
+
+ // 2. ์ธ๋ง์ดํธ ๋๋ ์์กด์ฑ(value, delay) ๋ณ๊ฒฝ ์ ๊ธฐ์กด ํ์ด๋จธ clear
+ return () => {
+ console.log(`โ ์ด์ debounce ํ์ด๋จธ ์ทจ์ (clearTimeout) -> ํ์ฌ๊ฐ: ${value}`);
+ clearTimeout(timer);
+ };
+ }, [value, delay]); // delay ๋ณ๊ฒฝ๋ ์ฆ์ ๋ฐ์๋๋๋ก ์์กด์ฑ ๋ฐฐ์ด์ ์ถ๊ฐ
+
+ return debouncedValue;
+}
\ No newline at end of file
diff --git a/week4/client/src/hooks/useForm.ts b/week8/client/src/hooks/useForm.ts
similarity index 100%
rename from week4/client/src/hooks/useForm.ts
rename to week8/client/src/hooks/useForm.ts
diff --git a/week8/client/src/index.css b/week8/client/src/index.css
new file mode 100644
index 00000000..bd6b0274
--- /dev/null
+++ b/week8/client/src/index.css
@@ -0,0 +1,43 @@
+@import "tailwindcss";
+
+
+@keyframes float {
+ 0% { transform: translateY(0px) rotate(0deg); }
+ 50% { transform: translateY(-20px) rotate(5deg); }
+ 100% { transform: translateY(0px) rotate(0deg); }
+}
+
+body {
+ margin: 0;
+ background: radial-gradient(circle at center, #101525 0%, #000000 100%);
+ min-height: 100vh;
+ overflow: hidden;
+ color: white;
+}
+
+.animate-float {
+ animation: float 6s ease-in-out infinite;
+}
+
+@layer base {
+ body {
+ @apply bg-black;
+ }
+}
+
+/* ์ ๋ฆฌ ์ปดํฌ๋ํธ ๊ดํ ํจ๊ณผ */
+.glass-panel {
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.8);
+}
+
+/* ์ปค์คํ
์คํฌ๋กค๋ฐ */
+.custom-scrollbar::-webkit-scrollbar {
+ width: 6px;
+}
+.custom-scrollbar::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+}
\ No newline at end of file
diff --git a/week4/client/src/main.tsx b/week8/client/src/main.tsx
similarity index 100%
rename from week4/client/src/main.tsx
rename to week8/client/src/main.tsx
diff --git a/week8/client/src/pages/GoogleCallback.tsx b/week8/client/src/pages/GoogleCallback.tsx
new file mode 100644
index 00000000..643f786c
--- /dev/null
+++ b/week8/client/src/pages/GoogleCallback.tsx
@@ -0,0 +1,31 @@
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+const GoogleCallback = () => {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+
+ const params = new URLSearchParams(window.location.search);
+ const accessToken = params.get('accessToken');
+ const refreshToken = params.get('refreshToken');
+
+ if (accessToken && refreshToken) {
+ localStorage.setItem('accessToken', accessToken);
+ localStorage.setItem('refreshToken', refreshToken);
+ alert("๊ตฌ๊ธ ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ์ต๋๋ค! ");
+ navigate('/mypage');
+ } else {
+ alert("๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค์ง ๋ชปํ์ต๋๋ค.");
+ navigate('/login');
+ }
+ }, [navigate]);
+
+ return (
+
+
๊ตฌ๊ธ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ์ค...
+
+ );
+};
+
+export default GoogleCallback;
\ No newline at end of file
diff --git a/week8/client/src/pages/LoginPage.tsx b/week8/client/src/pages/LoginPage.tsx
new file mode 100644
index 00000000..5c819b8a
--- /dev/null
+++ b/week8/client/src/pages/LoginPage.tsx
@@ -0,0 +1,89 @@
+import { useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useForm } from 'react-hook-form';
+import { useMutation } from '@tanstack/react-query';
+import api from '../apis/axios';
+import { type LoginFormValues } from '../utils/validate';
+import { useBallAnimation } from '../hooks/useBallAnimation';
+
+const LoginPage = () => {
+ const navigate = useNavigate();
+ const containerRef = useRef(null);
+ const balls = useBallAnimation(containerRef);
+
+ const { register, handleSubmit, formState: { errors } } = useForm();
+
+ // 7์ฃผ์ฐจ ํต์ฌ: ๋ก๊ทธ์ธ ๋ก์ง์ useMutation์ผ๋ก ์ ํ
+ const loginMutation = useMutation({
+ mutationFn: (data: LoginFormValues) => api.post('/auth/signin', data),
+ onSuccess: (response) => {
+ if (response.data.status) {
+ const { accessToken, refreshToken, name } = response.data.data;
+
+ // ํ ํฐ ๋ฐ ์ ๋ณด ์ ์ฅ
+ localStorage.setItem('accessToken', accessToken);
+ localStorage.setItem('refreshToken', refreshToken);
+ localStorage.setItem('nickname', name);
+
+ alert(`${name}๋ ํ์ํฉ๋๋ค!`);
+ navigate('/'); // ์ฑ๊ณต ์ ํ์ผ๋ก ๋ฆฌ๋ค์ด๋ ์
+ }
+ },
+ onError: (error: any) => {
+ alert(error.response?.data?.message || '๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค. ๋ค์ ํ์ธํด์ฃผ์ธ์! ใ
ใ
');
+ }
+ });
+
+ const handleGoogleLogin = () => {
+ window.location.href = 'http://localhost:8000/v1/auth/google/login';
+ };
+
+ return (
+
+ {balls.map((ball) => (
+
+ ))}
+
+
+
+
DORI
+
๋ก๊ทธ์ธ
+
+
+
+
+
+ {/* loginMutation.mutate๋ฅผ ํธ์ถํ๋๋ก ์์ */}
+
+
+
+ );
+};
+
+export default LoginPage;
\ No newline at end of file
diff --git a/week8/client/src/pages/LpDetailPage.tsx b/week8/client/src/pages/LpDetailPage.tsx
new file mode 100644
index 00000000..183e978c
--- /dev/null
+++ b/week8/client/src/pages/LpDetailPage.tsx
@@ -0,0 +1,193 @@
+import { useParams, useNavigate } from 'react-router-dom';
+import { useQuery, useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useInView } from 'react-intersection-observer';
+import { useState, useEffect } from 'react';
+import api from '../apis/axios';
+
+const LpDetailPage = () => {
+ const { lpid } = useParams();
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const [order, setOrder] = useState<'asc' | 'desc'>('desc');
+ const [commentInput, setCommentInput] = useState('');
+ const { ref, inView } = useInView();
+
+ const lpIdNum = Number(lpid);
+
+ // 1. ์์ธ ๋ฐ์ดํฐ ์กฐํ
+ const { data: lp, isLoading: isLpLoading } = useQuery({
+ queryKey: ['lp', lpIdNum],
+ queryFn: async () => {
+ const res = await api.get(`/lps/${lpIdNum}`);
+ return res.data.data;
+ }
+ });
+
+ // toggleLike Mutation ๋ถ๋ถ๋ง ์ด ๋ก์ง์ผ๋ก ์์ ํ ๊ต์ฒดํด๋ด!
+const toggleLike = useMutation({
+ mutationFn: async (isCurrentlyLiked: boolean) => {
+ return isCurrentlyLiked
+ ? api.delete(`/lps/${lpIdNum}/likes`)
+ : api.post(`/lps/${lpIdNum}/likes`);
+ },
+ onMutate: async (isCurrentlyLiked) => {
+ // 1. ์งํ ์ค์ธ ๋ชจ๋ ๋ฆฌํจ์น ๊ฐ์ ์ทจ์ (๋งค์ฐ ์ค์!)
+ await queryClient.cancelQueries({ queryKey: ['lp', lpIdNum] });
+
+ const previousLp = queryClient.getQueryData(['lp', lpIdNum]);
+
+ // 2. UI ์ฆ์ ์
๋ฐ์ดํธ (๋๊ด์ ์
๋ฐ์ดํธ)
+ queryClient.setQueryData(['lp', lpIdNum], (old: any) => {
+ if (!old) return old;
+ const currentLikes = old._count?.likes ?? 0;
+ return {
+ ...old,
+ isLiked: !isCurrentlyLiked,
+ _count: {
+ ...old._count,
+ likes: isCurrentlyLiked ? Math.max(0, currentLikes - 1) : currentLikes + 1
+ }
+ };
+ });
+
+ return { previousLp };
+ },
+ onError: (err: any, isCurrentlyLiked, context) => {
+ if (err.response?.status === 409) {
+
+ console.warn("โ ๏ธ 409 ์๋ฌ ๋ฐ์: ์๋ฒ์ ๋ฌด๊ดํ๊ฒ UI๋ฅผ ์ข์์ ์ํ๋ก ๊ณ ์ ํฉ๋๋ค.");
+ queryClient.setQueryData(['lp', lpIdNum], (old: any) => ({
+ ...old,
+ isLiked: true
+ }));
+ } else {
+ // ์ง์ง ํต์ ์๋ฌ์ผ ๋๋ง ๋กค๋ฐฑ
+ queryClient.setQueryData(['lp', lpIdNum], context?.previousLp);
+ alert('์ข์์ ์ฒ๋ฆฌ์ ์คํจํ์ด์!');
+ }
+ },
+ onSettled: (data, error) => {
+
+ const isConflict = (error as any)?.response?.status === 409;
+
+ if (!isConflict) {
+ // 409 ์๋ฌ๊ฐ ์๋ ๋๋ง 0.5์ด ๋ค์ ๋๊ธฐํ
+ setTimeout(() => {
+ queryClient.invalidateQueries({ queryKey: ['lp', lpIdNum] });
+ queryClient.invalidateQueries({ queryKey: ['myList'] });
+ }, 500);
+ } else {
+ console.log("409 ์๋ฌ์ด๋ฏ๋ก ์๋ฒ ๋๊ธฐํ๋ฅผ ๊ฑด๋๋ฐ๊ณ ๋ก์ปฌ UI๋ฅผ ์ ์งํฉ๋๋ค.");
+ }
+ }
+});
+
+ // 3. ๋๊ธ ๋ชฉ๋ก (๋ฌดํ์คํฌ๋กค)
+ const { data: commentData, fetchNextPage, hasNextPage } = useInfiniteQuery({
+ queryKey: ['lpComments', lpIdNum, order],
+ queryFn: async ({ pageParam = undefined }) => {
+ const res = await api.get(`/lps/${lpIdNum}/comments`, { params: { cursor: pageParam, limit: 10, order } });
+ return res.data.data;
+ },
+ initialPageParam: undefined,
+ getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.nextCursor : undefined,
+ });
+
+ const createComment = useMutation({
+ mutationFn: (content: string) => api.post(`/lps/${lpIdNum}/comments`, { content }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['lpComments', lpIdNum] });
+ setCommentInput('');
+ }
+ });
+
+ useEffect(() => { if (inView && hasNextPage) fetchNextPage(); }, [inView, hasNextPage]);
+
+ if (isLpLoading) return Vinyl Loading...
;
+
+ return (
+
+ {/* LP ์์ธ ์นด๋ */}
+
+
+ {lp?.title}
+
+
+
+
+

+
+
+
+ {lp?.tags?.map((tag: any) => (
+
+ #{tag.name}
+
+ ))}
+
+
+
+ "{lp?.content}"
+
+
+ {/* ๐ ์ข์์ ๋ฒํผ ์น์
*/}
+
+
+
+ {lp?._count?.likes || 0}
+
+
+
+
+ {/* ๋๊ธ ์น์
*/}
+
+
Comments
+
+ setCommentInput(e.target.value)}
+ placeholder="Write a comment..."
+ className="flex-1 bg-white/5 border border-white/10 rounded-3xl p-5 text-sm focus:border-pink-500/50 outline-none transition-all placeholder:text-white/20"
+ />
+
+
+
+
+ {commentData?.pages.map((page) =>
+ page.data.map((comment: any) => (
+
+
{comment.author.name}
+
{comment.content}
+
+ ))
+ )}
+
+
+
+
+ );
+};
+
+export default LpDetailPage;
\ No newline at end of file
diff --git a/week8/client/src/pages/LpListPage.tsx b/week8/client/src/pages/LpListPage.tsx
new file mode 100644
index 00000000..d3b8a88c
--- /dev/null
+++ b/week8/client/src/pages/LpListPage.tsx
@@ -0,0 +1,343 @@
+import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useInView } from 'react-intersection-observer';
+import { useEffect, useState, useRef } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import api from '../apis/axios';
+import LpCard from '../components/LpCard';
+import { useDebounce } from '../hooks/useDebounce';
+
+const LpListPage = () => {
+ const [sort, setSort] = useState<'asc' | 'desc'>('desc');
+ const { ref, inView } = useInView();
+ const queryClient = useQueryClient();
+ const fileInputRef = useRef(null);
+
+ // URL์ ์ฟผ๋ฆฌ ์คํธ๋ง ๊ฐ์ง (?searchMode=true ์ผ ๋๋ง ๊ฒ์์ฐฝ ํ์ฑํ)
+ const [searchParams] = useSearchParams();
+ const isSearchMode = searchParams.get('searchMode') === 'true';
+
+ // ๊ฒ์์ด ๋ฐ ๊ฒ์ ํ์
์ํ
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchType, setSearchType] = useState<'title' | 'tag'>('title');
+
+ // ๋๋ฐ์ด์ค ํ
์ฐ๋ (0.3์ด ์ง์ฐ)
+ const debouncedQuery = useDebounce(searchQuery, 300);
+
+ // ์ต๊ทผ ๊ฒ์์ด ์ํ ๊ด๋ฆฌ (๋ก์ปฌ์คํ ๋ฆฌ์ง ์ฐ๋)
+ const [recentSearches, setRecentSearches] = useState(() => {
+ const saved = localStorage.getItem('recentSearches');
+ return saved ? JSON.parse(saved) : [];
+ });
+
+ // ๋๋ฐ์ด์ค๋ ๊ฒ์์ด๊ฐ ์์ผ๋ฉด ์ต๊ทผ ๊ฒ์์ด ๊ธฐ๋ก ๋ฆฌ์คํธ์ ์ถ๊ฐ
+ useEffect(() => {
+ if (debouncedQuery.trim()) {
+ setRecentSearches((prev) => {
+ const filtered = prev.filter((item) => item !== debouncedQuery.trim());
+ const updated = [debouncedQuery.trim(), ...filtered].slice(0, 5); // ์ต๊ทผ 5๊ฐ ์ ์ง
+ localStorage.setItem('recentSearches', JSON.stringify(updated));
+ return updated;
+ });
+ }
+ }, [debouncedQuery]);
+
+ // ์ต๊ทผ ๊ฒ์์ด ๊ฐ๋ณ ์ญ์
+ const handleDeleteRecent = (textToDelete: string, e: React.MouseEvent) => {
+ e.stopPropagation(); // ๋ถ๋ชจ ํด๋ฆญ ์ด๋ฒคํธ ๋ฐฉ์ง
+ setRecentSearches((prev) => {
+ const updated = prev.filter((item) => item !== textToDelete);
+ localStorage.setItem('recentSearches', JSON.stringify(updated));
+ return updated;
+ });
+ };
+
+ // ์ต๊ทผ ๊ฒ์์ด ์ ์ฒด ์ญ์
+ const handleClearAllRecent = () => {
+ setRecentSearches([]);
+ localStorage.removeItem('recentSearches');
+ };
+
+ // ๊ฒ์ ๋ชจ๋๊ฐ ์๋ ๋๋ ์
๋ ฅ๊ฐ ์์ ํ ๋น์์ฃผ๊ธฐ
+ useEffect(() => {
+ if (!isSearchMode) {
+ setSearchQuery('');
+ }
+ }, [isSearchMode]);
+
+ // ๋ชจ๋ฌ ๋ฐ LP ์ถ๊ฐ ์
๋ ฅ ์ํ ๊ด๋ฆฌ (๊ธฐ์กด ์ ์ง)
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [tags, setTags] = useState([]);
+ const [tagInput, setTagInput] = useState('');
+ const [thumbnailUrl, setThumbnailUrl] = useState('');
+ const [previewUrl, setPreviewUrl] = useState('');
+
+ // 1. ์ด๋ฏธ์ง ์
๋ก๋ Mutation (๊ธฐ์กด ์ ์ง)
+ const uploadImageMutation = useMutation({
+ mutationFn: async (file: File) => {
+ const formData = new FormData();
+ formData.append('file', file);
+ const res = await api.post('/uploads', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ });
+ return res.data.data.imageUrl;
+ },
+ onSuccess: (url) => {
+ setThumbnailUrl(url);
+ },
+ onError: () => alert('์ด๋ฏธ์ง ์
๋ก๋์ ์คํจํ์ด์!')
+ });
+
+ // 2. LP ๊ฒ์๊ธ ์์ฑ Mutation (๊ธฐ์กด ์ ์ง)
+ const createLpMutation = useMutation({
+ mutationFn: (newLp: any) => api.post('/lps', newLp),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['lps'] });
+ setIsModalOpen(false);
+ resetForm();
+ },
+ onError: (error: any) => alert(error.response?.data?.message || 'LP ์์ฑ ์คํจ!')
+ });
+
+ // ํผ ์ด๊ธฐํ
+ const resetForm = () => {
+ setTitle('');
+ setContent('');
+ setTags([]);
+ setTagInput('');
+ setThumbnailUrl('');
+ setPreviewUrl('');
+ };
+
+ // ์ฌ์ง ์ ํ ํธ๋ค๋ฌ
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setPreviewUrl(URL.createObjectURL(file));
+ uploadImageMutation.mutate(file);
+ }
+ };
+
+ // ์ต์ข
๋ฑ๋ก ํธ๋ค๋ฌ
+ const handleAddLp = () => {
+ if (!title || !content) return alert('์ ๋ชฉ๊ณผ ๋ด์ฉ์ ์
๋ ฅํด์ฃผ์ธ์!');
+ if (!thumbnailUrl) return alert('์ด๋ฏธ์ง๊ฐ ์
๋ก๋ ์ค์
๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์!');
+
+ createLpMutation.mutate({
+ title,
+ content,
+ thumbnail: thumbnailUrl,
+ tags,
+ published: true
+ });
+ };
+
+ // 3. ๋ฌดํ ์คํฌ๋กค ๋ฐ์ดํฐ ํจ์นญ (๊ฒ์ ์ฟผ๋ฆฌ ๊ฐ๋ณ ์ฐ๋)
+ const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
+ queryKey: ['lps', sort, debouncedQuery, searchType, isSearchMode],
+ queryFn: async ({ pageParam = undefined }) => {
+ const params: any = {
+ order: sort,
+ cursor: pageParam,
+ limit: 10
+ };
+
+ // ๊ฒ์์ฐฝ์ด ์ด๋ ค ์๊ณ , ๋๋ฐ์ด์ค๋ ๊ฒ์์ด๊ฐ ์์ ๋๋ง ํ๋ผ๋ฏธํฐ ๋ถ๊ธฐ ์ฃผ์
+ if (isSearchMode && debouncedQuery.trim()) {
+ if (searchType === 'title') {
+ params.search = debouncedQuery;
+ } else {
+ params.tag = debouncedQuery;
+ }
+ }
+
+ const res = await api.get('/lps', { params });
+ return res.data.data;
+ },
+ initialPageParam: undefined,
+ getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.nextCursor : undefined,
+ });
+
+ useEffect(() => {
+ if (inView && hasNextPage && !isFetchingNextPage) fetchNextPage();
+ }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
+
+ return (
+
+
+ {/* โโโโโโโโโโโโโโโโโโ ์๋จ 1/3 ์์ญ: ์ฌ์ด๋๋ฐ '์ฐพ๊ธฐ' ํด๋ฆญ ์์๋ง ํ์ฑํ โโโโโโโโโโโโโโโโโโ */}
+ {isSearchMode && (
+
+ {/* ์ธํ ์์ญ */}
+
+
+ ๐
+ setSearchQuery(e.target.value)}
+ placeholder="๊ฒ์์ด๋ฅผ ์
๋ ฅํ์ธ์"
+ className="w-full bg-transparent outline-none text-white text-md placeholder:text-white/20"
+ />
+ {searchQuery && (
+
+ )}
+
+
+ {/* ์ ๋ ฌ ์ต์
์
๋ ํธ ๋ฐ์ค */}
+
+
+
+ {/* ์ต๊ทผ ๊ฒ์์ด ๊ธฐ๋กํ */}
+
+
+ ์ต๊ทผ ๊ฒ์์ด
+ {recentSearches.length > 0 && (
+
+ )}
+
+
+ {recentSearches.length === 0 ? (
+
์ต๊ทผ ๊ฒ์ ๋ด์ญ์ด ์์ต๋๋ค.
+ ) : (
+
+ {recentSearches.map((item, index) => (
+
setSearchQuery(item)}
+ className="flex items-center gap-2 bg-white/5 border border-white/10 hover:border-white/30 px-3 py-1 rounded-full text-xs cursor-pointer transition-colors"
+ >
+ {item}
+
+
+ ))}
+
+ )}
+
+
+
+ )}
+
+ {/* โโโโโโโโโโโโโโโโโโ ํ๋จ 2/3 ์์ญ: ์ ๋ ฌ ๋ฐ ๋ฆฌ์คํธ ๊ณตํต ๋ฉ์ธ ์์ญ โโโโโโโโโโโโโโโโโโ */}
+ {/* ์ต์ ์ / ์ค๋๋์ ์ ๋ ฌ ํํฐ */}
+
+ {['desc', 'asc'].map((order) => (
+
+ ))}
+
+
+ {/* LP ์นด๋ ๋ฐฐ์น ๊ทธ๋ฆฌ๋ */}
+
+ {isLoading && [...Array(10)].map((_, i) => (
+
+ ))}
+ {data?.pages.map((page) =>
+ page.data.map((lp: any) =>
)
+ )}
+
+
+ {/* ๋ฌดํ์คํฌ๋กค ๊ฐ์ง ์คํ์ด์กด */}
+
+
+ {/* LP ์ถ๊ฐ ๋ฑ๋ก ๋ชจ๋ฌ */}
+ {isModalOpen && (
+
setIsModalOpen(false)}>
+
e.stopPropagation()}>
+
New Vinyl
+
+
fileInputRef.current?.click()}
+ className="relative w-44 h-44 mx-auto cursor-pointer group mb-4"
+ >
+

+
+ UPLOAD PHOTO
+
+ {uploadImageMutation.isPending && (
+
+ )}
+
+
+
+
setTitle(e.target.value)}
+ placeholder="LP Name"
+ className="bg-white/5 border border-white/10 p-4 rounded-2xl outline-none focus:border-cyan-500 transition-colors text-sm"
+ />
+
+
+ )}
+
+
+
+ );
+};
+
+export default LpListPage;
\ No newline at end of file
diff --git a/week8/client/src/pages/MyPage.tsx b/week8/client/src/pages/MyPage.tsx
new file mode 100644
index 00000000..05129cd0
--- /dev/null
+++ b/week8/client/src/pages/MyPage.tsx
@@ -0,0 +1,132 @@
+import { useRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
+import api from '../apis/axios';
+import { useBallAnimation } from '../hooks/useBallAnimation';
+import LpCard from '../components/LpCard';
+import { useInView } from 'react-intersection-observer';
+
+const MyPage = () => {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const containerRef = useRef(null);
+ const balls = useBallAnimation(containerRef);
+ const { ref, inView } = useInView();
+
+ // ํญ ์ํ: 'likes' (์ข์์ ํ LP), 'my' (๋ด๊ฐ ์์ฑํ LP)
+ const [activeTab, setActiveTab] = useState<'likes' | 'my'>('likes');
+ const [sort, setSort] = useState<'asc' | 'desc'>('desc');
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [editName, setEditName] = useState('');
+
+ // 1. ๋ด ์ ๋ณด ์กฐํ
+ const { data: user } = useQuery({
+ queryKey: ['userMe'],
+ queryFn: async () => {
+ const res = await api.get('/users/me');
+ return res.data.data;
+ }
+ });
+
+ // 2. ํญ์ ๋ฐ๋ฅธ ๋ฆฌ์คํธ ๋ฌดํ ์คํฌ๋กค (๋ช
์ธ์ v1/lps/likes/me ๋๋ v1/lps/user ์ฌ์ฉ)
+ const { data: listData, fetchNextPage, hasNextPage } = useInfiniteQuery({
+ queryKey: ['myList', activeTab, sort],
+ queryFn: async ({ pageParam = undefined }) => {
+ const endpoint = activeTab === 'likes' ? '/lps/likes/me' : '/lps/user';
+ const res = await api.get(endpoint, { params: { order: sort, cursor: pageParam, limit: 10 } });
+ return res.data.data;
+ },
+ initialPageParam: undefined,
+ getNextPageParam: (lastPage) => lastPage.hasNext ? lastPage.nextCursor : undefined,
+ });
+
+ // 3. ๋๋ค์ ๋ณ๊ฒฝ ๋๊ด์ ์
๋ฐ์ดํธ
+ const updateNickname = useMutation({
+ mutationFn: (newName: string) => api.patch('/users/nickname', { nickname: newName }),
+ onMutate: async (newName) => {
+ await queryClient.cancelQueries({ queryKey: ['userMe'] });
+ const previousUser = queryClient.getQueryData(['userMe']);
+ queryClient.setQueryData(['userMe'], (old: any) => ({ ...old, name: newName }));
+ setIsEditMode(false);
+ return { previousUser };
+ },
+ onSettled: () => queryClient.invalidateQueries({ queryKey: ['userMe'] }),
+ });
+
+ // ์คํฌ๋กค ๊ฐ์ง
+ if (inView && hasNextPage) fetchNextPage();
+
+ return (
+
+ {/* ๋ฐฐ๊ฒฝ ์ ๋๋ฉ์ด์
*/}
+
+ {balls.map((ball) => (
+
+ ))}
+
+
+ {/* ์๋จ ํ๋กํ ์น์
*/}
+
+
+

+
+
+
+ {isEditMode ? (
+
+ setEditName(e.target.value)} className="bg-transparent text-2xl font-black outline-none text-center" autoFocus />
+
+
+ ) : (
+
+
{user?.name}
+
+
+ )}
+
ํ๋ก ํธ ์งฑ
+
{user?.email}
+
+
+
+ {/* ํญ ๋ฉ๋ด */}
+
+
+
+
+
+ {/* ๋ฆฌ์คํธ ์น์
*/}
+
+ {/* ์ ๋ ฌ ๋ฒํผ */}
+
+ {['asc', 'desc'].map(o => (
+
+ ))}
+
+
+ {/* ๊ทธ๋ฆฌ๋ ๋ฆฌ์คํธ */}
+
+ {listData?.pages.map(page =>
+ page.data.map((lp: any) => )
+ )}
+
+
+
+
+ );
+};
+
+export default MyPage;
\ No newline at end of file
diff --git a/week8/client/src/pages/SearchPage.tsx b/week8/client/src/pages/SearchPage.tsx
new file mode 100644
index 00000000..5f4b077f
--- /dev/null
+++ b/week8/client/src/pages/SearchPage.tsx
@@ -0,0 +1,141 @@
+import { useState, useEffect } from 'react';
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { useInView } from 'react-intersection-observer';
+import api from '../apis/axios';
+import LpCard from '../components/LpCard';
+import { useDebounce } from '../hooks/useDebounce';
+
+const SearchPage = () => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchType, setSearchType] = useState<'title' | 'tag'>('title');
+ const debouncedQuery = useDebounce(searchQuery, 300);
+
+ // ์ต๊ทผ ๊ฒ์์ด ์ํ (๋ก์ปฌ์คํ ๋ฆฌ์ง)
+ const [recentSearches, setRecentSearches] = useState(() => {
+ const saved = localStorage.getItem('recentSearches');
+ return saved ? JSON.parse(saved) : [];
+ });
+
+ const { ref, inView } = useInView();
+
+ // ๋๋ฐ์ด์ค๋ ๊ฒ์์ด ์ ์ฅ
+ useEffect(() => {
+ if (debouncedQuery.trim()) {
+ setRecentSearches((prev) => {
+ const filtered = prev.filter((item) => item !== debouncedQuery.trim());
+ const updated = [debouncedQuery.trim(), ...filtered].slice(0, 5);
+ localStorage.setItem('recentSearches', JSON.stringify(updated));
+ return updated;
+ });
+ }
+ }, [debouncedQuery]);
+
+ const handleDeleteRecent = (textToDelete: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ setRecentSearches((prev) => {
+ const updated = prev.filter((item) => item !== textToDelete);
+ localStorage.setItem('recentSearches', JSON.stringify(updated));
+ return updated;
+ });
+ };
+
+ const handleClearAllRecent = () => {
+ setRecentSearches([]);
+ localStorage.removeItem('recentSearches');
+ };
+
+ // React Query ๋ฐ์ดํฐ ํ์นญ
+ const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
+ queryKey: ['lps', debouncedQuery, searchType],
+ queryFn: async ({ pageParam = undefined }) => {
+ const params: any = { cursor: pageParam, limit: 10, order: 'desc' };
+ if (debouncedQuery.trim()) {
+ if (searchType === 'title') params.search = debouncedQuery;
+ else params.tag = debouncedQuery;
+ }
+ const res = await api.get('/lps', { params });
+ return res.data.data;
+ },
+ initialPageParam: undefined,
+ getNextPageParam: (lastPage) => (lastPage.hasNext ? lastPage.nextCursor : undefined),
+ });
+
+ useEffect(() => {
+ if (inView && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
+
+ return (
+
+
+
+
+ ๐
+ setSearchQuery(e.target.value)}
+ placeholder="๊ฒ์์ด๋ฅผ ์
๋ ฅํ์ธ์"
+ className="w-full bg-transparent outline-none text-white text-md placeholder:text-white/20"
+ />
+ {searchQuery && (
+
+ )}
+
+
+
+
+
+ {/* ์ต๊ทผ ๊ฒ์์ด */}
+
+
+ ์ต๊ทผ ๊ฒ์์ด
+ {recentSearches.length > 0 && (
+
+ )}
+
+ {recentSearches.length === 0 ? (
+
์ต๊ทผ ๊ฒ์ ๋ด์ญ์ด ์์ต๋๋ค.
+ ) : (
+
+ {recentSearches.map((item, index) => (
+
setSearchQuery(item)}
+ className="flex items-center gap-2 bg-white/5 border border-white/10 px-3 py-1 rounded-full text-xs cursor-pointer"
+ >
+ {item}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ {/* โโโโโโโโโโโโโโโโโโ ํ๋จ ๋ชฉ๋ก ๊ฒฐ๊ณผ ์์ญ โโโโโโโโโโโโโโโโโโ */}
+
+
+ {isLoading && [...Array(10)].map((_, i) =>
)}
+ {data?.pages[0]?.data.length === 0 && (
+
์ผ์นํ๋ LP๊ฐ ์์ต๋๋ค. ๐ฅฒ
+ )}
+ {data?.pages.map((page) => page.data.map((lp: any) =>
))}
+
+ {hasNextPage &&
}
+
+
+ );
+};
+
+export default SearchPage;
\ No newline at end of file
diff --git a/week4/client/src/pages/SignupPage.tsx b/week8/client/src/pages/SignupPage.tsx
similarity index 100%
rename from week4/client/src/pages/SignupPage.tsx
rename to week8/client/src/pages/SignupPage.tsx
diff --git a/week8/client/src/pages/UploadPage.tsx b/week8/client/src/pages/UploadPage.tsx
new file mode 100644
index 00000000..fc658aec
--- /dev/null
+++ b/week8/client/src/pages/UploadPage.tsx
@@ -0,0 +1,4 @@
+const UploadPage = () => {
+ return ์ฌ๊ธฐ๋ ์
๋ก๋ ํ์ด์ง์
๋๋ค! (์ค๋น ์ค )
;
+};
+export default UploadPage;
\ No newline at end of file
diff --git a/week4/client/src/utils/validate.ts b/week8/client/src/utils/validate.ts
similarity index 100%
rename from week4/client/src/utils/validate.ts
rename to week8/client/src/utils/validate.ts
diff --git a/week8/client/src/vite-env.d.ts b/week8/client/src/vite-env.d.ts
new file mode 100644
index 00000000..41237e8b
--- /dev/null
+++ b/week8/client/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+
+///
+///
+
+declare module '*.svg?react' {
+ import React = require('react');
+ export const ReactComponent: React.FC>;
+ const src: React.FC>;
+ export default src;
+}
\ No newline at end of file
diff --git a/week4/client/tailwind.config.js b/week8/client/tailwind.config.js
similarity index 100%
rename from week4/client/tailwind.config.js
rename to week8/client/tailwind.config.js
diff --git a/week4/client/tsconfig.app.json b/week8/client/tsconfig.app.json
similarity index 100%
rename from week4/client/tsconfig.app.json
rename to week8/client/tsconfig.app.json
diff --git a/week4/client/tsconfig.json b/week8/client/tsconfig.json
similarity index 100%
rename from week4/client/tsconfig.json
rename to week8/client/tsconfig.json
diff --git a/week4/client/tsconfig.node.json b/week8/client/tsconfig.node.json
similarity index 100%
rename from week4/client/tsconfig.node.json
rename to week8/client/tsconfig.node.json
diff --git a/week8/client/vite.config.ts b/week8/client/vite.config.ts
new file mode 100644
index 00000000..bb2324da
--- /dev/null
+++ b/week8/client/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import svgr from 'vite-plugin-svgr';
+
+export default defineConfig({
+ plugins: [react(), svgr()],
+});
\ No newline at end of file
diff --git a/week9/.gitignore b/week9/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/week9/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/week9/README.md b/week9/README.md
new file mode 100644
index 00000000..7dbf7ebf
--- /dev/null
+++ b/week9/README.md
@@ -0,0 +1,73 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/week9/eslint.config.js b/week9/eslint.config.js
new file mode 100644
index 00000000..ef614d25
--- /dev/null
+++ b/week9/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/week9/index.html b/week9/index.html
new file mode 100644
index 00000000..70f0165f
--- /dev/null
+++ b/week9/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ week9
+
+
+
+
+
+
diff --git a/week9/package.json b/week9/package.json
new file mode 100644
index 00000000..ea8bb3ac
--- /dev/null
+++ b/week9/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "week9",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@reduxjs/toolkit": "^2.12.0",
+ "@tailwindcss/vite": "^4.3.0",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "react-icons": "^5.6.0",
+ "react-redux": "^9.3.0",
+ "tailwindcss": "^4.3.0",
+ "zustand": "^5.0.14"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.12.3",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.59.2",
+ "vite": "^8.0.12"
+ }
+}
diff --git a/week9/pnpm-lock.yaml b/week9/pnpm-lock.yaml
new file mode 100644
index 00000000..6b0c916a
--- /dev/null
+++ b/week9/pnpm-lock.yaml
@@ -0,0 +1,2065 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@reduxjs/toolkit':
+ specifier: ^2.12.0
+ version: 2.12.0(react-redux@9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1))(react@19.2.6)
+ '@tailwindcss/vite':
+ specifier: ^4.3.0
+ version: 4.3.0(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))
+ react:
+ specifier: ^19.2.6
+ version: 19.2.6
+ react-dom:
+ specifier: ^19.2.6
+ version: 19.2.6(react@19.2.6)
+ react-icons:
+ specifier: ^5.6.0
+ version: 5.6.0(react@19.2.6)
+ react-redux:
+ specifier: ^9.3.0
+ version: 9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1)
+ tailwindcss:
+ specifier: ^4.3.0
+ version: 4.3.0
+ zustand:
+ specifier: ^5.0.14
+ version: 5.0.14(@types/react@19.2.15)(immer@11.1.8)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6))
+ devDependencies:
+ '@eslint/js':
+ specifier: ^10.0.1
+ version: 10.0.1(eslint@10.4.1(jiti@2.7.0))
+ '@types/node':
+ specifier: ^24.12.3
+ version: 24.12.4
+ '@types/react':
+ specifier: ^19.2.14
+ version: 19.2.15
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.15)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.1
+ version: 6.0.2(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))
+ eslint:
+ specifier: ^10.3.0
+ version: 10.4.1(jiti@2.7.0)
+ eslint-plugin-react-hooks:
+ specifier: ^7.1.1
+ version: 7.1.1(eslint@10.4.1(jiti@2.7.0))
+ eslint-plugin-react-refresh:
+ specifier: ^0.5.2
+ version: 0.5.2(eslint@10.4.1(jiti@2.7.0))
+ globals:
+ specifier: ^17.6.0
+ version: 17.6.0
+ typescript:
+ specifier: ~6.0.2
+ version: 6.0.3
+ typescript-eslint:
+ specifier: ^8.59.2
+ version: 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
+ vite:
+ specifier: ^8.0.12
+ version: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+
+packages:
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.7':
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.7':
+ resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.7':
+ resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ engines: {node: '>=6.9.0'}
+
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
+
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
+
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
+
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.23.5':
+ resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/config-helpers@0.6.0':
+ resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/core@1.2.1':
+ resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/js@10.0.1':
+ resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ peerDependencies:
+ eslint: ^10.0.0
+ peerDependenciesMeta:
+ eslint:
+ optional: true
+
+ '@eslint/object-schema@3.0.5':
+ resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/plugin-kit@0.7.2':
+ resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@humanfs/core@0.19.2':
+ resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.8':
+ resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/types@0.15.0':
+ resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/wasm-runtime@1.1.4':
+ resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.132.0':
+ resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==}
+
+ '@reduxjs/toolkit@2.12.0':
+ resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18 || ^19
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+
+ '@rolldown/binding-android-arm64@1.0.2':
+ resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.2':
+ resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.2':
+ resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.2':
+ resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.2':
+ resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.2':
+ resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.2':
+ resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.2':
+ resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.2':
+ resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.2':
+ resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.0.2':
+ resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.0.2':
+ resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.2':
+ resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.2':
+ resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.2':
+ resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
+ '@tailwindcss/node@4.3.0':
+ resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==}
+
+ '@tailwindcss/oxide-android-arm64@4.3.0':
+ resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.0':
+ resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.3.0':
+ resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.0':
+ resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
+ resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
+ resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.0':
+ resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.0':
+ resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.0':
+ resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.0':
+ resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
+ resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.0':
+ resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.3.0':
+ resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.3.0':
+ resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@tybys/wasm-util@0.10.2':
+ resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
+
+ '@types/esrecurse@4.3.1':
+ resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/node@24.12.4':
+ resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.15':
+ resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==}
+
+ '@types/use-sync-external-store@0.0.6':
+ resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+
+ '@typescript-eslint/eslint-plugin@8.60.0':
+ resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.60.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/parser@8.60.0':
+ resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/project-service@8.60.0':
+ resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/scope-manager@8.60.0':
+ resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.60.0':
+ resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/type-utils@8.60.0':
+ resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/types@8.60.0':
+ resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.60.0':
+ resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/utils@8.60.0':
+ resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/visitor-keys@8.60.0':
+ resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react@6.0.2':
+ resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
+ babel-plugin-react-compiler: ^1.0.0
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@rolldown/plugin-babel':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ baseline-browser-mapping@2.10.33:
+ resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ brace-expansion@5.0.6:
+ resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
+ engines: {node: 18 || 20 || >=22}
+
+ browserslist@4.28.2:
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ caniuse-lite@1.0.30001793:
+ resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ electron-to-chromium@1.5.364:
+ resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==}
+
+ enhanced-resolve@5.22.1:
+ resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==}
+ engines: {node: '>=10.13.0'}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@7.1.1:
+ resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0
+
+ eslint-plugin-react-refresh@0.5.2:
+ resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==}
+ peerDependencies:
+ eslint: ^9 || ^10
+
+ eslint-scope@9.1.2:
+ resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@10.4.1:
+ resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@11.2.0:
+ resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.4.2:
+ resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@17.6.0:
+ resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==}
+ engines: {node: '>=18'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ hermes-estree@0.25.1:
+ resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+ hermes-parser@0.25.1:
+ resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ immer@11.1.8:
+ resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ minimatch@10.2.5:
+ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
+ engines: {node: 18 || 20 || >=22}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.12:
+ resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ node-releases@2.0.46:
+ resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
+ engines: {node: '>=18'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.15:
+ resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ react-dom@19.2.6:
+ resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==}
+ peerDependencies:
+ react: ^19.2.6
+
+ react-icons@5.6.0:
+ resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==}
+ peerDependencies:
+ react: '*'
+
+ react-redux@9.3.0:
+ resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
+ peerDependencies:
+ '@types/react': ^18.2.25 || ^19
+ react: ^18.0 || ^19
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ redux:
+ optional: true
+
+ react@19.2.6:
+ resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
+ engines: {node: '>=0.10.0'}
+
+ redux-thunk@3.1.0:
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+
+ redux@5.0.1:
+ resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+
+ reselect@5.2.0:
+ resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
+
+ rolldown@1.0.2:
+ resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.8.1:
+ resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ tailwindcss@4.3.0:
+ resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==}
+
+ tapable@2.3.3:
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
+ engines: {node: '>=6'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.60.0:
+ resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ vite@8.0.14:
+ resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.18
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod-validation-error@4.0.2:
+ resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+
+ zustand@5.0.14:
+ resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
+snapshots:
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helpers': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.7':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.28.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.7':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/parser@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
+ '@babel/traverse@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.7':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@emnapi/core@1.10.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))':
+ dependencies:
+ eslint: 10.4.1(jiti@2.7.0)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.23.5':
+ dependencies:
+ '@eslint/object-schema': 3.0.5
+ debug: 4.4.3
+ minimatch: 10.2.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.6.0':
+ dependencies:
+ '@eslint/core': 1.2.1
+
+ '@eslint/core@1.2.1':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/js@10.0.1(eslint@10.4.1(jiti@2.7.0))':
+ optionalDependencies:
+ eslint: 10.4.1(jiti@2.7.0)
+
+ '@eslint/object-schema@3.0.5': {}
+
+ '@eslint/plugin-kit@0.7.2':
+ dependencies:
+ '@eslint/core': 1.2.1
+ levn: 0.4.1
+
+ '@humanfs/core@0.19.2':
+ dependencies:
+ '@humanfs/types': 0.15.0
+
+ '@humanfs/node@0.16.8':
+ dependencies:
+ '@humanfs/core': 0.19.2
+ '@humanfs/types': 0.15.0
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanfs/types@0.15.0': {}
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@tybys/wasm-util': 0.10.2
+ optional: true
+
+ '@oxc-project/types@0.132.0': {}
+
+ '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1))(react@19.2.6)':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@standard-schema/utils': 0.3.0
+ immer: 11.1.8
+ redux: 5.0.1
+ redux-thunk: 3.1.0(redux@5.0.1)
+ reselect: 5.2.0
+ optionalDependencies:
+ react: 19.2.6
+ react-redux: 9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1)
+
+ '@rolldown/binding-android-arm64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.2':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.2':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.2':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.2':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.2':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.2':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@standard-schema/utils@0.3.0': {}
+
+ '@tailwindcss/node@4.3.0':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.22.1
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.3.0
+
+ '@tailwindcss/oxide-android-arm64@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.0':
+ optional: true
+
+ '@tailwindcss/oxide@4.3.0':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.3.0
+ '@tailwindcss/oxide-darwin-arm64': 4.3.0
+ '@tailwindcss/oxide-darwin-x64': 4.3.0
+ '@tailwindcss/oxide-freebsd-x64': 4.3.0
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0
+ '@tailwindcss/oxide-linux-arm64-musl': 4.3.0
+ '@tailwindcss/oxide-linux-x64-gnu': 4.3.0
+ '@tailwindcss/oxide-linux-x64-musl': 4.3.0
+ '@tailwindcss/oxide-wasm32-wasi': 4.3.0
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
+ '@tailwindcss/oxide-win32-x64-msvc': 4.3.0
+
+ '@tailwindcss/vite@4.3.0(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))':
+ dependencies:
+ '@tailwindcss/node': 4.3.0
+ '@tailwindcss/oxide': 4.3.0
+ tailwindcss: 4.3.0
+ vite: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+
+ '@tybys/wasm-util@0.10.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/esrecurse@4.3.1': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/node@24.12.4':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/react-dom@19.2.3(@types/react@19.2.15)':
+ dependencies:
+ '@types/react': 19.2.15
+
+ '@types/react@19.2.15':
+ dependencies:
+ csstype: 3.2.3
+
+ '@types/use-sync-external-store@0.0.6': {}
+
+ '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.60.0
+ '@typescript-eslint/type-utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.60.0
+ eslint: 10.4.1(jiti@2.7.0)
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.60.0
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.60.0
+ debug: 4.4.3
+ eslint: 10.4.1(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.60.0(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.60.0
+ debug: 4.4.3
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.60.0':
+ dependencies:
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/visitor-keys': 8.60.0
+
+ '@typescript-eslint/tsconfig-utils@8.60.0(typescript@6.0.3)':
+ dependencies:
+ typescript: 6.0.3
+
+ '@typescript-eslint/type-utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
+ debug: 4.4.3
+ eslint: 10.4.1(jiti@2.7.0)
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.60.0': {}
+
+ '@typescript-eslint/typescript-estree@8.60.0(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/visitor-keys': 8.60.0
+ debug: 4.4.3
+ minimatch: 10.2.5
+ semver: 7.8.1
+ tinyglobby: 0.2.17
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0))
+ '@typescript-eslint/scope-manager': 8.60.0
+ '@typescript-eslint/types': 8.60.0
+ '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3)
+ eslint: 10.4.1(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.60.0':
+ dependencies:
+ '@typescript-eslint/types': 8.60.0
+ eslint-visitor-keys: 5.0.1
+
+ '@vitejs/plugin-react@6.0.2(vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ vite: 8.0.14(@types/node@24.12.4)(jiti@2.7.0)
+
+ acorn-jsx@5.3.2(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
+ acorn@8.16.0: {}
+
+ ajv@6.15.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ balanced-match@4.0.4: {}
+
+ baseline-browser-mapping@2.10.33: {}
+
+ brace-expansion@5.0.6:
+ dependencies:
+ balanced-match: 4.0.4
+
+ browserslist@4.28.2:
+ dependencies:
+ baseline-browser-mapping: 2.10.33
+ caniuse-lite: 1.0.30001793
+ electron-to-chromium: 1.5.364
+ node-releases: 2.0.46
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
+
+ caniuse-lite@1.0.30001793: {}
+
+ convert-source-map@2.0.0: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ detect-libc@2.1.2: {}
+
+ electron-to-chromium@1.5.364: {}
+
+ enhanced-resolve@5.22.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@7.1.1(eslint@10.4.1(jiti@2.7.0)):
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/parser': 7.29.7
+ eslint: 10.4.1(jiti@2.7.0)
+ hermes-parser: 0.25.1
+ zod: 4.4.3
+ zod-validation-error: 4.0.2(zod@4.4.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react-refresh@0.5.2(eslint@10.4.1(jiti@2.7.0)):
+ dependencies:
+ eslint: 10.4.1(jiti@2.7.0)
+
+ eslint-scope@9.1.2:
+ dependencies:
+ '@types/esrecurse': 4.3.1
+ '@types/estree': 1.0.9
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@10.4.1(jiti@2.7.0):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.23.5
+ '@eslint/config-helpers': 0.6.0
+ '@eslint/core': 1.2.1
+ '@eslint/plugin-kit': 0.7.2
+ '@humanfs/node': 0.16.8
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.9
+ ajv: 6.15.0
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 9.1.2
+ eslint-visitor-keys: 5.0.1
+ espree: 11.2.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ minimatch: 10.2.5
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.7.0
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@11.2.0:
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ eslint-visitor-keys: 5.0.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.4.2
+ keyv: 4.5.4
+
+ flatted@3.4.2: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ gensync@1.0.0-beta.2: {}
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@17.6.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ hermes-estree@0.25.1: {}
+
+ hermes-parser@0.25.1:
+ dependencies:
+ hermes-estree: 0.25.1
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ immer@11.1.8: {}
+
+ imurmurhash@0.1.4: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ isexe@2.0.0: {}
+
+ jiti@2.7.0: {}
+
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ minimatch@10.2.5:
+ dependencies:
+ brace-expansion: 5.0.6
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.12: {}
+
+ natural-compare@1.4.0: {}
+
+ node-releases@2.0.46: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.4: {}
+
+ postcss@8.5.15:
+ dependencies:
+ nanoid: 3.3.12
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ punycode@2.3.1: {}
+
+ react-dom@19.2.6(react@19.2.6):
+ dependencies:
+ react: 19.2.6
+ scheduler: 0.27.0
+
+ react-icons@5.6.0(react@19.2.6):
+ dependencies:
+ react: 19.2.6
+
+ react-redux@9.3.0(@types/react@19.2.15)(react@19.2.6)(redux@5.0.1):
+ dependencies:
+ '@types/use-sync-external-store': 0.0.6
+ react: 19.2.6
+ use-sync-external-store: 1.6.0(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ redux: 5.0.1
+
+ react@19.2.6: {}
+
+ redux-thunk@3.1.0(redux@5.0.1):
+ dependencies:
+ redux: 5.0.1
+
+ redux@5.0.1: {}
+
+ reselect@5.2.0: {}
+
+ rolldown@1.0.2:
+ dependencies:
+ '@oxc-project/types': 0.132.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.2
+ '@rolldown/binding-darwin-arm64': 1.0.2
+ '@rolldown/binding-darwin-x64': 1.0.2
+ '@rolldown/binding-freebsd-x64': 1.0.2
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.2
+ '@rolldown/binding-linux-arm64-gnu': 1.0.2
+ '@rolldown/binding-linux-arm64-musl': 1.0.2
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.2
+ '@rolldown/binding-linux-s390x-gnu': 1.0.2
+ '@rolldown/binding-linux-x64-gnu': 1.0.2
+ '@rolldown/binding-linux-x64-musl': 1.0.2
+ '@rolldown/binding-openharmony-arm64': 1.0.2
+ '@rolldown/binding-wasm32-wasi': 1.0.2
+ '@rolldown/binding-win32-arm64-msvc': 1.0.2
+ '@rolldown/binding-win32-x64-msvc': 1.0.2
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.8.1: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ tailwindcss@4.3.0: {}
+
+ tapable@2.3.3: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ ts-api-utils@2.5.0(typescript@6.0.3):
+ dependencies:
+ typescript: 6.0.3
+
+ tslib@2.8.1:
+ optional: true
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)
+ eslint: 10.4.1(jiti@2.7.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@6.0.3: {}
+
+ undici-types@7.16.0: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
+ dependencies:
+ browserslist: 4.28.2
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ use-sync-external-store@1.6.0(react@19.2.6):
+ dependencies:
+ react: 19.2.6
+
+ vite@8.0.14(@types/node@24.12.4)(jiti@2.7.0):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.15
+ rolldown: 1.0.2
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 24.12.4
+ fsevents: 2.3.3
+ jiti: 2.7.0
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ yallist@3.1.1: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod-validation-error@4.0.2(zod@4.4.3):
+ dependencies:
+ zod: 4.4.3
+
+ zod@4.4.3: {}
+
+ zustand@5.0.14(@types/react@19.2.15)(immer@11.1.8)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)):
+ optionalDependencies:
+ '@types/react': 19.2.15
+ immer: 11.1.8
+ react: 19.2.6
+ use-sync-external-store: 1.6.0(react@19.2.6)
diff --git a/week9/public/favicon.svg b/week9/public/favicon.svg
new file mode 100644
index 00000000..6893eb13
--- /dev/null
+++ b/week9/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/week9/public/icons.svg b/week9/public/icons.svg
new file mode 100644
index 00000000..e9522193
--- /dev/null
+++ b/week9/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/week9/src/App.tsx b/week9/src/App.tsx
new file mode 100644
index 00000000..161e6d87
--- /dev/null
+++ b/week9/src/App.tsx
@@ -0,0 +1,21 @@
+import { useEffect } from 'react';
+import { useCartStore } from './store/useCartStore';
+import Navbar from './components/Navbar';
+import CartList from './components/CartList';
+import Modal from './components/Modal';
+
+export default function App() {
+ const { cartItems, calculateTotals, isModalOpen } = useCartStore();
+
+ useEffect(() => {
+ calculateTotals();
+ }, [cartItems]);
+
+ return (
+
+
+
+ {isModalOpen && }
+
+ );
+}
\ No newline at end of file
diff --git a/week9/src/components/CartItemCard.tsx b/week9/src/components/CartItemCard.tsx
new file mode 100644
index 00000000..8668e482
--- /dev/null
+++ b/week9/src/components/CartItemCard.tsx
@@ -0,0 +1,41 @@
+import { type CartItemType } from '../constants/cartItems';
+import { useCartStore } from '../store/useCartStore';
+
+interface CartItemProps {
+ item: CartItemType;
+}
+
+export default function CartItemCard({ item }: CartItemProps) {
+ const { increase, decrease } = useCartStore();
+
+ return (
+
+
+

+
+ {item.title}
+ {item.singer}
+ ${item.price}
+
+
+
+
+
+
+ {item.amount}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/week9/src/components/CartList.tsx b/week9/src/components/CartList.tsx
new file mode 100644
index 00000000..72eeffa6
--- /dev/null
+++ b/week9/src/components/CartList.tsx
@@ -0,0 +1,39 @@
+import { useCartStore } from '../store/useCartStore';
+import CartItemCard from './CartItemCard';
+import { type CartItemType } from '../constants/cartItems';
+
+export default function CartList() {
+ const { cartItems, total, openModal } = useCartStore();
+
+ if (cartItems.length === 0) {
+ return (
+
+
์ฅ๋ฐ๊ตฌ๋๊ฐ ํ
๋น์์ต๋๋ค.
+
+ );
+ }
+
+ return (
+
+
+ {cartItems.map((item: CartItemType) => (
+
+ ))}
+
+
+
+
+ ์ด ๊ฐ๊ฒฉ
+ ${total.toLocaleString()}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/week9/src/components/Modal.tsx b/week9/src/components/Modal.tsx
new file mode 100644
index 00000000..3518cbc5
--- /dev/null
+++ b/week9/src/components/Modal.tsx
@@ -0,0 +1,32 @@
+import { useCartStore } from '../store/useCartStore';
+
+export default function Modal() {
+ const { closeModal, clearCart } = useCartStore();
+
+ const handleConfirm = () => {
+ clearCart();
+ closeModal();
+ };
+
+ return (
+
+
+
์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น?
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/week9/src/components/Navbar.tsx b/week9/src/components/Navbar.tsx
new file mode 100644
index 00000000..bafc6df9
--- /dev/null
+++ b/week9/src/components/Navbar.tsx
@@ -0,0 +1,18 @@
+import { FaShoppingCart } from 'react-icons/fa';
+import { useCartStore } from '../store/useCartStore';
+
+export default function Navbar() {
+ const amount = useCartStore((state) => state.amount);
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/week9/src/constants/cartItems.ts b/week9/src/constants/cartItems.ts
new file mode 100644
index 00000000..77cbe550
--- /dev/null
+++ b/week9/src/constants/cartItems.ts
@@ -0,0 +1,109 @@
+export interface CartItemType {
+ id: string;
+ title: string;
+ singer: string;
+ price: string;
+ img: string;
+ amount: number;
+}
+
+const cartItems: CartItemType[] = [
+ {
+ id: 'recB6qcHPxb62YJ75',
+ title: 'Vancouver',
+ singer: 'BIG Naughty (์๋ํ)',
+ price: '25000',
+ img: 'https://image.bugsm.co.kr/album/images/500/40752/4075248.jpg',
+ amount: 1,
+ },
+ {
+ id: 'recdRxBsE14Rr2VuJ',
+ title: 'Empty Island',
+ singer: 'greenblue',
+ price: '18000',
+ img: 'https://f4.bcbits.com/img/a1472100223_10.jpg',
+ amount: 1,
+ },
+ {
+ id: 'recwTo120XST3PIoW',
+ title: 'golden hour',
+ singer: 'JVKE',
+ price: '28000',
+ img: 'https://image.bugsm.co.kr/album/images/200/193874/19387484.jpg?version=20230503022513.0',
+ amount: 1,
+ },
+ {
+ id: 'rec1JZlfCIBOPdcT2',
+ title: 'Home Sweet Home(From "์ด์ฉ๋ฉด ์ฐ๋ฆฐ ํค์ด์ก๋์ง ๋ชจ๋ฅธ๋ค")',
+ singer: 'Gogang (๊ณ ๊ฐฑ)',
+ price: '20000',
+ img: 'https://is1-ssl.mzstatic.com/image/thumb/Music116/v4/8d/d7/0f/8dd70fba-0a8f-b7ce-a2d2-f0d32dad2837/8809912894132.jpg/1200x1200bf-60.jpg',
+ amount: 1,
+ },
+ {
+ id: 'recwTo160XST3PIoW',
+ title: 'Lemon',
+ singer: 'Kenshi Yonezu(์ผ์ ์๋ค์ฆ/็ฑณๆดฅ ็ๅธซ)',
+ price: '30000',
+ img: 'https://image.bugsm.co.kr/album/images/200/7222/722272.jpg?version=20220514022202.0',
+ amount: 1,
+ },
+ {
+ id: 'recaBo120XST3PIoW',
+ title: '๋๋ฉฉ์ด',
+ singer: 'MASYTA (๋ง์๋ฐ)',
+ price: '12000',
+ img: 'https://image.bugsm.co.kr/album/images/200/3271/327113.jpg?version=20230606014806.0',
+ amount: 1,
+ },
+ {
+ id: 'recqBo123XST3PIoK',
+ title: 'LโAmour, Les Baguettes, Paris',
+ singer: '์คํ
๋ผ ์ฅ(Stella Jang)',
+ price: '32000',
+ img: 'https://image.bugsm.co.kr/album/images/200/40660/4066056.jpg?version=20211020003912.0',
+ amount: 1,
+ },
+ {
+ id: 'recqBo133XST3PIoK',
+ title: 'NO PAIN',
+ singer: '์ค๋ฆฌ์นด๊ฒ',
+ price: '22000',
+ img: 'https://image.bugsm.co.kr/album/images/200/40790/4079061.jpg?version=20220826063340.0',
+ amount: 1,
+ },
+ {
+ id: 'recqBo145XST3PIoK',
+ title: '๋์๊ฒ (feat. HYUN SEO)',
+ singer: 'Halsoon',
+ price: '20000',
+ img: 'https://image.bugsm.co.kr/album/images/200/204634/20463445.jpg?version=20230110013144.0',
+ amount: 1,
+ },
+ {
+ id: 'recqBo129XST3PIoK',
+ title: '๋ ๋ ์ฌ๋ฆฌ๋ ์ค์ด์ผ(Think About You)',
+ singer: 'PATEKO (ํํ
์ฝ) , Jayci yucca(์ ์ด์จ ์ ์นด)',
+ price: '25000',
+ img: 'https://image.bugsm.co.kr/album/images/200/40581/4058181.jpg?version=20210726063528.0',
+ amount: 1,
+ },
+ {
+ id: 'rdaqBo129XST3PIoK',
+ title: '๋๋์ง ์์ ์๊ธฐ(feat. ๋ค์ด๋๋ฏน ๋์ค)',
+ singer: '๋ฆด๋ฌ๋ง์ฆ & TOIL',
+ price: '23000',
+ img: 'https://image.bugsm.co.kr/album/images/200/204692/20469237.jpg?version=20220827004220.0',
+ amount: 1,
+ },
+ {
+ id: 'rdaqBo149XQT3PIoK',
+ title: '๊ฐ์์ ๋ฐค',
+ singer: '๋์ํ์จ ๋ฐด๋',
+ price: '21000',
+ img: 'https://image.bugsm.co.kr/album/images/200/202235/20223594.jpg?version=20230904194021.0',
+ amount: 1,
+ },
+];
+
+export default cartItems;
\ No newline at end of file
diff --git a/week9/src/features/cart/cartSlice.ts b/week9/src/features/cart/cartSlice.ts
new file mode 100644
index 00000000..b0c7a96c
--- /dev/null
+++ b/week9/src/features/cart/cartSlice.ts
@@ -0,0 +1,65 @@
+import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
+import cartItems from '../../constants/cartItems';
+import { type CartItemType } from '../../constants/cartItems';
+
+interface CartState {
+ cartItems: CartItemType[];
+ amount: number;
+ total: number;
+}
+
+const initialState: CartState = {
+ cartItems: cartItems,
+ amount: 0,
+ total: 0,
+};
+
+const cartSlice = createSlice({
+ name: 'cart',
+ initialState,
+ reducers: {
+ // ํน์ ์๋ฐ ์๋ +1
+ increase: (state: CartState, action: PayloadAction<{ id: string }>) => {
+ const item = state.cartItems.find((item) => item.id === action.payload.id);
+ if (item) {
+ item.amount += 1;
+ }
+ },
+ // ํน์ ์๋ฐ ์๋ -1 (1๋ฏธ๋ง์ด ๋๋ฉด ์๋ ์ ๊ฑฐ)
+ decrease: (state: CartState, action: PayloadAction<{ id: string }>) => {
+ const item = state.cartItems.find((item) => item.id === action.payload.id);
+ if (item) {
+ item.amount -= 1;
+ if (item.amount < 1) {
+ state.cartItems = state.cartItems.filter((i) => i.id !== action.payload.id);
+ }
+ }
+ },
+ // ์์ดํ
๊ฐ์ ์ ๊ฑฐ
+ removeItem: (state: CartState, action: PayloadAction<{ id: string }>) => {
+ state.cartItems = state.cartItems.filter((item) => item.id !== action.payload.id);
+ },
+ // ์ฅ๋ฐ๊ตฌ๋ ์ ์ฒด ์ญ์
+ clearCart: (state: CartState) => {
+ state.cartItems = [];
+ state.amount = 0;
+ state.total = 0;
+ },
+ // ์ด ์๋ ๋ฐ ๊ธ์ก ์๋ ๊ณ์ฐ
+ calculateTotals: (state: CartState) => {
+ let totalAmount = 0;
+ let totalPrice = 0;
+
+ state.cartItems.forEach((item) => {
+ totalAmount += item.amount;
+ totalPrice += item.amount * Number(item.price);
+ });
+
+ state.amount = totalAmount;
+ state.total = totalPrice;
+ },
+ },
+});
+
+export const { increase, decrease, removeItem, clearCart, calculateTotals } = cartSlice.actions;
+export default cartSlice.reducer;
\ No newline at end of file
diff --git a/week9/src/features/modal/modalSlice.ts b/week9/src/features/modal/modalSlice.ts
new file mode 100644
index 00000000..70161767
--- /dev/null
+++ b/week9/src/features/modal/modalSlice.ts
@@ -0,0 +1,25 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+interface ModalState {
+ isOpen: boolean;
+}
+
+const initialState: ModalState = {
+ isOpen: false,
+};
+
+const modalSlice = createSlice({
+ name: 'modal',
+ initialState,
+ reducers: {
+ openModal: (state) => {
+ state.isOpen = true;
+ },
+ closeModal: (state) => {
+ state.isOpen = false;
+ },
+ },
+});
+
+export const { openModal, closeModal } = modalSlice.actions;
+export default modalSlice.reducer;
\ No newline at end of file
diff --git a/week9/src/hooks/useCustomRedux.ts b/week9/src/hooks/useCustomRedux.ts
new file mode 100644
index 00000000..ecb3ccda
--- /dev/null
+++ b/week9/src/hooks/useCustomRedux.ts
@@ -0,0 +1,5 @@
+import { useDispatch as useDefaultDispatch, useSelector as useDefaultSelector, type TypedUseSelectorHook } from 'react-redux';
+import type { RootState, AppDispatch } from '../store';
+
+export const useDispatch = () => useDefaultDispatch();
+export const useSelector: TypedUseSelectorHook = useDefaultSelector;
\ No newline at end of file
diff --git a/week9/src/index.css b/week9/src/index.css
new file mode 100644
index 00000000..a461c505
--- /dev/null
+++ b/week9/src/index.css
@@ -0,0 +1 @@
+@import "tailwindcss";
\ No newline at end of file
diff --git a/week9/src/main.tsx b/week9/src/main.tsx
new file mode 100644
index 00000000..b3c11a73
--- /dev/null
+++ b/week9/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
\ No newline at end of file
diff --git a/week9/src/store.ts b/week9/src/store.ts
new file mode 100644
index 00000000..59f37743
--- /dev/null
+++ b/week9/src/store.ts
@@ -0,0 +1,13 @@
+import { configureStore } from '@reduxjs/toolkit';
+import cartReducer from './features/cart/cartSlice';
+import modalReducer from './features/modal/modalSlice';
+
+export const store = configureStore({
+ reducer: {
+ cart: cartReducer,
+ modal: modalReducer,
+ },
+});
+
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
\ No newline at end of file
diff --git a/week9/src/store/useCartStore.ts b/week9/src/store/useCartStore.ts
new file mode 100644
index 00000000..94948dfc
--- /dev/null
+++ b/week9/src/store/useCartStore.ts
@@ -0,0 +1,69 @@
+import { create } from 'zustand';
+import cartItems, { type CartItemType } from '../constants/cartItems';
+
+interface CartState {
+ // ์ํ ๋ณ์
+ cartItems: CartItemType[];
+ amount: number;
+ total: number;
+ isModalOpen: boolean;
+
+ // ์ก์
ํจ์
+ increase: (id: string) => void;
+ decrease: (id: string) => void;
+ removeItem: (id: string) => void;
+ clearCart: () => void;
+ calculateTotals: () => void;
+ openModal: () => void;
+ closeModal: () => void;
+}
+
+export const useCartStore = create((set) => ({
+ // 1. ์ด๊ธฐ๊ฐ ์ ์ธ
+ cartItems: cartItems,
+ amount: 0,
+ total: 0,
+ isModalOpen: false,
+
+ // 2. ์ก์
ํจ์ ๊ตฌํ
+ increase: (id) =>
+ set((state) => ({
+ cartItems: state.cartItems.map((item) =>
+ item.id === id ? { ...item, amount: item.amount + 1 } : item
+ ),
+ })),
+
+ decrease: (id) =>
+ set((state) => {
+ const updatedItems = state.cartItems
+ .map((item) => (item.id === id ? { ...item, amount: item.amount - 1 } : item))
+ .filter((item) => item.amount >= 1); // 1๋ณด๋ค ์์์ง๋ฉด ์๋ ์ ๊ฑฐ ์กฐ์น
+ return { cartItems: updatedItems };
+ }),
+
+ removeItem: (id) =>
+ set((state) => ({
+ cartItems: state.cartItems.filter((item) => item.id !== id),
+ })),
+
+ clearCart: () =>
+ set({
+ cartItems: [],
+ amount: 0,
+ total: 0,
+ }),
+
+ calculateTotals: () =>
+ set((state) => {
+ let totalAmount = 0;
+ let totalPrice = 0;
+ state.cartItems.forEach((item) => {
+ totalAmount += item.amount;
+ totalPrice += item.amount * Number(item.price);
+ });
+ return { amount: totalAmount, total: totalPrice };
+ }),
+
+ openModal: () => set({ isModalOpen: true }),
+ closeModal: () => set({ isModalOpen: false }),
+}));
\ No newline at end of file
diff --git a/week9/tsconfig.app.json b/week9/tsconfig.app.json
new file mode 100644
index 00000000..7f42e5f7
--- /dev/null
+++ b/week9/tsconfig.app.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/week9/tsconfig.json b/week9/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/week9/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/week9/tsconfig.node.json b/week9/tsconfig.node.json
new file mode 100644
index 00000000..d3c52ea6
--- /dev/null
+++ b/week9/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "module": "esnext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/week9/vite.config.ts b/week9/vite.config.ts
new file mode 100644
index 00000000..c4fe1f73
--- /dev/null
+++ b/week9/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import tailwindcss from '@tailwindcss/vite';
+
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+});
\ No newline at end of file