diff --git a/.gitignore b/.gitignore index 3e0cc99b..23ba285a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ .env # Next-PWA -public/sw.js -public/workbox-*.js -public/sw.js.map \ No newline at end of file +public/*.js +public/*.js.map diff --git a/public/icons/bookmark.svg b/public/icons/bookmark.svg index 72a0962e..90c3a8e6 100644 --- a/public/icons/bookmark.svg +++ b/public/icons/bookmark.svg @@ -1,8 +1,8 @@ - + - - + + diff --git a/public/images/profile_default.svg b/public/images/profile_default.svg new file mode 100644 index 00000000..1bda6108 --- /dev/null +++ b/public/images/profile_default.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/apis/apis.ts b/src/apis/apis.ts index dca3df88..33e39c8e 100644 --- a/src/apis/apis.ts +++ b/src/apis/apis.ts @@ -15,11 +15,16 @@ export const getPoseDetail = (poseId: number) => export const getPoseTalk = () => publicApi.get('/pose/talk'); -export const getPoseFeed = (peopleCount: number, frameCount: number, tags: string) => - publicApi.get(`/pose`, { +export const getPoseFeed = async ( + peopleCount: number, + frameCount: number, + tags: string, + pageNumber: number +) => + await publicApi.get(`/pose`, { params: { frameCount, - pageNumber: 0, + pageNumber, peopleCount, tags, }, diff --git a/src/apis/queries.ts b/src/apis/queries.ts index 61768095..b7291757 100644 --- a/src/apis/queries.ts +++ b/src/apis/queries.ts @@ -1,4 +1,9 @@ -import { UseQueryOptions, useQuery } from '@tanstack/react-query'; +import { + UseInfiniteQueryOptions, + UseQueryOptions, + useInfiniteQuery, + useQuery, +} from '@tanstack/react-query'; import { FilterTagsResponse, @@ -30,12 +35,22 @@ export const usePoseTalkQuery = (options?: UseQueryOptions) => export const usePoseFeedQuery = ( { peopleCount, frameCount, tags }: FilterState, - options?: UseQueryOptions + options?: UseInfiniteQueryOptions ) => - useQuery( + useInfiniteQuery( ['poseFeed', peopleCount, frameCount, tags], - () => getPoseFeed(peopleCount, frameCount, tags.join(',')), + ({ pageParam = '' }) => getPoseFeed(peopleCount, frameCount, tags.join(','), pageParam), { + getNextPageParam: (lastPage) => { + let target = lastPage.filteredContents; + if (lastPage.recommendation) { + target = lastPage.recommendedContents; + } else { + target = lastPage.filteredContents; + } + if (target.last) return false; + return target.number + 1; + }, ...options, } ); diff --git a/src/app/(Main)/components/MainHeader.tsx b/src/app/(Main)/components/MainHeader.tsx index 4a049847..0e19ca5d 100644 --- a/src/app/(Main)/components/MainHeader.tsx +++ b/src/app/(Main)/components/MainHeader.tsx @@ -12,7 +12,11 @@ export default function MainHeader() {

