diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b72838c1..98c6f3c7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,8 +8,9 @@ import KaKaoScript from './api/kakao/KaKaoScript'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; -import Provider from '@/shared/api/Provider'; + import ClientInitHook from '@/domains/login/components/ClientInitHook'; +import Provider from '@/shared/provider/Provider'; export const metadata: Metadata = { title: { default: 'SSOUL', template: 'SSOUL | %s' }, diff --git a/src/app/page.tsx b/src/app/page.tsx index afb6fad3..30f266d2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,12 @@ import Landing from '@/domains/shared/components/3d/Landing'; +import MainSlide from '@/domains/main/components/mainSlide/components/MainSlide'; + export default function Home() { - return ; + return ( +
+ + +
+ ); } diff --git a/src/app/recipe/[id]/page.tsx b/src/app/recipe/[id]/page.tsx index ad217540..45957480 100644 --- a/src/app/recipe/[id]/page.tsx +++ b/src/app/recipe/[id]/page.tsx @@ -1,5 +1,6 @@ import { getApi } from '@/app/api/config/appConfig'; -import DetailMain from '@/domains/recipe/details/DetailMain'; +import DetailMain from '@/domains/recipe/components/details/DetailMain'; + import StarBg from '@/domains/shared/components/star-bg/StarBg'; import { Metadata } from 'next'; diff --git a/src/domains/main/components/mainSlide/components/MainSlide.tsx b/src/domains/main/components/mainSlide/components/MainSlide.tsx new file mode 100644 index 00000000..67f73fb6 --- /dev/null +++ b/src/domains/main/components/mainSlide/components/MainSlide.tsx @@ -0,0 +1,187 @@ +'use client'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import MainSlideAbv from './MainSlideAbv'; + +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import MobileSlide from './mobile/MobileSlide'; +import MainSlideIntro from './MainSlideIntro'; +import MainSlideTest from './MainSlideTest'; +import MainSlideCommunity from './MainSlideCommunity'; + +gsap.registerPlugin(ScrollTrigger); + +function MainSlide() { + const root = useRef(null); + const [isMobile, setIsMobile] = useState(false); + const [mounted, setMounted] = useState(false); + const cleanupFnRef = useRef<(() => void) | null>(null); + const resizeTimeoutRef = useRef(null); + + // 초기 마운트 + useEffect(() => { + setIsMobile(window.innerWidth < 1024); + setMounted(true); + + const handleResize = () => { + // 디바운스: resize 이벤트를 200ms 지연 + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + + resizeTimeoutRef.current = setTimeout(() => { + const newIsMobile = window.innerWidth < 1024; + + // 모바일 ↔ 데스크탑 전환 시에만 cleanup 실행 + if (newIsMobile !== isMobile) { + // GSAP을 먼저 완전히 정리 + if (cleanupFnRef.current) { + cleanupFnRef.current(); + cleanupFnRef.current = null; + } + + // 상태 업데이트 + setIsMobile(newIsMobile); + } + }, 200); + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + if (cleanupFnRef.current) { + cleanupFnRef.current(); + } + }; + }, [isMobile]); + + // GSAP 초기화 - 데스크탑에서만 + useLayoutEffect(() => { + if (!mounted) return; + if (isMobile) return; + if (!root.current) return; + + const el = root.current; + const stage = el.querySelector('.stage') as HTMLElement; + if (!stage) return; + + // 약간의 지연을 줘서 DOM이 안정화되도록 + const timer = setTimeout(() => { + if (!root.current) return; + + const ctx = gsap.context(() => { + const panels = Array.from(el.querySelectorAll('.panel')); + const tl = gsap.timeline({ paused: true, defaults: { ease: 'none' } }); + + panels.forEach((panel, i) => { + const c = panel.querySelector('.slide-content'); + if (!c) return; + const stageW = () => stage.clientWidth; + const contentW = () => c.getBoundingClientRect().width; + + gsap.set(c, { x: stageW() }); + + tl.to( + c, + { + x: () => stageW() - contentW(), + duration: 1, + immediateRender: false, + onStart: () => c.classList.remove('invisible'), + }, + i + ); + }); + + ScrollTrigger.create({ + trigger: el, + start: 'top top', + end: `+=${panels.length * 100}%`, + pin: true, + scrub: true, + animation: tl, + invalidateOnRefresh: true, + }); + + ScrollTrigger.refresh(); + }, root); + + // cleanup 함수를 ref에 저장 + cleanupFnRef.current = () => { + // ScrollTrigger를 먼저 완전히 제거 + const allTriggers = ScrollTrigger.getAll(); + allTriggers.forEach((st) => { + if (st.trigger === el || el.contains(st.trigger as Node)) { + st.kill(true); + } + }); + + // GSAP context revert + try { + ctx.revert(); + } catch (e) { + // 무시 + } + + // 혹시 남아있는 pin-spacer 수동 제거 + const pinSpacers = document.querySelectorAll('.pin-spacer'); + pinSpacers.forEach((spacer) => { + if (spacer.contains(el) || el.contains(spacer)) { + try { + const child = spacer.querySelector('section'); + if (child && spacer.parentElement) { + spacer.parentElement.appendChild(child); + } + spacer.remove(); + } catch (e) { + // 무시 + } + } + }); + }; + }, 50); + + return () => { + clearTimeout(timer); + if (cleanupFnRef.current) { + cleanupFnRef.current(); + cleanupFnRef.current = null; + } + }; + }, [isMobile, mounted]); + + // SSR 방지 + if (!mounted) { + return null; + } + + return ( + <> + {isMobile ? ( + + ) : ( +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ )} + + ); +} + +export default MainSlide; diff --git a/src/domains/main/components/mainSlide/components/MainSlideAbv.tsx b/src/domains/main/components/mainSlide/components/MainSlideAbv.tsx new file mode 100644 index 00000000..494a1a85 --- /dev/null +++ b/src/domains/main/components/mainSlide/components/MainSlideAbv.tsx @@ -0,0 +1,72 @@ +import Ssury1 from '@/shared/assets/ssury/ssury_level1.webp'; +import Ssury2 from '@/shared/assets/ssury/ssury_level2.webp'; +import Ssury3 from '@/shared/assets/ssury/ssury_level3.webp'; +import Ssury4 from '@/shared/assets/ssury/ssury_level4.webp'; +import Ssury5 from '@/shared/assets/ssury/ssury_level5.webp'; +import Ssury6 from '@/shared/assets/ssury/ssury_level6.webp'; +import MainSsuryDrunk from './MainSsuryDrunk'; + +function MainSlideAbv() { + const SSURY_DRUNK = [ + { + id: 1, + src: Ssury1, + abv: 5, + }, + { + id: 2, + src: Ssury2, + abv: 11, + }, + { + id: 3, + src: Ssury3, + abv: 26, + }, + { + id: 4, + src: Ssury4, + abv: 46, + }, + { + id: 5, + src: Ssury5, + abv: 66, + }, + { + id: 6, + src: Ssury6, + abv: 86, + }, + ]; + + return ( +
+
+ 3 +
+

+ 내 알콜도수 UP +

+

+ 5도 부터 시작하는 내 알콜도수
글 작성,댓글,좋아요 / 킵으로 알콜도수 UP!
+ 알콜도수에 따라 변하는 쑤리(SSURY)를 보는 재미도 있어요. +

+
+
+
    + {SSURY_DRUNK.map(({ id, src, abv }) => ( +
  • + +
  • + ))} +
+
+ +
+
+
+
+ ); +} +export default MainSlideAbv; diff --git a/src/domains/main/components/mainSlide/components/MainSlideCommunity.tsx b/src/domains/main/components/mainSlide/components/MainSlideCommunity.tsx new file mode 100644 index 00000000..5489143d --- /dev/null +++ b/src/domains/main/components/mainSlide/components/MainSlideCommunity.tsx @@ -0,0 +1,24 @@ +function MainSlideCommunity() { + return ( +
+
+
+ 2 +
+

+ 술술 즐기는, 커뮤니티 +

+

+ 칵테일에 대해 물어볼 곳이 없어 목이 마른 당신!
+ 초보자부터 애호가까지, Ssoul에서는 누구나 칵테일 이야기를 나눌 수 있어요. +
+ 회원들과 소통하면 내 칵테일 솜씨를 뽐내보세요. +

+
+
+ +
+
+ ); +} +export default MainSlideCommunity; diff --git a/src/domains/main/components/mainSlide/components/MainSlideDummyCard.tsx b/src/domains/main/components/mainSlide/components/MainSlideDummyCard.tsx new file mode 100644 index 00000000..18a5286e --- /dev/null +++ b/src/domains/main/components/mainSlide/components/MainSlideDummyCard.tsx @@ -0,0 +1,25 @@ +import Keep from '@/shared/assets/icons/keep_36.svg'; +import Image from 'next/image'; + +interface Props { + id: number; + src: string; + cocktailName: string; +} + +function MainSlideDummyCard({ src, cocktailName }: Props) { + return ( +
+
+ +
+ +
+ {cocktailName} + + 상세보기 +
+ +
+ ); +} +export default MainSlideDummyCard; diff --git a/src/domains/main/components/mainSlide/components/MainSlideIntro.tsx b/src/domains/main/components/mainSlide/components/MainSlideIntro.tsx new file mode 100644 index 00000000..69ec2f03 --- /dev/null +++ b/src/domains/main/components/mainSlide/components/MainSlideIntro.tsx @@ -0,0 +1,19 @@ +import background from '@/shared/assets/images/main_slide.webp'; +import Image from 'next/image'; + +function MainSlideIntro() { + return ( +
+ +
+

+ 칵테일
누구나 쉽게 즐길 수 있어요 +

+

+ SSOUL의 재밌고 다양한 기능들로 더 친근하게 접해보세요 +

+
+
+ ); +} +export default MainSlideIntro; diff --git a/src/domains/main/components/mainSlide/components/MainSlideTest.tsx b/src/domains/main/components/mainSlide/components/MainSlideTest.tsx new file mode 100644 index 00000000..c1b050a6 --- /dev/null +++ b/src/domains/main/components/mainSlide/components/MainSlideTest.tsx @@ -0,0 +1,51 @@ +import MainTestDummy from './MainTestDummy'; + +const DUMMY_TEST = [ + { + id: 1, + message: '질문형 취향 찾기', + active: false, + }, + { + id: 2, + message: '단계별 취향 찾기', + active: true, + }, +]; + +function MainSlideTest() { + return ( +
+
+ 1 +
+
+

+ AI기반 취향테스트 +

+

+ 복잡한 이름과 긴 설명 때문에 내 취향 칵테일 찾기 어려우셨나요?
+ AI쑤리가 당신에게 딱 맞는 칵테일을 추천해 드려요! +

+
+
    + + 안녕하세요! 🍹바텐더 쑤리에요. +
    + 취향에 맞는 칵테일을 추천해드릴게요.
    + 어떤 유형으로 찾아드릴까요? + + } + option={DUMMY_TEST} + type="option" + /> + +
+
+
+
+ ); +} +export default MainSlideTest; diff --git a/src/domains/main/components/mainSlide/components/MainSsuryDrunk.tsx b/src/domains/main/components/mainSlide/components/MainSsuryDrunk.tsx new file mode 100644 index 00000000..1d979cf4 --- /dev/null +++ b/src/domains/main/components/mainSlide/components/MainSsuryDrunk.tsx @@ -0,0 +1,38 @@ +import Image, { StaticImageData } from 'next/image'; + +interface Props { + src: StaticImageData; + abv: number; +} +function MainSsuryDrunk({ src, abv }: Props) { + const className = (abv: number) => { + switch (abv) { + case 5: + return 'text-stone-100'; + case 11: + return 'text-rose-100'; + case 26: + return 'text-rose-50'; + case 46: + return 'text-rose-200'; + case 66: + return 'text-rose-300'; + case 86: + return 'text-red-600'; + } + }; + return ( +
+

+ {abv} + {abv !== 86 ? ( + % + ) : ( + %~ + )} +

+ +
+ ); +} +export default MainSsuryDrunk; diff --git a/src/domains/main/components/mainSlide/components/MainTestDummy.tsx b/src/domains/main/components/mainSlide/components/MainTestDummy.tsx new file mode 100644 index 00000000..503acfa9 --- /dev/null +++ b/src/domains/main/components/mainSlide/components/MainTestDummy.tsx @@ -0,0 +1,81 @@ +import Image from 'next/image'; +import Ssury from '@/shared/assets/ssury/ssury_shaker.webp'; +import clsx from 'clsx'; +import MainSlideDummyCard from './MainSlideDummyCard'; +type Dummy = { + id: number; + message: string; + active: boolean; +}; + +interface Props { + message?: React.ReactNode; + option?: Dummy[]; + type: 'text' | 'option'; +} + +const DUMMY_CARD = [ + { + id: 1, + src: 'https://www.thecocktaildb.com/images/media/drink/fwpd0v1614006733.jpg', + cocktailName: '윈터 리타', + }, + { + id: 2, + src: 'https://www.thecocktaildb.com/images/media/drink/lnjoc81619696191.jpg', + cocktailName: '핑크 문', + }, + { + id: 3, + src: 'https://www.thecocktaildb.com/images/media/drink/pbw4e51606766578.jpg', + cocktailName: '피기 타임', + }, +]; + +function MainTestDummy({ message, option, type }: Props) { + return ( +
  • +
    +
    + 쑤리아바타 +
    +

    쑤리

    +
    + {message && ( +
    +

    {message}

    + +
    + {type == 'option' && + option && + option.map(({ id, message, active }) => ( + + {message} + + ))} +
    +
    + )} + {type == 'text' && ( +
    + {DUMMY_CARD.map(({ id, src, cocktailName }) => ( + + ))} +
    + )} +
  • + ); +} +export default MainTestDummy; diff --git a/src/domains/main/components/mainSlide/components/mobile/MobileAbv.tsx b/src/domains/main/components/mainSlide/components/mobile/MobileAbv.tsx new file mode 100644 index 00000000..43b3b16a --- /dev/null +++ b/src/domains/main/components/mainSlide/components/mobile/MobileAbv.tsx @@ -0,0 +1,72 @@ +import Ssury1 from '@/shared/assets/ssury/ssury_level1.webp'; +import Ssury2 from '@/shared/assets/ssury/ssury_level2.webp'; +import Ssury3 from '@/shared/assets/ssury/ssury_level3.webp'; +import Ssury4 from '@/shared/assets/ssury/ssury_level4.webp'; +import Ssury5 from '@/shared/assets/ssury/ssury_level5.webp'; +import Ssury6 from '@/shared/assets/ssury/ssury_level6.webp'; +import MainSsuryDrunk from '../MainSsuryDrunk'; + +function MobileAbv() { + const SSURY_DRUNK = [ + { + id: 1, + src: Ssury1, + abv: 5, + }, + { + id: 2, + src: Ssury2, + abv: 11, + }, + { + id: 3, + src: Ssury3, + abv: 26, + }, + { + id: 4, + src: Ssury4, + abv: 46, + }, + { + id: 5, + src: Ssury5, + abv: 66, + }, + { + id: 6, + src: Ssury6, + abv: 86, + }, + ]; + + return ( +
    +
    + 3 +
    +

    + 내 알콜도수 UP +

    +

    + 5도 부터 시작하는 내 알콜도수
    글 작성,댓글,좋아요 / 킵으로 알콜도수 UP!
    + 알콜도수에 따라 변하는 쑤리(SSURY)를 보는 재미도 있어요. +

    +
    +
    +
      + {SSURY_DRUNK.map(({ id, src, abv }) => ( +
    • + +
    • + ))} +
    +
    + +
    +
    +
    +
    + ); +} +export default MobileAbv; diff --git a/src/domains/main/components/mainSlide/components/mobile/MobileSlide.tsx b/src/domains/main/components/mainSlide/components/mobile/MobileSlide.tsx new file mode 100644 index 00000000..c0154be0 --- /dev/null +++ b/src/domains/main/components/mainSlide/components/mobile/MobileSlide.tsx @@ -0,0 +1,25 @@ +import Image from 'next/image'; +import background from '@/shared/assets/images/main_slide.webp'; +import MobileSlideTest from './MobileSlideTest'; +import MobileSlideCommunity from './MobileSlideCommunity'; +import MobileAbv from './MobileAbv'; + +function MobileSlide() { + return ( +
    + +

    + 칵테일
    누구나 쉽게 즐길 수 있어요 +

    +

    + SSOUL의 재밌고 다양한 기능들로 더 친근하게 접해보세요 +

    +
    + + + +
    +
    + ); +} +export default MobileSlide; diff --git a/src/domains/main/components/mainSlide/components/mobile/MobileSlideCommunity.tsx b/src/domains/main/components/mainSlide/components/mobile/MobileSlideCommunity.tsx new file mode 100644 index 00000000..393240d5 --- /dev/null +++ b/src/domains/main/components/mainSlide/components/mobile/MobileSlideCommunity.tsx @@ -0,0 +1,21 @@ +function MobileSlideCommunity() { + return ( +
    +
    + 2 +
    +

    + 술술 즐기는, 커뮤니티 +

    +

    + 칵테일에 대해 물어볼 곳이 없어 목이 마른 당신!
    + 초보자부터 애호가까지, Ssoul에서는 누구나 칵테일 이야기를 나눌 수 있어요. +
    + 회원들과 소통하면 내 칵테일 솜씨를 뽐내보세요. +

    +
    +
    +
    + ); +} +export default MobileSlideCommunity; diff --git a/src/domains/main/components/mainSlide/components/mobile/MobileSlideTest.tsx b/src/domains/main/components/mainSlide/components/mobile/MobileSlideTest.tsx new file mode 100644 index 00000000..fe04895f --- /dev/null +++ b/src/domains/main/components/mainSlide/components/mobile/MobileSlideTest.tsx @@ -0,0 +1,49 @@ +import MainTestDummy from '../MainTestDummy'; + +const DUMMY_TEST = [ + { + id: 1, + message: '질문형 취향 찾기', + active: false, + }, + { + id: 2, + message: '단계별 취향 찾기', + active: true, + }, +]; + +function MobileSlideTest() { + return ( +
    + 1 +
    +
    +

    + AI기반 취향테스트 +

    +

    + 복잡한 이름과 긴 설명 때문에 내 취향 칵테일 찾기 어려우셨나요?
    + AI쑤리가 당신에게 딱 맞는 칵테일을 추천해 드려요! +

    +
    +
      + + 안녕하세요! 🍹바텐더 쑤리에요. +
      + 취향에 맞는 칵테일을 추천해드릴게요.
      + 어떤 유형으로 찾아드릴까요? + + } + option={DUMMY_TEST} + type="option" + /> + +
    +
    +
    + ); +} +export default MobileSlideTest; diff --git a/src/domains/mypage/api/fetchAlarm.ts b/src/domains/mypage/api/fetchAlarm.ts new file mode 100644 index 00000000..a28151ed --- /dev/null +++ b/src/domains/mypage/api/fetchAlarm.ts @@ -0,0 +1,38 @@ +import { getApi } from '@/app/api/config/appConfig'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +function useFetchAlarm() { + const queryClient = useQueryClient(); + + const fetchAlarm = async () => { + const res = await fetch(`${getApi}/me/notifications`, { + method: 'GET', + credentials: 'include', + }); + const json = await res.json(); + return json.data; + }; + + const deleteAlarm = useMutation({ + mutationFn: async () => { + const res = await fetch(`${getApi}/me/notifications`, { + method: 'DELETE', + credentials: 'include', + }); + if (!res.ok) throw new Error('알림 제거 에러'); + const json = await res.json(); + return json.data; + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: ['alarm'] }); + const prev = queryClient.getQueryData(['alarm']); + queryClient.setQueryData(['alarm'], []); + return { prev }; + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['alarm'] }); + }, + }); + return { fetchAlarm, deleteAlarm }; +} +export default useFetchAlarm; diff --git a/src/domains/mypage/api/fetchMyBar.ts b/src/domains/mypage/api/fetchMyBar.ts new file mode 100644 index 00000000..d6422cc3 --- /dev/null +++ b/src/domains/mypage/api/fetchMyBar.ts @@ -0,0 +1,40 @@ +import { getApi } from '@/app/api/config/appConfig'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +function useFetchMyBar() { + const queryClient = useQueryClient(); + + const fetchMyBar = async () => { + const res = await fetch(`${getApi}/me/bar/detail`, { + method: 'GET', + credentials: 'include', + }); + const json = await res.json(); + return json.data; + }; + + const deleteMyBar = useMutation({ + mutationFn: async () => { + const res = await fetch(`${getApi}/me/bar`, { + method: 'DELETE', + credentials: 'include', + }); + if (!res.ok) throw new Error('전체 삭제 실패'); + const json = await res.json(); + return json.data; + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: ['myBar'] }); + const prev = queryClient.getQueryData(['myBar']); + queryClient.setQueryData(['myBar'], []); + return { prev }; + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['myBar'] }); + }, + }); + + return { fetchMyBar, deleteMyBar }; +} + +export default useFetchMyBar; diff --git a/src/domains/mypage/components/Alarm.tsx b/src/domains/mypage/components/Alarm.tsx index 880c11cd..7b7b177f 100644 --- a/src/domains/mypage/components/Alarm.tsx +++ b/src/domains/mypage/components/Alarm.tsx @@ -1,5 +1,6 @@ 'use client'; import SsuryAlram from '@/shared/assets/ssury/ssury_bell.webp'; +import { elapsedTime } from '@/shared/utills/elapsedTime'; import clsx from 'clsx'; import Image from 'next/image'; import { useState } from 'react'; @@ -7,10 +8,15 @@ import { useState } from 'react'; interface Props { title: string; content: string; + createdAt: Date; + read: boolean; } -function Alarm({ title, content }: Props) { - const [isClick, setIsClick] = useState(false); +function Alarm({ title, content, createdAt, read }: Props) { + const [isClick, setIsClick] = useState(read); + const date = new Date(createdAt); + const alarmDate = `${date.getMonth() + 1}월 ${date.getDate()}일`; + const time = elapsedTime(createdAt.toString()); const handleClick = () => { setIsClick(!isClick); @@ -27,13 +33,13 @@ function Alarm({ title, content }: Props) {
    알람 -

    9월 18일

    +

    {alarmDate}

    -

    10분 전

    +

    {time}

    -

    {title}

    -

    {content}

    +

    {content}

    +

    {title}

    diff --git a/src/domains/mypage/components/DeleteAllModal.tsx b/src/domains/mypage/components/DeleteAllModal.tsx index f4073df5..c7720a56 100644 --- a/src/domains/mypage/components/DeleteAllModal.tsx +++ b/src/domains/mypage/components/DeleteAllModal.tsx @@ -1,17 +1,37 @@ import ConfirmModal from '@/shared/components/modal-pop/ConfirmModal'; +import { Dispatch, SetStateAction } from 'react'; +import useFetchMyBar from '../api/fetchMyBar'; +import useFetchAlarm from '../api/fetchAlarm'; interface Props { open: boolean; onClose: () => void; + setIsModal: Dispatch>; + type: 'myBar' | 'myAlarm'; } -function DeleteAllModal({ open, onClose }: Props) { +function DeleteAllModal({ open, onClose, setIsModal, type }: Props) { + const { deleteMyBar } = useFetchMyBar(); + const { deleteAlarm } = useFetchAlarm(); + const handleBarDelete = () => { + deleteMyBar.mutate(undefined, { + onSuccess: () => setIsModal(false), + }); + }; + const handleAlarmDelete = () => { + deleteAlarm.mutate(undefined, { + onSuccess: () => setIsModal(false), + }); + }; + return ( ); } diff --git a/src/domains/mypage/components/pages/my-active/MyPost.tsx b/src/domains/mypage/components/pages/my-active/MyPost.tsx index c17c6dc8..4e2d0201 100644 --- a/src/domains/mypage/components/pages/my-active/MyPost.tsx +++ b/src/domains/mypage/components/pages/my-active/MyPost.tsx @@ -12,12 +12,10 @@ function MyPost() { credentials: 'include', }); const json = await res.json(); - console.log(json); setMyPost(json.data.items); }; useEffect(() => { - console.log(myPost); fetchPost(); }, []); diff --git a/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx b/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx index 512248bd..0124b1ee 100644 --- a/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx +++ b/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx @@ -1,47 +1,73 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import Alarm from '../../Alarm'; import { getApi } from '@/app/api/config/appConfig'; import TextButton from '@/shared/components/button/TextButton'; +import Link from 'next/link'; +import useFetchAlarm from '@/domains/mypage/api/fetchAlarm'; +import { useQuery } from '@tanstack/react-query'; +import DeleteAllModal from '../../DeleteAllModal'; interface MyAlarm { - notificationId: number; - title: string; - content: string; - isRead: boolean; createdAt: Date; + id: number; + message: string; + postCategoryName: string; + postId: number; + postThumbnailUrl: string | null; + postTitle: string; + read: boolean; + type: string; } function MyAlarm() { - const [myAlarm, setMyAlarm] = useState([]); + const [isModal, setIsModal] = useState(false); + const { fetchAlarm } = useFetchAlarm(); + const { data } = useQuery({ + queryKey: ['alarm'], + queryFn: fetchAlarm, + }); - const fetchAlarm = async () => { - const res = await fetch(`${getApi}/me/notifications`, { - method: 'GET', + const handleDelete = () => { + setIsModal(!isModal); + }; + + const handleRead = async (id: number) => { + await fetch(`${getApi}/me/notifications/${id}`, { + method: 'POST', credentials: 'include', }); - const json = await res.json(); - setMyAlarm(json.data.items); }; - - useEffect(() => { - fetchAlarm(); - }, []); + const items = data?.items ?? []; return (
    - 전체삭제 + {isModal && ( + setIsModal(!isModal)} + setIsModal={setIsModal} + type="myAlarm" + /> + )} + + 전체삭제 + +
    +
    + {items.length !== 0 ? ( + items.map(({ id, postId, postTitle, read, message, createdAt }: MyAlarm) => ( + handleRead(id)}> + + + )) + ) : ( +
    +

    알림이 없습니다.

    +
    + )}
    - {myAlarm.length !== 0 ? ( - myAlarm.map(({ notificationId, title, content }) => ( - - )) - ) : ( -
    -

    알림이 없습니다.

    -
    - )}
    ); } diff --git a/src/domains/mypage/components/pages/my-bar/MyBar.tsx b/src/domains/mypage/components/pages/my-bar/MyBar.tsx index 3bceb240..9ebd921d 100644 --- a/src/domains/mypage/components/pages/my-bar/MyBar.tsx +++ b/src/domains/mypage/components/pages/my-bar/MyBar.tsx @@ -1,10 +1,13 @@ 'use client'; -import { getApi } from '@/app/api/config/appConfig'; + import { abvMap } from '@/domains/mypage/utills/abvMap'; import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard'; import TextButton from '@/shared/components/button/TextButton'; import Link from 'next/link'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; +import DeleteAllModal from '../../DeleteAllModal'; +import useFetchMyBar from '@/domains/mypage/api/fetchMyBar'; +import { useQuery } from '@tanstack/react-query'; interface MyCocktail { cocktailId: number; @@ -16,26 +19,36 @@ interface MyCocktail { } function MyBar() { - const [myCocktail, setMyCocktail] = useState([]); - const fetchData = async () => { - const res = await fetch(`${getApi}/me/bar/detail`, { - method: 'GET', - credentials: 'include', - }); - const json = await res.json(); - setMyCocktail(json.data.items ?? []); + const [isModal, setIsModal] = useState(false); + const { fetchMyBar } = useFetchMyBar(); + const { data } = useQuery({ + queryKey: ['myBar'], + queryFn: fetchMyBar, + staleTime: 0, + }); + + const handleDelete = () => { + setIsModal(!isModal); }; - useEffect(() => { - fetchData(); - }, []); + const items = data?.items ?? []; return (
    - 전체삭제 + {isModal && ( + setIsModal(!isModal)} + setIsModal={setIsModal} + type="myBar" + /> + )} + + 전체삭제 +
    - {myCocktail.length !== 0 ? ( + {items.length > 0 ? (
    - {myCocktail.map( - ({ cocktailId, cocktailName, imageUrl, cocktailNameKo, alcoholStrength }) => { + {items.map( + ({ + cocktailId, + cocktailName, + imageUrl, + cocktailNameKo, + alcoholStrength, + }: MyCocktail) => { const alcohol = abvMap(alcoholStrength); return ( diff --git a/src/domains/mypage/hook/useProfileSsury.tsx b/src/domains/mypage/hook/useProfileSsury.tsx index d683fb66..1aa8e1d7 100644 --- a/src/domains/mypage/hook/useProfileSsury.tsx +++ b/src/domains/mypage/hook/useProfileSsury.tsx @@ -4,7 +4,7 @@ import Ssury3 from '@/shared/assets/ssury/ssury_level3.webp'; import Ssury4 from '@/shared/assets/ssury/ssury_level4.webp'; import Ssury5 from '@/shared/assets/ssury/ssury_level5.webp'; import Ssury6 from '@/shared/assets/ssury/ssury_level6.webp'; -import Image, { StaticImageData } from 'next/image'; +import { StaticImageData } from 'next/image'; const SSURY_MAP: Record = { 1: Ssury1, diff --git a/src/domains/mypage/main/MyNav.tsx b/src/domains/mypage/main/MyNav.tsx index c0579e76..e4141ba7 100644 --- a/src/domains/mypage/main/MyNav.tsx +++ b/src/domains/mypage/main/MyNav.tsx @@ -1,9 +1,6 @@ 'use client'; import TabMenu from '@/domains/mypage/main/TabMenu'; -import TextButton from '@/shared/components/button/TextButton'; import Link from 'next/link'; -import { useState } from 'react'; -import DeleteAllModal from '../components/DeleteAllModal'; import { usePathname } from 'next/navigation'; const MAIN_TABMENU = [ @@ -49,13 +46,8 @@ function MyNav() { const subIndex = SUB_TABMENU.findIndex((opt) => pathname.startsWith(opt.href)); const isSubActive = subIndex === -1 ? 0 : subIndex; - const [isDeleteAll, setIsDeleteAll] = useState(false); - return (
    - {isDeleteAll && ( - setIsDeleteAll(!isDeleteAll)} /> - )}

    마이페이지 탭 메뉴

    diff --git a/src/domains/recipe/api/RecipeFetch.tsx b/src/domains/recipe/api/RecipeFetch.tsx index a86fecb5..39dbe35d 100644 --- a/src/domains/recipe/api/RecipeFetch.tsx +++ b/src/domains/recipe/api/RecipeFetch.tsx @@ -2,7 +2,7 @@ import { getApi } from '@/app/api/config/appConfig'; import { Cocktail } from '../types/types'; -import { Dispatch, SetStateAction, useCallback, useState } from 'react'; +import { Dispatch, SetStateAction, useCallback } from 'react'; import { useAuthStore } from '@/domains/shared/store/auth'; interface Props { @@ -23,7 +23,7 @@ export const RecipeFetch = ({ setHasNextPage, SIZE = 20, }: Props) => { - const user = useAuthStore(); + const user = useAuthStore((state) => state.user); const fetchData = useCallback(async () => { // 쿼리파라미터에 값 넣기 if (!hasNextPage) return; diff --git a/src/domains/recipe/details/DetailItem.tsx b/src/domains/recipe/components/details/DetailItem.tsx similarity index 97% rename from src/domains/recipe/details/DetailItem.tsx rename to src/domains/recipe/components/details/DetailItem.tsx index 64f10910..09302fee 100644 --- a/src/domains/recipe/details/DetailItem.tsx +++ b/src/domains/recipe/components/details/DetailItem.tsx @@ -1,8 +1,8 @@ import Image from 'next/image'; import Label from '@/domains/shared/components/label/Label'; import AbvGraph from '@/domains/shared/components/abv-graph/AbvGraph'; -import { labelTitle } from '../utills/labelTitle'; -import useGlass from '../hook/useGlass'; +import { labelTitle } from '../../utills/labelTitle'; +import useGlass from '../../hook/useGlass'; interface Props { name: string; diff --git a/src/domains/recipe/details/DetailList.tsx b/src/domains/recipe/components/details/DetailList.tsx similarity index 95% rename from src/domains/recipe/details/DetailList.tsx rename to src/domains/recipe/components/details/DetailList.tsx index 8baabfca..90e9445f 100644 --- a/src/domains/recipe/details/DetailList.tsx +++ b/src/domains/recipe/components/details/DetailList.tsx @@ -3,8 +3,9 @@ import { useEffect, useState } from 'react'; import DetailRecommendList from './DetailRecommendList'; import { getApi } from '@/app/api/config/appConfig'; import { useParams } from 'next/navigation'; -import { RecommendCocktail } from '../types/types'; + import Link from 'next/link'; +import { RecommendCocktail } from '../../types/types'; function DetailList() { const { id } = useParams(); diff --git a/src/domains/recipe/details/DetailMain.tsx b/src/domains/recipe/components/details/DetailMain.tsx similarity index 96% rename from src/domains/recipe/details/DetailMain.tsx rename to src/domains/recipe/components/details/DetailMain.tsx index de057e02..3ee858ba 100644 --- a/src/domains/recipe/details/DetailMain.tsx +++ b/src/domains/recipe/components/details/DetailMain.tsx @@ -8,10 +8,10 @@ import SsuryDrink from '@/shared/assets/ssury/ssury_drink.webp'; import Image from 'next/image'; import DetailList from './DetailList'; import { Suspense, useEffect, useState } from 'react'; -import SkeletonDetail from '../skeleton/SkeletonDetail'; -import RecipeComment from '../components/details/RecipeComment'; import { getApi } from '@/app/api/config/appConfig'; import { useAuthStore } from '@/domains/shared/store/auth'; +import SkeletonDetail from '../../skeleton/SkeletonDetail'; +import RecipeComment from './RecipeComment'; interface Kept { cocktailId: number; diff --git a/src/domains/recipe/details/DetailRecipe.tsx b/src/domains/recipe/components/details/DetailRecipe.tsx similarity index 97% rename from src/domains/recipe/details/DetailRecipe.tsx rename to src/domains/recipe/components/details/DetailRecipe.tsx index 810ce7b1..c565e2ac 100644 --- a/src/domains/recipe/details/DetailRecipe.tsx +++ b/src/domains/recipe/components/details/DetailRecipe.tsx @@ -1,4 +1,4 @@ -import { ozToMl } from '../hook/ozToMl'; +import { ozToMl } from '../../hook/ozToMl'; type Recipe = { ingredientName: string; diff --git a/src/domains/recipe/details/DetailRecommendList.tsx b/src/domains/recipe/components/details/DetailRecommendList.tsx similarity index 100% rename from src/domains/recipe/details/DetailRecommendList.tsx rename to src/domains/recipe/components/details/DetailRecommendList.tsx diff --git a/src/domains/recipe/details/DetailsHeader.tsx b/src/domains/recipe/components/details/DetailsHeader.tsx similarity index 96% rename from src/domains/recipe/details/DetailsHeader.tsx rename to src/domains/recipe/components/details/DetailsHeader.tsx index acd9e5d7..31f4bb07 100644 --- a/src/domains/recipe/details/DetailsHeader.tsx +++ b/src/domains/recipe/components/details/DetailsHeader.tsx @@ -1,10 +1,11 @@ 'use client'; import Share from '@/domains/shared/components/share/Share'; -import BackBtn from '../components/details/BackBtn'; + import Keep from '@/domains/shared/components/keep/Keep'; import { useEffect, useState } from 'react'; import ShareModal from '@/domains/shared/components/share/ShareModal'; import { getApi } from '@/app/api/config/appConfig'; +import BackBtn from './BackBtn'; interface Meta { title: string; diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index 1941e240..589823c6 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -12,7 +12,7 @@ interface Props { function CocktailFilter({ cocktailsEA, setData }: Props) { const sortMap = { 최신순: 'recent', - 인기순: 'popular', + 인기순: 'keeps', 댓글순: 'comments', }; const searchParams = useSearchParams(); diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 4d075d04..a63dc93f 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -9,8 +9,11 @@ import { RecipeFetch } from '../../api/RecipeFetch'; import CocktailSearchBar from './CocktailSearchBar'; import useSearchControl from '../../hook/useSearchControl'; import CocktailSearch from '../../api/CocktailSearch'; +import { useAuthStore } from '@/domains/shared/store/auth'; function Cocktails() { + const user = useAuthStore((state) => state.user); + const [data, setData] = useState([]); const [lastId, setLastId] = useState(null); const [hasNextPage, setHasNextPage] = useState(true); @@ -41,13 +44,13 @@ function Cocktails() { : `전체 ${data.length}`; // 초기 로드 시 검색어가 있으면 검색 실행 - useEffect(() => { - const readyForFirstLoad = !isSearching && hasNextPage && lastId == null && data.length === 0; + // useEffect(() => { + // const readyForFirstLoad = !isSearching && hasNextPage && lastId == null && data.length === 0; - if (readyForFirstLoad) { - fetchData(); - } - }, [hasNextPage, lastId]); + // if (readyForFirstLoad) { + // fetchData(); + // } + // }, [hasNextPage, lastId]); // 검색어 변경 시 useEffect(() => { diff --git a/src/domains/shared/components/3d/Landing.tsx b/src/domains/shared/components/3d/Landing.tsx index abf77bff..ceded718 100644 --- a/src/domains/shared/components/3d/Landing.tsx +++ b/src/domains/shared/components/3d/Landing.tsx @@ -16,7 +16,6 @@ function Landing() { setIsLoading(false)} /> {!isLoading && }
    -
    ); diff --git a/src/shared/assets/images/main_slide.webp b/src/shared/assets/images/main_slide.webp new file mode 100644 index 00000000..a6b0bbcf Binary files /dev/null and b/src/shared/assets/images/main_slide.webp differ diff --git a/src/shared/assets/images/ssuryExam1.webp b/src/shared/assets/images/ssuryExam1.webp new file mode 100644 index 00000000..0121f9d3 Binary files /dev/null and b/src/shared/assets/images/ssuryExam1.webp differ diff --git a/src/shared/assets/images/ssuryExam2.webp b/src/shared/assets/images/ssuryExam2.webp new file mode 100644 index 00000000..b093d69b Binary files /dev/null and b/src/shared/assets/images/ssuryExam2.webp differ diff --git a/src/shared/assets/images/ssuryExam3.webp b/src/shared/assets/images/ssuryExam3.webp new file mode 100644 index 00000000..2e4accc8 Binary files /dev/null and b/src/shared/assets/images/ssuryExam3.webp differ diff --git a/src/shared/api/Provider.tsx b/src/shared/provider/Provider.tsx similarity index 100% rename from src/shared/api/Provider.tsx rename to src/shared/provider/Provider.tsx