diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index f9538abe2..f5f66ab4a 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,7 +2,9 @@ import React from 'react'; import { themes } from '@storybook/theming'; import { LazyMotion } from 'framer-motion'; import { initialize, mswDecorator } from 'msw-storybook-addon'; +import NextAdapterPages from 'next-query-params/pages'; import { RouterContext } from 'next/dist/shared/lib/router-context'; +import { QueryParamProvider } from 'use-query-params'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { RecoilRoot } from 'recoil'; @@ -43,18 +45,20 @@ export const parameters = { export const decorators = [ (Story) => ( - - import('framer-motion').then((mod) => mod.domAnimation)}> - - - - - - - - - - + + + import('framer-motion').then((mod) => mod.domAnimation)}> + + + + + + + + + + + ), mswDecorator, diff --git a/package.json b/package.json index f40267782..64735f4f3 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "lodash-es": "^4.17.21", "nanoid": "^4.0.0", "next": "^13.1.1", + "next-query-params": "^4.2.3", "next-seo": "^6.1.0", "qs": "^6.11.0", "react": "^18.2.0", @@ -62,6 +63,7 @@ "react-remove-scroll": "^2.5.6", "recoil": "^0.7.2", "swiper": "^9.4.1", + "use-query-params": "^2.2.1", "yup": "^1.0.0", "zod": "^3.20.6" }, diff --git a/src/components/community/layout/DesktopCommunityLayout.tsx b/src/components/community/layout/DesktopCommunityLayout.tsx new file mode 100644 index 000000000..3954efa8b --- /dev/null +++ b/src/components/community/layout/DesktopCommunityLayout.tsx @@ -0,0 +1,60 @@ +import styled from '@emotion/styled'; +import { m } from 'framer-motion'; +import { FC, ReactNode } from 'react'; + +import { layoutCSSVariable } from '@/components/layout/utils'; + +interface DesktopCommunityLayoutProps { + isDetailOpen: boolean; + listSlot: ReactNode; + detailSlot: ReactNode; +} + +const DETAIL_SLOT_WIDTH = 560; + +const DesktopCommunityLayout: FC = ({ isDetailOpen, listSlot, detailSlot }) => { + return ( + + {listSlot} + + {detailSlot} + + + ); +}; + +export default DesktopCommunityLayout; + +const Container = styled.div` + display: flex; + justify-content: center; + width: 100%; + height: ${layoutCSSVariable.contentAreaHeight}; +`; + +const ListSlot = styled.div` + flex: 1 1 0; + max-width: 560px; + height: 100%; +`; + +const DetailSlot = styled(m.div)` + position: relative; + width: 100%; + max-width: 560px; + height: 100%; + overflow: hidden; +`; + +const DetailSlotInner = styled.div` + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 560px; + min-width: ${DETAIL_SLOT_WIDTH}px; +`; diff --git a/src/components/community/layout/MobileCommunityLayout.tsx b/src/components/community/layout/MobileCommunityLayout.tsx new file mode 100644 index 000000000..7c7d38293 --- /dev/null +++ b/src/components/community/layout/MobileCommunityLayout.tsx @@ -0,0 +1,42 @@ +import styled from '@emotion/styled'; +import { colors } from '@sopt-makers/colors'; +import { m } from 'framer-motion'; +import { FC, ReactNode } from 'react'; + +import { layoutCSSVariable } from '@/components/layout/utils'; + +interface MobileCommunityLayoutProps { + isDetailOpen: boolean; + listSlot: ReactNode; + detailSlot: ReactNode; +} + +const MobileCommunityLayout: FC = ({ isDetailOpen, listSlot, detailSlot }) => { + return ( + + {listSlot} + + {detailSlot} + + + ); +}; + +export default MobileCommunityLayout; + +const Container = styled.div` + position: relative; + height: ${layoutCSSVariable.contentAreaHeight}; + overflow: hidden; +`; + +const ListSlotBox = styled.div` + position: absolute; + inset: 0; +`; + +const DetailSlotBox = styled(m.div)` + position: absolute; + inset: 0; + background-color: ${colors.background}; +`; diff --git a/src/components/community/page.tsx b/src/components/community/page.tsx new file mode 100644 index 000000000..0f6e501bd --- /dev/null +++ b/src/components/community/page.tsx @@ -0,0 +1,43 @@ +import { colors } from '@sopt-makers/colors'; +import { FC } from 'react'; + +import Responsive from '@/components/common/Responsive'; +import DesktopCommunityLayout from '@/components/community/layout/DesktopCommunityLayout'; +import MobileCommunityLayout from '@/components/community/layout/MobileCommunityLayout'; +import { FeedDetailLink, TagLink, useFeedDetailParam, useTagParam } from '@/components/community/queryParam'; + +const CommunityPage: FC = () => { + const [feed] = useFeedDetailParam(); + const [tag] = useTagParam(); + + const feedList = ( +
+ 태그1 + 태그2 + 태그3 +
CurrentTag: {tag}
+ 한강라면 + 치킨 +
+ ); + + const feedDetail = ( +
+ 닫기 +
피드 ID: {feed}
+
+ ); + + return ( + <> + + + + + + + + ); +}; + +export default CommunityPage; diff --git a/src/components/community/queryParam.tsx b/src/components/community/queryParam.tsx new file mode 100644 index 000000000..a3e9d5e10 --- /dev/null +++ b/src/components/community/queryParam.tsx @@ -0,0 +1,51 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { ComponentPropsWithoutRef, forwardRef } from 'react'; +import { StringParam, useQueryParam, withDefault } from 'use-query-params'; + +/** + * 쿼리 파라미터와 연동된 상태를 쉽게 만들 수 있게 해줍니다. paramKey가 쿼리 파라미터의 키가 됩니다. + */ +function createLinkComponent({ paramKey }: { paramKey: T }) { + type Key = { + [K in `${T}Id`]: string | undefined; + }; + + const ParamLink = forwardRef< + HTMLAnchorElement, + Omit, 'href' | 'shallow'> & Key + >((props, ref) => { + const { pathname, query } = useRouter(); + const value = props[`${paramKey}Id`] as string | undefined; + + const newProps = { ...props }; + delete newProps[`${paramKey}Id`]; + + return ( + + ); + }); + + const useParam = () => { + return useQueryParam(paramKey, withDefault(StringParam, undefined)); + }; + + return [ParamLink, useParam] as const; +} + +export const [TagLink, useTagParam] = createLinkComponent({ paramKey: 'tag' }); + +export const [CategoryLink, useCategoryParam] = createLinkComponent({ paramKey: 'category' }); + +export const [FeedDetailLink, useFeedDetailParam] = createLinkComponent({ paramKey: 'feed' }); diff --git a/src/components/layout/FullScreenLayout.tsx b/src/components/layout/FullScreenLayout.tsx index d47005151..2fa8b7f05 100644 --- a/src/components/layout/FullScreenLayout.tsx +++ b/src/components/layout/FullScreenLayout.tsx @@ -1,6 +1,8 @@ import styled from '@emotion/styled'; import { FC, ReactNode } from 'react'; +import { createLayoutCSSVariable } from '@/components/layout/utils'; + interface FullScreenLayoutProps { children: ReactNode; } @@ -13,4 +15,6 @@ export default FullScreenLayout; const StyledFullScreenLayout = styled.div` height: 100vh; + + ${createLayoutCSSVariable({ headerHeight: 0 })} `; diff --git a/src/components/layout/HeaderFooterLayout.tsx b/src/components/layout/HeaderFooterLayout.tsx index 7f250dc19..74c699060 100644 --- a/src/components/layout/HeaderFooterLayout.tsx +++ b/src/components/layout/HeaderFooterLayout.tsx @@ -5,6 +5,7 @@ import { RemoveScroll } from 'react-remove-scroll'; import Footer from '@/components/common/Footer'; import Header from '@/components/common/Header'; import Responsive from '@/components/common/Responsive'; +import { createLayoutCSSVariable } from '@/components/layout/utils'; import { MOBILE_MEDIA_QUERY } from '@/styles/mediaQuery'; interface HeaderLayoutProps { @@ -31,8 +32,12 @@ export default HeaderFooterLayout; const StyledContainer = styled.div` padding-top: 80px; + ${createLayoutCSSVariable({ headerHeight: 80 })} + @media ${MOBILE_MEDIA_QUERY} { padding-top: 56px; + + ${createLayoutCSSVariable({ headerHeight: 80 })} } `; diff --git a/src/components/layout/HeaderLayout.tsx b/src/components/layout/HeaderLayout.tsx index f042b275b..8e23d362d 100644 --- a/src/components/layout/HeaderLayout.tsx +++ b/src/components/layout/HeaderLayout.tsx @@ -3,6 +3,7 @@ import { FC, ReactNode } from 'react'; import { RemoveScroll } from 'react-remove-scroll'; import Header from '@/components/common/Header'; +import { createLayoutCSSVariable } from '@/components/layout/utils'; import { MOBILE_MEDIA_QUERY } from '@/styles/mediaQuery'; interface HeaderLayoutProps { @@ -25,8 +26,12 @@ export default HeaderLayout; const StyledContainer = styled.div` padding-top: 80px; + ${createLayoutCSSVariable({ headerHeight: 80 })} + @media ${MOBILE_MEDIA_QUERY} { padding-top: 56px; + + ${createLayoutCSSVariable({ headerHeight: 56 })} } `; diff --git a/src/components/layout/utils.ts b/src/components/layout/utils.ts new file mode 100644 index 000000000..41a095267 --- /dev/null +++ b/src/components/layout/utils.ts @@ -0,0 +1,22 @@ +interface CreateLayoutCSSVariableOptions { + headerHeight: number; + footerHeight?: number; +} + +const layoutCSSVariableNames = { + globalHeaderHeight: '--global-header-height', + globalFooterHeight: '--global-footer-height', + contentAreaHeight: '--content-area-height', +}; + +export const layoutCSSVariable = Object.fromEntries( + Object.entries(layoutCSSVariableNames).map(([key, value]) => [key, `var(${value})`]), +) as Record; + +export function createLayoutCSSVariable({ headerHeight, footerHeight = 0 }: CreateLayoutCSSVariableOptions) { + return ` + ${layoutCSSVariableNames.globalHeaderHeight}: ${headerHeight}px; + ${layoutCSSVariableNames.globalFooterHeight}: ${footerHeight}px; + ${layoutCSSVariableNames.contentAreaHeight}: calc(100vh - var(--global-header-height) - var(--global-footer-height)); + `; +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 80fa66302..25cc00389 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -7,9 +7,11 @@ import type { AppProps } from 'next/app'; import dynamic from 'next/dynamic'; import Head from 'next/head'; import Router, { useRouter } from 'next/router'; +import NextAdapterPages from 'next-query-params/pages'; import { NextSeo } from 'next-seo'; import { useEffect } from 'react'; import { RecoilRoot } from 'recoil'; +import { QueryParamProvider } from 'use-query-params'; import ResponsiveProvider from '@/components/common/Responsive/ResponsiveProvider'; import ToastProvider from '@/components/common/Toast/providers/ToastProvider'; @@ -57,21 +59,24 @@ function MyApp({ Component, pageProps }: AppProps) { - - - import('framer-motion').then((mod) => mod.domAnimation)}> - - - - - - - - {DEBUG && } - - - - + + + + + import('framer-motion').then((mod) => mod.domAnimation)}> + + + + + + + + {DEBUG && } + + + + + ); diff --git a/src/pages/community/index.tsx b/src/pages/community/index.tsx new file mode 100644 index 000000000..bf0331ebb --- /dev/null +++ b/src/pages/community/index.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react'; + +import CommunityPage from '@/components/community/page'; +import { setLayout } from '@/utils/layout'; + +const Community: FC = () => { + return ; +}; + +setLayout(Community, 'header'); + +export default Community; diff --git a/yarn.lock b/yarn.lock index bc8dc2b67..94e1333d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15033,6 +15033,19 @@ __metadata: languageName: node linkType: hard +"next-query-params@npm:^4.2.3": + version: 4.2.3 + resolution: "next-query-params@npm:4.2.3" + dependencies: + tslib: ^2.0.3 + peerDependencies: + next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-query-params: ^2.0.0 + checksum: 19495b4cac190f8dc3b43e241930e82af7bc2aa88a47fbcf78b5ec654954ddcd14994dff6362734b619d0385ddbaa8309ccc60b6e2f13ea897c2f119f239cc27 + languageName: node + linkType: hard + "next-seo@npm:^6.1.0": version: 6.1.0 resolution: "next-seo@npm:6.1.0" @@ -17469,6 +17482,13 @@ __metadata: languageName: node linkType: hard +"serialize-query-params@npm:^2.0.2": + version: 2.0.2 + resolution: "serialize-query-params@npm:2.0.2" + checksum: 6dccdbd68cbec44c99599c4f77968e8685a74cc597f473ed93b15a8d03c3a67f50c958af9af3d186e64aabd7f98b59d674a05aa4e69d997ed86a5478cf133b27 + languageName: node + linkType: hard + "serve-favicon@npm:^2.5.0": version: 2.5.0 resolution: "serve-favicon@npm:2.5.0" @@ -17756,6 +17776,7 @@ __metadata: msw-storybook-addon: ^1.8.0 nanoid: ^4.0.0 next: ^13.1.1 + next-query-params: ^4.2.3 next-seo: ^6.1.0 postcss-html: ^1.4.1 postcss-syntax: ^0.36.2 @@ -17783,6 +17804,7 @@ __metadata: tsup: ^7.1.0 typescript: ^4.9.5 url-loader: ^4.1.1 + use-query-params: ^2.2.1 webpack: ^5.72.0 yup: ^1.0.0 zod: ^3.20.6 @@ -19468,6 +19490,25 @@ __metadata: languageName: node linkType: hard +"use-query-params@npm:^2.2.1": + version: 2.2.1 + resolution: "use-query-params@npm:2.2.1" + dependencies: + serialize-query-params: ^2.0.2 + peerDependencies: + "@reach/router": ^1.2.1 + react: ">=16.8.0" + react-dom: ">=16.8.0" + react-router-dom: ">=5" + peerDependenciesMeta: + "@reach/router": + optional: true + react-router-dom: + optional: true + checksum: 5bf16511a561cf09902c3526950c6c88999767a1990619a322685448abbd09f5d71a17289d0f727bb3e68d5baced36b3fc0bbf855664b3dd5046d5d553204d28 + languageName: node + linkType: hard + "use-resize-observer@npm:^9.1.0": version: 9.1.0 resolution: "use-resize-observer@npm:9.1.0"