From c8c5802b6b40ccf09178a0e0d93b66e99fc80c7d Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Sun, 8 Feb 2026 22:20:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat=20:=20Footer=20=ED=95=98=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=ED=9B=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/package.json | 1 + apps/web/src/Footer.tsx | 61 +++++++++++++++++++++------ apps/web/src/stores/useFooterStore.ts | 16 +++++++ pnpm-lock.yaml | 29 +++++++------ 4 files changed, 81 insertions(+), 26 deletions(-) create mode 100644 apps/web/src/stores/useFooterStore.ts diff --git a/apps/web/package.json b/apps/web/package.json index b7bcb60..b0a6d9f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "framer-motion": "^12.33.0", "gsap": "^3.14.2", "immer": "^10.1.1", "lottie-react": "^2.4.1", diff --git a/apps/web/src/Footer.tsx b/apps/web/src/Footer.tsx index ebce66e..8276a4e 100644 --- a/apps/web/src/Footer.tsx +++ b/apps/web/src/Footer.tsx @@ -1,38 +1,73 @@ 'use client'; +import { motion } from 'framer-motion'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { useEffect } from 'react'; import { Button } from '@/components/ui/button'; +import { useFooterStore } from '@/stores/useFooterStore'; import { cn } from '@/utils/cn'; +const FOOTER_KEY = { + SEARCH: 'SEARCH', + RECENT: 'RECENT', + TOSING: 'TOSING', + POPULAR: 'POPULAR', + INFO: 'INFO', +}; + const navigation = [ - { name: '최신 곡', href: '/recent' }, + { name: '최신 곡', href: '/recent', key: FOOTER_KEY.RECENT }, - { name: '부를 곡', href: '/tosing' }, - { name: '검색', href: '/' }, + { name: '부를 곡', href: '/tosing', key: FOOTER_KEY.TOSING }, + { name: '검색', href: '/', key: FOOTER_KEY.SEARCH }, - { name: '인기곡', href: '/popular' }, - { name: '정보', href: '/info' }, + { name: '인기곡', href: '/popular', key: FOOTER_KEY.POPULAR }, + { name: '정보', href: '/info', key: FOOTER_KEY.INFO }, ]; export default function Footer() { const pathname = usePathname(); + const { activeFooterItem } = useFooterStore(); const navPath = pathname.split('/')[1]; return ( diff --git a/apps/web/src/stores/useFooterStore.ts b/apps/web/src/stores/useFooterStore.ts new file mode 100644 index 0000000..bdfc246 --- /dev/null +++ b/apps/web/src/stores/useFooterStore.ts @@ -0,0 +1,16 @@ +import { create } from 'zustand'; + +interface FooterStore { + activeFooterItem: string | null; + triggerFooterAnimation: (href: string) => void; +} + +export const useFooterStore = create(set => ({ + activeFooterItem: null, + triggerFooterAnimation: href => { + set({ activeFooterItem: href }); + setTimeout(() => { + set({ activeFooterItem: null }); + }, 300); + }, +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7549fd..047eddf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,6 +220,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + framer-motion: + specifier: ^12.33.0 + version: 12.33.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) gsap: specifier: ^3.14.2 version: 3.14.2 @@ -4564,8 +4567,8 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.29.0: - resolution: {integrity: sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg==} + framer-motion@12.33.0: + resolution: {integrity: sha512-ca8d+rRPcDP5iIF+MoT3WNc0KHJMjIyFAbtVLvM9eA7joGSpeqDfiNH/kCs1t4CHi04njYvWyj0jS4QlEK/rJQ==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -5836,11 +5839,11 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - motion-dom@12.29.0: - resolution: {integrity: sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA==} + motion-dom@12.33.0: + resolution: {integrity: sha512-XRPebVypsl0UM+7v0Hr8o9UAj0S2djsQWRdHBd5iVouVpMrQqAI0C/rDAT3QaYnXnHuC5hMcwDHCboNeyYjPoQ==} - motion-utils@12.27.2: - resolution: {integrity: sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q==} + motion-utils@12.29.2: + resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==} motion@12.29.0: resolution: {integrity: sha512-rjB5CP2N9S2ESAyEFnAFMgTec6X8yvfxLNcz8n12gPq3M48R7ZbBeVYkDOTj8SPMwfvGIFI801SiPSr1+HCr9g==} @@ -12824,10 +12827,10 @@ snapshots: fraction.js@5.3.4: {} - framer-motion@12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@12.33.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.29.0 - motion-utils: 12.27.2 + motion-dom: 12.33.0 + motion-utils: 12.29.2 tslib: 2.8.1 optionalDependencies: react: 19.2.3 @@ -14441,15 +14444,15 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - motion-dom@12.29.0: + motion-dom@12.33.0: dependencies: - motion-utils: 12.27.2 + motion-utils: 12.29.2 - motion-utils@12.27.2: {} + motion-utils@12.29.2: {} motion@12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - framer-motion: 12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.33.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 optionalDependencies: react: 19.2.3 From d48b43b617dd116289e2c42a593932f7a7721a5b Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Sun, 8 Feb 2026 22:47:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat=20:=20Framer=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80.=20store?= =?UTF-8?q?=EB=A1=9C=20=EB=8C=80=EC=9D=91=ED=95=98=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98=20=ED=98=B8=EC=B6=9C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/Footer.tsx | 35 +++++++++----------- apps/web/src/components/ThumbUpModal.tsx | 5 +++ apps/web/src/hooks/useSearchSong.ts | 14 ++++++++ apps/web/src/stores/useFooterAnimateStore.ts | 25 ++++++++++++++ apps/web/src/stores/useFooterStore.ts | 16 --------- 5 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 apps/web/src/stores/useFooterAnimateStore.ts delete mode 100644 apps/web/src/stores/useFooterStore.ts diff --git a/apps/web/src/Footer.tsx b/apps/web/src/Footer.tsx index 8276a4e..6cd01d9 100644 --- a/apps/web/src/Footer.tsx +++ b/apps/web/src/Footer.tsx @@ -3,33 +3,30 @@ import { motion } from 'framer-motion'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { useEffect } from 'react'; import { Button } from '@/components/ui/button'; -import { useFooterStore } from '@/stores/useFooterStore'; +import useFooterAnimateStore, { FooterKey } from '@/stores/useFooterAnimateStore'; import { cn } from '@/utils/cn'; -const FOOTER_KEY = { - SEARCH: 'SEARCH', - RECENT: 'RECENT', - TOSING: 'TOSING', - POPULAR: 'POPULAR', - INFO: 'INFO', -}; +interface Navigation { + name: string; + href: string; + key: FooterKey; +} -const navigation = [ - { name: '최신 곡', href: '/recent', key: FOOTER_KEY.RECENT }, +const navigation: Navigation[] = [ + { name: '최신 곡', href: '/recent', key: 'RECENT' }, - { name: '부를 곡', href: '/tosing', key: FOOTER_KEY.TOSING }, - { name: '검색', href: '/', key: FOOTER_KEY.SEARCH }, + { name: '부를 곡', href: '/tosing', key: 'TOSING' }, + { name: '검색', href: '/', key: 'SEARCH' }, - { name: '인기곡', href: '/popular', key: FOOTER_KEY.POPULAR }, - { name: '정보', href: '/info', key: FOOTER_KEY.INFO }, + { name: '인기곡', href: '/popular', key: 'POPULAR' }, + { name: '정보', href: '/info', key: 'INFO' }, ]; export default function Footer() { const pathname = usePathname(); - const { activeFooterItem } = useFooterStore(); + const { activeFooterItem } = useFooterAnimateStore(); const navPath = pathname.split('/')[1]; return ( @@ -42,9 +39,9 @@ export default function Footer() {
{isAnimating && ( @@ -60,7 +57,7 @@ export default function Footer() { {item.name} diff --git a/apps/web/src/components/ThumbUpModal.tsx b/apps/web/src/components/ThumbUpModal.tsx index 30e0557..917763c 100644 --- a/apps/web/src/components/ThumbUpModal.tsx +++ b/apps/web/src/components/ThumbUpModal.tsx @@ -11,6 +11,7 @@ import { Slider } from '@/components/ui/slider'; import { useSongThumbMutation } from '@/queries/songThumbQuery'; import { useUserQuery } from '@/queries/userQuery'; import { usePatchSetPointMutation } from '@/queries/userQuery'; +import useFooterAnimateStore from '@/stores/useFooterAnimateStore'; import FallingIcons from './FallingIcons'; @@ -29,10 +30,14 @@ export default function ThumbUpModal({ songId, handleClose }: ThumbUpModalProps) const { mutate: patchSongThumb, isPending: isPendingSongThumb } = useSongThumbMutation(); const { mutate: patchSetPoint, isPending: isPendingSetPoint } = usePatchSetPointMutation(); + const { triggerFooterAnimation } = useFooterAnimateStore(); + const handleClickThumb = () => { patchSongThumb({ songId, point: value[0] }); patchSetPoint({ point: point - value[0] }); + triggerFooterAnimation('POPULAR'); + handleClose(); }; diff --git a/apps/web/src/hooks/useSearchSong.ts b/apps/web/src/hooks/useSearchSong.ts index 3682606..d102c88 100644 --- a/apps/web/src/hooks/useSearchSong.ts +++ b/apps/web/src/hooks/useSearchSong.ts @@ -9,6 +9,7 @@ import { useToggleToSingMutation, } from '@/queries/searchSongQuery'; import useAuthStore from '@/stores/useAuthStore'; +import useFooterAnimateStore from '@/stores/useFooterAnimateStore'; import useGuestToSingStore from '@/stores/useGuestToSingStore'; import useSearchHistoryStore from '@/stores/useSearchHistoryStore'; import { Method } from '@/types/common'; @@ -46,6 +47,7 @@ export default function useSearchSong() { isError, } = useInfiniteSearchSongQuery(query, searchType, isAuthenticated); + const { triggerFooterAnimation } = useFooterAnimateStore(); const { addToHistory } = useSearchHistoryStore(); const { addGuestToSingSong, removeGuestToSingSong } = useGuestToSingStore(); @@ -75,6 +77,7 @@ export default function useSearchSong() { if (!isAuthenticated) { if (method === 'POST') { addGuestToSingSong(song); + triggerFooterAnimation('TOSING'); } else { removeGuestToSingSong(song.id); } @@ -85,6 +88,10 @@ export default function useSearchSong() { toast.error('요청 중입니다. 잠시 후 다시 시도해주세요.'); return; } + + if (method === 'POST') { + triggerFooterAnimation('TOSING'); + } toggleToSing({ songId: song.id, method }); }; @@ -98,6 +105,10 @@ export default function useSearchSong() { toast.error('요청 중입니다. 잠시 후 다시 시도해주세요.'); return; } + + if (method === 'POST') { + triggerFooterAnimation('INFO'); + } toggleLike({ songId, method }); }; @@ -116,6 +127,8 @@ export default function useSearchSong() { toast.error('요청 중입니다. 잠시 후 다시 시도해주세요.'); return; } + + triggerFooterAnimation('INFO'); postSong({ songId, folderName, query, searchType }); }; @@ -125,6 +138,7 @@ export default function useSearchSong() { return; } + triggerFooterAnimation('INFO'); moveSong({ songIdArray: [songId], folderId }); }; diff --git a/apps/web/src/stores/useFooterAnimateStore.ts b/apps/web/src/stores/useFooterAnimateStore.ts new file mode 100644 index 0000000..520ab76 --- /dev/null +++ b/apps/web/src/stores/useFooterAnimateStore.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand'; + +export type FooterKey = 'SEARCH' | 'RECENT' | 'TOSING' | 'POPULAR' | 'INFO' | null; + +interface FooterStore { + activeFooterItem: FooterKey; + triggerFooterAnimation: (key: FooterKey) => void; +} + +const initialState = { + activeFooterItem: null, +}; + +const useFooterAnimateStore = create(set => ({ + ...initialState, + + triggerFooterAnimation: key => { + set({ activeFooterItem: key }); + setTimeout(() => { + set({ activeFooterItem: null }); + }, 300); + }, +})); + +export default useFooterAnimateStore; diff --git a/apps/web/src/stores/useFooterStore.ts b/apps/web/src/stores/useFooterStore.ts deleted file mode 100644 index bdfc246..0000000 --- a/apps/web/src/stores/useFooterStore.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { create } from 'zustand'; - -interface FooterStore { - activeFooterItem: string | null; - triggerFooterAnimation: (href: string) => void; -} - -export const useFooterStore = create(set => ({ - activeFooterItem: null, - triggerFooterAnimation: href => { - set({ activeFooterItem: href }); - setTimeout(() => { - set({ activeFooterItem: null }); - }, 300); - }, -})); From c411286ce43f4ba33378a076c96c6641938681af Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Sun, 8 Feb 2026 23:10:01 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix=20:=20store=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/Footer.tsx | 4 ++-- apps/web/src/components/ThumbUpModal.tsx | 4 ++-- apps/web/src/hooks/useSearchSong.ts | 12 ++++++------ apps/web/src/stores/useFooterAnimateStore.ts | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/web/src/Footer.tsx b/apps/web/src/Footer.tsx index 6cd01d9..acfccf2 100644 --- a/apps/web/src/Footer.tsx +++ b/apps/web/src/Footer.tsx @@ -26,14 +26,14 @@ const navigation: Navigation[] = [ export default function Footer() { const pathname = usePathname(); - const { activeFooterItem } = useFooterAnimateStore(); + const { footerAnimateKey } = useFooterAnimateStore(); const navPath = pathname.split('/')[1]; return (