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 = (
+
+ );
+
+ 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"