PosePicker

} - rightNode={24} + rightNode={ + + 24 + + } headerDownNode={} className="px-20" /> diff --git a/src/app/(Main)/feed/components/PhotoList.tsx b/src/app/(Main)/feed/components/PhotoList.tsx index f7637ed5..75f44c37 100644 --- a/src/app/(Main)/feed/components/PhotoList.tsx +++ b/src/app/(Main)/feed/components/PhotoList.tsx @@ -7,7 +7,7 @@ interface PhotoList { export default function PhotoList({ data }: PhotoList) { return ( -
+ <> {data ? ( data.map((item) => ( )} -
+ ); } diff --git a/src/app/(Main)/feed/page.tsx b/src/app/(Main)/feed/page.tsx index a13a8315..7c955a58 100644 --- a/src/app/(Main)/feed/page.tsx +++ b/src/app/(Main)/feed/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; +import { useCallback } from 'react'; import EmptyCase from './components/EmptyCase'; import FilterSheet from './components/FilterSheet'; @@ -9,15 +9,17 @@ import FilterTab from './components/FilterTab'; import PhotoList from './components/PhotoList'; import { usePoseFeedQuery } from '@/apis'; import { Spacing } from '@/components/Spacing'; +import { URL } from '@/constants/url'; import useDidMount from '@/hooks/useDidMount'; import useFilterState from '@/hooks/useFilterState'; +import useIntersect from '@/hooks/useObserver'; export default function Feed() { const params = useSearchParams(); const router = useRouter(); const { filterState, updateFilterState } = useFilterState(); - const { data, isFetched } = usePoseFeedQuery(filterState); + const { data, fetchNextPage, hasNextPage, isLoading } = usePoseFeedQuery(filterState); useDidMount(() => { if (!params.get('filter')) return; @@ -29,27 +31,45 @@ export default function Feed() { router.replace('/feed'); }); + const onIntersect = useCallback(async () => { + if (hasNextPage && !isLoading) { + await fetchNextPage(); + } + }, [fetchNextPage, hasNextPage, isLoading]); + + const target = useIntersect(onIntersect); + return ( <> - -
- {data?.recommendation && ( + +
+ {data?.pages[0].recommendation ? ( <> -

이런 포즈는 어때요?

- +
+ {data?.pages.map((page) => ( + + ))} +
+ ) : ( +
+ {data?.pages.map((page) => ( + + ))} +
)} - {isFetched ? : } +
diff --git a/src/app/(Main)/layout.tsx b/src/app/(Main)/layout.tsx index cb5a0bab..2d646a3f 100644 --- a/src/app/(Main)/layout.tsx +++ b/src/app/(Main)/layout.tsx @@ -1,13 +1,15 @@ -import MainHeader from './components/MainHeader'; import { Spacing } from '@/components/Spacing'; +import MainHeader from './components/MainHeader'; import { StrictPropsWithChildren } from '@/types'; export default function MainLayout({ children }: StrictPropsWithChildren) { return ( <> - -
{children}
+
+ + {children} +
); } diff --git a/src/app/(Main)/pick/components/PickSection.tsx b/src/app/(Main)/pick/components/PickSection.tsx index f850fe1e..981ca477 100644 --- a/src/app/(Main)/pick/components/PickSection.tsx +++ b/src/app/(Main)/pick/components/PickSection.tsx @@ -33,28 +33,22 @@ export default function PickSection() { }; return ( -
- -
+ <> +
- - - -
-
- {true && } +
+
+ {isLoading && } sample open(({ exit }) => ( @@ -71,10 +65,11 @@ export default function PickSection() { )) } + alt="이미지" />
- + {!!image ? '포즈 pick!' : '인원수 선택하고 포즈 pick!'} -
+ ); } diff --git a/src/app/(Main)/talk/page.tsx b/src/app/(Main)/talk/page.tsx index d4944690..36b57aee 100644 --- a/src/app/(Main)/talk/page.tsx +++ b/src/app/(Main)/talk/page.tsx @@ -4,10 +4,12 @@ import { Spacing } from '@/components/Spacing'; export default function Talk() { return ( -
+ <> +
+ + +
- - -
+ ); } diff --git a/src/app/(Sub)/bookmark/components/BookmarkHeader.tsx b/src/app/(Sub)/bookmark/components/BookmarkHeader.tsx deleted file mode 100644 index 49a413fc..00000000 --- a/src/app/(Sub)/bookmark/components/BookmarkHeader.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import Image from 'next/image'; - -import { Header } from '@/components/Header'; -import { Spacing } from '@/components/Spacing'; - -export default function BookmarkHeader() { - const LeftNode = ( -
- back - -

북마크

-
- ); - const RightNode = back; - - return
; -} diff --git a/src/app/(Sub)/detail/[id]/components/DetailHeader.tsx b/src/app/(Sub)/detail/[id]/components/DetailHeader.tsx index a5086364..f467d670 100644 --- a/src/app/(Sub)/detail/[id]/components/DetailHeader.tsx +++ b/src/app/(Sub)/detail/[id]/components/DetailHeader.tsx @@ -1,23 +1,17 @@ 'use client'; import Image from 'next/image'; -import Link from 'next/link'; import IconButton from '@/components/Button/IconButton'; import { Header } from '@/components/Header'; import { Popup } from '@/components/Modal'; import { useOverlay } from '@/components/Overlay/useOverlay'; +import CloseButton from '@/components/Header/CloseButton'; export default function DetailHeader() { const { open } = useOverlay(); return (
- - back - - - } + leftNode={} rightNode={ void; - onClose: () => void; -} - -export default function MenuModal({ onConfirm, onClose }: MenuModalProps) { - return ( - - - - 문의사항을 남기시겠습니까? - - -
- - -
-
- ); -} diff --git a/src/app/(Sub)/menu/components/LoginSection.tsx b/src/app/(Sub)/menu/components/LoginSection.tsx index a93c7a5c..d51d3be0 100644 --- a/src/app/(Sub)/menu/components/LoginSection.tsx +++ b/src/app/(Sub)/menu/components/LoginSection.tsx @@ -1,14 +1,38 @@ 'use client'; +import { Popup } from '@/components/Modal'; +import { useOverlay } from '@/components/Overlay/useOverlay'; import { Spacing } from '@/components/Spacing'; +import { IMAGE } from '@/constants/image'; +import Image from 'next/image'; + +function DefaultProfile() { + return ( +
+ 🪄 +
+ ); +} export default function LoginSection() { + const { open } = useOverlay(); + return (
-
-
+
+ open(({ exit }) => ( + +

해당 기능은 아직 준비중이에요!

+

업데이트를 기대해 주세요.

+
+ )) + } + > + -

dnd9_5_ozteam@kakao.com

+
로그인하기
); diff --git a/src/app/(Sub)/menu/components/LogoutModal.tsx b/src/app/(Sub)/menu/components/LogoutModal.tsx deleted file mode 100644 index 8a16f7da..00000000 --- a/src/app/(Sub)/menu/components/LogoutModal.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button } from '@/components/Button'; -import { Popup } from '@/components/Modal'; -import { Spacing } from '@/components/Spacing'; - -interface MenuModalProps { - onConfirm?: () => void; - onClose: () => void; -} - -export default function LogoutModal({ onConfirm, onClose }: MenuModalProps) { - return ( - - -

로그아웃

- -

로그아웃시 북마크를 쓸 수 없어요.

-

정말 로그아웃하시겠어요?

- -
- - -
-
- ); -} diff --git a/src/app/(Sub)/menu/components/MenuHeader.tsx b/src/app/(Sub)/menu/components/MenuHeader.tsx new file mode 100644 index 00000000..b28af68a --- /dev/null +++ b/src/app/(Sub)/menu/components/MenuHeader.tsx @@ -0,0 +1,15 @@ +import { Header } from '@/components/Header'; +import { Spacing } from '@/components/Spacing'; +import CloseButton from '@/components/Header/CloseButton'; + +export default function MenuHeader() { + const LeftNode = ( +
+ + +

메뉴

+
+ ); + + return
; +} diff --git a/src/app/(Sub)/menu/components/MenuListSection.tsx b/src/app/(Sub)/menu/components/MenuListSection.tsx index d1e14bf8..563f183e 100644 --- a/src/app/(Sub)/menu/components/MenuListSection.tsx +++ b/src/app/(Sub)/menu/components/MenuListSection.tsx @@ -1,16 +1,46 @@ -import Image from 'next/image'; +'use client'; -import { Header } from '@/components/Header'; +import MenuModal from '@/components/Modal/MenuModal'; +import { useOverlay } from '@/components/Overlay/useOverlay'; +import { URL } from '@/constants/url'; import { Spacing } from '@/components/Spacing'; -export default function MenuHeader() { - const LeftNode = ( -
- back - -

메뉴

-
- ); +const InquiryModalContent = () =>

문의사항을 남기시겠습니까?

; +const LogoutModalContent = () => ( + <> +

로그아웃

+ +

로그아웃시 북마크를 쓸 수 없어요.

+

정말 로그아웃하시겠어요?

+ +); + +export default function MenuListSection() { + const { open } = useOverlay(); + const handleInquiryClick = () => { + open(({ exit }) => ( + window.open(URL.inquiry)}> + + + )); + }; - return
; + const handleLogoutClick = () => { + open(({ exit }) => ( + console.log('로그아웃')}> + + + )); + }; + + return ( +
+ + {/* */} +
+ ); } diff --git a/src/app/(Sub)/menu/components/MenuSection.tsx b/src/app/(Sub)/menu/components/MenuSection.tsx deleted file mode 100644 index 47fe302b..00000000 --- a/src/app/(Sub)/menu/components/MenuSection.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client'; - -import MenuModal from './InqueryModal'; -import LogoutModal from './LogoutModal'; -import { useOverlay } from '@/components/Overlay/useOverlay'; - -export default function MenuListSection() { - const { open } = useOverlay(); - const handleInquiryClick = () => { - open(({ exit }) => ); - }; - - const handleLogoutClick = () => { - open(({ exit }) => ); - }; - - return ( -
-
- 서비스 이용 문의 -
-
- 로그아웃 -
-
- ); -} diff --git a/src/app/(Sub)/menu/page.tsx b/src/app/(Sub)/menu/page.tsx index 61f7ddfe..0eb57df5 100644 --- a/src/app/(Sub)/menu/page.tsx +++ b/src/app/(Sub)/menu/page.tsx @@ -1,11 +1,11 @@ import LoginSection from './components/LoginSection'; import MakerSection from './components/MakerSection'; -import MenuHeader from './components/MenuListSection'; -import MenuListSection from './components/MenuSection'; +import MenuHeader from './components/MenuHeader'; +import MenuListSection from './components/MenuListSection'; export default function MenuPage() { return ( -
+
diff --git a/src/app/globals.css b/src/app/globals.css index a751de94..1dff8366 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -14,16 +14,18 @@ display: none; } -html { - font-size: calc(16vw / 440 * 100); -} - @media (min-width: 440px) { html { font-size: 16px; } } +@supports (-webkit-touch-callout: none) { + .h-screen { + height: -webkit-fill-available; + } +} + h1 { font-weight: 700; font-size: 3rem; @@ -88,3 +90,7 @@ caption { line-height: 1.125rem; letter-spacing: 0.4px; } + +button { + cursor: pointer; +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e7514a90..3842255a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,6 @@ import './globals.css'; import '../../styles/font.css'; import '../../styles/typography.css'; -import Script from 'next/script'; import { Suspense } from 'react'; import { Analytics } from '@/components/Analytics'; @@ -20,7 +19,7 @@ const DEFAULT_OG_IMAGE = '/images/main_star.png'; export const metadata: Metadata = { metadataBase: new URL(BASE_SITE_URL), title: { - template: `${DEFAULT_OG_TITLE} / %s `, + template: DEFAULT_OG_TITLE, default: DEFAULT_OG_TITLE, }, description: DEFAULT_OG_DESC, @@ -40,6 +39,9 @@ export const metadata: Metadata = { maximumScale: 1, userScalable: false, }, + icons: { + icon: './favicon.ico', + }, manifest: '/manifest.json', themeColor: '#ffffff', }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 71e8ac95..7792f599 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,5 +2,4 @@ import { redirect } from 'next/navigation'; export default function Home() { redirect('/pick'); - return
; } diff --git a/src/components/Button/PrimaryButton.tsx b/src/components/Button/PrimaryButton.tsx index b63072ed..98435867 100644 --- a/src/components/Button/PrimaryButton.tsx +++ b/src/components/Button/PrimaryButton.tsx @@ -5,6 +5,7 @@ interface Button { text: string; onClick?: () => void; type?: keyof Style; + className?: string; } interface Style { @@ -18,11 +19,11 @@ const style: Style = { secondary: `bg-sub-white text-secondary w-fit`, }; -export default function PrimaryButton({ icon, text, onClick, type = 'fill' }: Button) { +export default function PrimaryButton({ icon, text, onClick, type = 'fill', className }: Button) { return (
{icon && {''}}
{text}
diff --git a/src/components/Header/CloseButton.tsx b/src/components/Header/CloseButton.tsx new file mode 100644 index 00000000..baf64718 --- /dev/null +++ b/src/components/Header/CloseButton.tsx @@ -0,0 +1,15 @@ +'use client'; + +import IconButton from '../Button/IconButton'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +export default function CloseButton() { + const router = useRouter(); + + return ( + router.back()}> + back + + ); +} diff --git a/src/components/Modal/MenuModal.tsx b/src/components/Modal/MenuModal.tsx new file mode 100644 index 00000000..d652a4cf --- /dev/null +++ b/src/components/Modal/MenuModal.tsx @@ -0,0 +1,23 @@ +import { PrimaryButton } from '@/components/Button'; +import { Popup } from '@/components/Modal'; +import { Spacing } from '@/components/Spacing'; +import { PropsWithChildren } from 'react'; + +interface MenuModalProps extends PropsWithChildren { + onConfirm?: () => void; + onClose: () => void; +} + +export default function MenuModal({ children, onConfirm, onClose }: MenuModalProps) { + return ( + + + {children} + +
+ + +
+
+ ); +} diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index b993e50d..52a83621 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -15,7 +15,7 @@ export default function Popup({ return (
{children} diff --git a/src/constants/image.ts b/src/constants/image.ts new file mode 100644 index 00000000..73d30357 --- /dev/null +++ b/src/constants/image.ts @@ -0,0 +1,3 @@ +export const IMAGE = { + profile_default: '/images/profile_default.svg', +} as const; diff --git a/src/constants/url.ts b/src/constants/url.ts new file mode 100644 index 00000000..335b3f38 --- /dev/null +++ b/src/constants/url.ts @@ -0,0 +1,4 @@ +export const URL = { + inquiry: + 'https://docs.google.com/forms/d/e/1FAIpQLSeZuPZTXnO4rZ4k39SzXv96PWAW4gLcTYBrsRUrgRHSVV9Ldg/viewform?usp=sf_link', +}; diff --git a/src/hooks/useObserver.tsx b/src/hooks/useObserver.tsx new file mode 100644 index 00000000..e0cd9144 --- /dev/null +++ b/src/hooks/useObserver.tsx @@ -0,0 +1,16 @@ +import { useEffect, useRef } from 'react'; + +const useIntersect = (callback: () => void, options?: IntersectionObserverInit) => { + const target = useRef(null); + + useEffect(() => { + if (!target.current) return; + const observer = new IntersectionObserver(callback, options); + observer.observe(target.current); + return () => observer.disconnect(); + }, [callback, options]); + + return target; +}; + +export default useIntersect;