diff --git a/public/svgs/Img_Banner.svg b/public/svgs/Img_Banner.svg
new file mode 100644
index 00000000..da93c9cc
--- /dev/null
+++ b/public/svgs/Img_Banner.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/svgs/ic_bookmark.svg b/public/svgs/ic_bookmark.svg
new file mode 100644
index 00000000..aba55af2
--- /dev/null
+++ b/public/svgs/ic_bookmark.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/svgs/ic_chevron.svg b/public/svgs/ic_chevron.svg
new file mode 100644
index 00000000..2a7dc6ec
--- /dev/null
+++ b/public/svgs/ic_chevron.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/svgs/ic_comment_gray_24.svg b/public/svgs/ic_comment_gray_24.svg
index 114171e2..a2314e29 100644
--- a/public/svgs/ic_comment_gray_24.svg
+++ b/public/svgs/ic_comment_gray_24.svg
@@ -1,3 +1,3 @@
diff --git a/public/svgs/img_textlogo.svg b/public/svgs/img_textlogo.svg
new file mode 100644
index 00000000..5afdef9f
--- /dev/null
+++ b/public/svgs/img_textlogo.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/img_banner_attached.png b/src/assets/images/img_banner_attached.png
new file mode 100644
index 00000000..7a77d225
Binary files /dev/null and b/src/assets/images/img_banner_attached.png differ
diff --git a/src/assets/svgs/BtnWritingChipxIcon.tsx b/src/assets/svgs/BtnWritingChipxIcon.tsx
index e1262c44..ae80abd4 100644
--- a/src/assets/svgs/BtnWritingChipxIcon.tsx
+++ b/src/assets/svgs/BtnWritingChipxIcon.tsx
@@ -1,9 +1,15 @@
+import * as React from 'react';
import type { SVGProps } from 'react';
const SvgBtnWritingChipxIcon = (props: SVGProps) => (
);
export default SvgBtnWritingChipxIcon;
diff --git a/src/assets/svgs/IcAlarmBlack24.tsx b/src/assets/svgs/IcAlarmBlack24.tsx
index 5c94c9c6..a392a185 100644
--- a/src/assets/svgs/IcAlarmBlack24.tsx
+++ b/src/assets/svgs/IcAlarmBlack24.tsx
@@ -1,3 +1,4 @@
+import * as React from 'react';
import type { SVGProps } from 'react';
const SvgIcAlarmBlack24 = (props: SVGProps) => (
diff --git a/src/assets/svgs/IcArrowDownBlack24.tsx b/src/assets/svgs/IcArrowDownBlack24.tsx
index d38dca16..7a224153 100644
--- a/src/assets/svgs/IcArrowDownBlack24.tsx
+++ b/src/assets/svgs/IcArrowDownBlack24.tsx
@@ -1,3 +1,4 @@
+import * as React from 'react';
import type { SVGProps } from 'react';
const SvgIcArrowDownBlack24 = (props: SVGProps) => (
diff --git a/src/assets/svgs/IcBookmark.tsx b/src/assets/svgs/IcBookmark.tsx
new file mode 100644
index 00000000..1a6bf763
--- /dev/null
+++ b/src/assets/svgs/IcBookmark.tsx
@@ -0,0 +1,13 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+
+const SvgIcBookmark = (props: SVGProps) => (
+
+);
+export default SvgIcBookmark;
diff --git a/src/assets/svgs/IcChevron.tsx b/src/assets/svgs/IcChevron.tsx
new file mode 100644
index 00000000..00e43ac5
--- /dev/null
+++ b/src/assets/svgs/IcChevron.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+
+const SvgIcChevron = (props: SVGProps) => (
+
+);
+export default SvgIcChevron;
diff --git a/src/assets/svgs/IcCommentGray24.tsx b/src/assets/svgs/IcCommentGray24.tsx
index 94b58ea6..27dd6f3a 100644
--- a/src/assets/svgs/IcCommentGray24.tsx
+++ b/src/assets/svgs/IcCommentGray24.tsx
@@ -4,10 +4,9 @@ import type { SVGProps } from 'react';
const SvgIcCommentGray24 = (props: SVGProps) => (
);
diff --git a/src/assets/svgs/IcInstaGray20.tsx b/src/assets/svgs/IcInstaGray20.tsx
index 4189fb90..a38d67e1 100644
--- a/src/assets/svgs/IcInstaGray20.tsx
+++ b/src/assets/svgs/IcInstaGray20.tsx
@@ -1,3 +1,4 @@
+import * as React from 'react';
import type { SVGProps } from 'react';
const SvgIcInstaGray20 = (props: SVGProps) => (
diff --git a/src/assets/svgs/ImgBanner.tsx b/src/assets/svgs/ImgBanner.tsx
new file mode 100644
index 00000000..6941049d
--- /dev/null
+++ b/src/assets/svgs/ImgBanner.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+
+const SvgImgBanner = (props: SVGProps) => (
+
+);
+export default SvgImgBanner;
diff --git a/src/assets/svgs/ImgDarudalogo40.tsx b/src/assets/svgs/ImgDarudalogo40.tsx
index 6f2d2a92..b81a3c72 100644
--- a/src/assets/svgs/ImgDarudalogo40.tsx
+++ b/src/assets/svgs/ImgDarudalogo40.tsx
@@ -1,3 +1,4 @@
+import * as React from 'react';
import type { SVGProps } from 'react';
const SvgImgDarudalogo40 = (props: SVGProps) => (
diff --git a/src/assets/svgs/ImgTextlogo.tsx b/src/assets/svgs/ImgTextlogo.tsx
new file mode 100644
index 00000000..771d63cf
--- /dev/null
+++ b/src/assets/svgs/ImgTextlogo.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+
+const SvgImgTextlogo = (props: SVGProps) => (
+
+);
+export default SvgImgTextlogo;
diff --git a/src/assets/svgs/index.ts b/src/assets/svgs/index.ts
index fb789262..ee8dcd4d 100644
--- a/src/assets/svgs/index.ts
+++ b/src/assets/svgs/index.ts
@@ -1,18 +1,21 @@
+export { default as ImgBanner } from './ImgBanner';
+export { default as BtnWritingChipxIcon } from './BtnWritingChipxIcon';
export { default as IcAlarmBlack24 } from './IcAlarmBlack24';
export { default as IcArrowDownBlack24 } from './IcArrowDownBlack24';
-export { default as IcInstaGray20 } from './IcInstaGray20';
-export { default as ImgDarudalogo40 } from './ImgDarudalogo40';
-export { default as ImgModalcheck } from './ImgModalcheck';
-export { default as ImgModalexit } from './ImgModalexit';
-export { default as ImgModalexit2 } from './ImgModalexit2';
-export { default as BtnWritingChipxIcon } from './BtnWritingChipxIcon';
-export { default as IcOverflowGray24 } from './IcOverflowGray24';
-export { default as IcOverflowGray44 } from './IcOverflowGray44';
+export { default as IcBookmark } from './IcBookmark';
export { default as IcBookmark32 } from './IcBookmark32';
-
export { default as IcBookmarkGray24Dact } from './IcBookmarkGray24Dact';
+export { default as IcChevron } from './IcChevron';
export { default as IcCmtimgGray24 } from './IcCmtimgGray24';
export { default as IcCommentGray24 } from './IcCommentGray24';
+export { default as IcInstaGray20 } from './IcInstaGray20';
+export { default as IcOverflowGray24 } from './IcOverflowGray24';
+export { default as IcOverflowGray44 } from './IcOverflowGray44';
export { default as IcPlusWhite20 } from './IcPlusWhite20';
export { default as IcShareGray24 } from './IcShareGray24';
+export { default as ImgDarudalogo40 } from './ImgDarudalogo40';
+export { default as ImgModalcheck } from './ImgModalcheck';
+export { default as ImgModalexit } from './ImgModalexit';
+export { default as ImgModalexit2 } from './ImgModalexit2';
+export { default as ImgTextlogo } from './ImgTextlogo';
export { default as ImgUploadWhite48 } from './ImgUploadWhite48';
diff --git a/src/components/common/button/circleButton/CircleButton.styled.ts b/src/components/common/button/circleButton/CircleButton.styled.ts
index c96d05e0..f117b769 100644
--- a/src/components/common/button/circleButton/CircleButton.styled.ts
+++ b/src/components/common/button/circleButton/CircleButton.styled.ts
@@ -27,7 +27,7 @@ export const ButtonWrapper = styled.button<{
`,
small: css`
gap: 1.2rem;
- padding: 1.6rem 3.6rem;
+ padding: 1.6rem 3rem;
${theme.fonts.body_20_b};
border-radius: 3.2rem;
diff --git a/src/components/common/button/squareButton/SquareButton.styled.ts b/src/components/common/button/squareButton/SquareButton.styled.ts
index fb40bc52..5904aeb3 100644
--- a/src/components/common/button/squareButton/SquareButton.styled.ts
+++ b/src/components/common/button/squareButton/SquareButton.styled.ts
@@ -43,12 +43,20 @@ export const ButtonWrapper = styled.button<{
}
}}
+ &:hover span svg path {
+ stroke: ${({ theme }) => theme.colors.white1};
+ }
+
&:hover {
color: ${({ theme }) => theme.colors.white1};
background-color: ${({ theme }) => theme.colors.iris1};
}
+ &:active span svg path {
+ stroke: ${({ theme }) => theme.colors.white1};
+ }
+
&:active {
color: ${({ theme }) => theme.colors.white1};
diff --git a/src/components/common/dropdown/DropDown.stories.tsx b/src/components/common/dropdown/DropDown.stories.tsx
index 50ed33c9..2d5389a7 100644
--- a/src/components/common/dropdown/DropDown.stories.tsx
+++ b/src/components/common/dropdown/DropDown.stories.tsx
@@ -107,7 +107,7 @@ export const TopList = DropDownTemplate.bind({});
TopList.args = {
children: (
<>
-
+
{
alert('첫번째 클릭!');
diff --git a/src/components/common/dropdown/DropDown.styled.ts b/src/components/common/dropdown/DropDown.styled.ts
index fe7c133b..e9cb32af 100644
--- a/src/components/common/dropdown/DropDown.styled.ts
+++ b/src/components/common/dropdown/DropDown.styled.ts
@@ -8,16 +8,33 @@ export const DropDownContainer = styled.section<{ $position: 'start' | 'end' }>`
align-items: ${({ $position }) => $position};
`;
-export const DropDownToggleBtn = styled.div`
+export const DropDownToggleBtn = styled.button<{ $isOpen: boolean }>`
cursor: pointer;
svg {
vertical-align: middle;
}
+
+ ${({ $isOpen, theme }) =>
+ $isOpen &&
+ `
+ border-radius: 0.8rem;
+ background: ${theme.colors.gray4};
+`}
+
+ &:hover {
+ background: ${({ theme }) => theme.colors.gray4};
+ border-radius: 0.8rem;
+ }
+
+ &:hover svg path {
+ fill: ${({ theme, $isOpen }) => ($isOpen ? theme.colors.gray1 : theme.colors.white1)};
+ }
`;
-export const DropDownWrapper = styled.ul`
- position: abolsute;
+export const DropDownWrapper = styled.ul<{ $display: 'top' | 'bottom' }>`
+ position: absolute;
+ ${({ $display }) => $display}: 4.5rem;
z-index: 1;
width: min-content;
diff --git a/src/components/common/dropdown/DropDown.tsx b/src/components/common/dropdown/DropDown.tsx
index d9e85d4c..7e903ab8 100644
--- a/src/components/common/dropdown/DropDown.tsx
+++ b/src/components/common/dropdown/DropDown.tsx
@@ -38,15 +38,20 @@ const ToggleBtn = ({ children }: { children: ReactNode }) => {
setIsOpen(!isOpen);
};
- return {children};
+ return (
+
+ {children}
+
+ );
};
-const Content = ({ children }: { children: ReactNode }) => {
+// $diplay: 'bottom'의 경우, 드롭다운이 위로 펼쳐지는 경우 입니다.
+const Content = ({ children, $display = 'top' }: { children: ReactNode; $display?: 'top' | 'bottom' }) => {
const { isOpen } = useComponentContext(DropDownContext, 'DropDown');
- if (!isOpen) return;
+ if (!isOpen) return null;
- return {children};
+ return {children};
};
const Item = ({ status, onClick, children }: DropDownItemPropType) => {
diff --git a/src/components/common/modal/AlertModal.tsx b/src/components/common/modal/AlertModal.tsx
index 030173cc..f75490c6 100644
--- a/src/components/common/modal/AlertModal.tsx
+++ b/src/components/common/modal/AlertModal.tsx
@@ -10,10 +10,10 @@ interface AlertModalProps {
ImgPopupModal: FunctionComponent>;
isSingleModal: boolean;
- singleBtnContent: string;
+ singleBtnContent?: string;
- modalContent: string;
- DobblebtnProps: {
+ modalContent?: string;
+ DobblebtnProps?: {
isPrimaryRight: boolean;
primaryBtnContent: string;
secondaryBtnContent: string;
diff --git a/src/components/common/modal/component/ModalWrapper.tsx b/src/components/common/modal/component/ModalWrapper.tsx
index 741cc7db..a2e8e24c 100644
--- a/src/components/common/modal/component/ModalWrapper.tsx
+++ b/src/components/common/modal/component/ModalWrapper.tsx
@@ -24,13 +24,15 @@ export default ModalWrapper;
const S = {
ModalWrapper: styled.dialog`
+ position: fixed;
top: 0;
+ left: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
- width: 100vw;
- height: 100vh;
+ width: 100%;
+ height: 100%;
padding-top: 3.1rem;
overflow: auto;
@@ -44,7 +46,7 @@ const S = {
align-items: center;
justify-content: flex-end;
width: 40rem;
- height: auto;
+ margin: auto;
padding: ${({ $isSingleModal }) => ($isSingleModal ? '4.4rem 4.8rem 2.8rem 4.8rem' : '3.1rem 0 0 0')};
background: ${({ theme }) => theme.colors.white1};
diff --git a/src/pages/community/Community.stories.tsx b/src/pages/community/Community.stories.tsx
new file mode 100644
index 00000000..5a353e48
--- /dev/null
+++ b/src/pages/community/Community.stories.tsx
@@ -0,0 +1,31 @@
+import Footer from '@components/footer/Footer';
+import Header from '@components/header/Header';
+import { HEADER_STATE, HeaderState } from '@constants/headerState';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import Community from './Community';
+
+const headerState: HeaderState = HEADER_STATE.LOGGED_IN;
+
+const meta: Meta = {
+ title: 'Pages/Community',
+ component: Community,
+ parameters: {
+ layout: 'fullscreen',
+ },
+ decorators: [
+ (Story) => (
+ <>
+
+
+
+ >
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/pages/community/Community.style.ts b/src/pages/community/Community.style.ts
new file mode 100644
index 00000000..a8aedab0
--- /dev/null
+++ b/src/pages/community/Community.style.ts
@@ -0,0 +1,50 @@
+import styled from '@emotion/styled';
+
+export const CommunityWrapper = styled.section`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ background-color: ${({ theme }) => theme.colors.white2};
+`;
+
+export const CommunityContainer = styled.section`
+ display: inline-flex;
+ gap: 1.4rem;
+ align-items: flex-start;
+ justify-content: center;
+`;
+
+export const CardList = styled.ul`
+ display: flex;
+ flex-direction: column;
+ gap: 1.2rem;
+ width: 76.6rem;
+ margin: 2.4rem 0;
+`;
+
+export const FollowingBtns = styled.div`
+ position: fixed;
+ top: 60.9rem;
+ right: 8.7rem;
+ z-index: 999;
+ display: inline-flex;
+ flex-direction: column;
+ gap: 1.4rem;
+ align-items: flex-end;
+ justify-content: flex-end;
+`;
+
+export const TopBtn = styled.button`
+ display: flex;
+ gap: 1.2rem;
+ align-items: center;
+ justify-content: center;
+ width: 5.6rem;
+ height: 5.6rem;
+
+ background: ${({ theme }) => theme.colors.black_toast};
+ box-shadow: 0 0 12px 0 ${({ theme }) => theme.colors.shadow1};
+ border-radius: 3.2rem;
+`;
diff --git a/src/pages/community/Community.tsx b/src/pages/community/Community.tsx
index ab2c7bf6..dcf7f3ca 100644
--- a/src/pages/community/Community.tsx
+++ b/src/pages/community/Community.tsx
@@ -1,5 +1,40 @@
+import { IcPlusWhite20, IcChevron } from '@assets/svgs';
+import CircleButton from '@components/button/circleButton/CircleButton';
+
+import * as S from './Community.style';
+import Banner from './components/banner/Banner';
+import Card from './components/card/Card';
+import { POST_DATA } from './mocks';
+
const Community = () => {
- return Community
;
+ const handleScrollUp = () => {
+ if (!window.scrollY) return;
+
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ };
+ return (
+
+
+
+
+ {POST_DATA.map((post) => (
+
+ ))}
+
+
+
+ }>
+ 글쓰기
+
+
+
+
+
+
+ );
};
export default Community;
diff --git a/src/pages/community/components/banner/Banner.stories.tsx b/src/pages/community/components/banner/Banner.stories.tsx
new file mode 100644
index 00000000..d5e158d8
--- /dev/null
+++ b/src/pages/community/components/banner/Banner.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import Banner from './Banner';
+
+const meta: Meta = {
+ title: 'Components/Community Banner',
+ component: Banner,
+ parameters: {
+ layout: 'fullscreen',
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/pages/community/components/banner/Banner.styles.ts b/src/pages/community/components/banner/Banner.styles.ts
new file mode 100644
index 00000000..aa3824c2
--- /dev/null
+++ b/src/pages/community/components/banner/Banner.styles.ts
@@ -0,0 +1,50 @@
+import styled from '@emotion/styled';
+
+export const BannerWrapper = styled.section`
+ display: flex;
+ justify-content: flex-start;
+ width: 100%;
+ height: 19.9rem;
+
+ background: ${({ theme }) => theme.colors.orange1};
+`;
+
+export const MainImgBanner = styled.img`
+ width: 51.4rem;
+ margin: 2.6rem 0 0 18.6rem;
+`;
+
+export const BannerContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+ align-items: flex-start;
+ justify-content: center;
+ padding: 5.2rem 0 5.2rem 6.4rem;
+`;
+export const BannerTitle = styled.h1`
+ ${({ theme }) => theme.fonts.body_20_b};
+ color: ${({ theme }) => theme.colors.white1};
+`;
+
+export const ModalChipsContainer = styled.ul`
+ display: flex;
+ gap: 1rem;
+ width: max-content;
+`;
+
+export const ModalChip = styled.li`
+ display: flex;
+ gap: 0.2rem;
+ align-items: center;
+ justify-content: center;
+ padding: 0.4rem 1.2rem;
+
+ background: ${({ theme }) => theme.colors.white1};
+ border-radius: 2.8rem;
+`;
+
+export const ModalChipText = styled.p`
+ ${({ theme }) => theme.fonts.caption_12_b};
+ color: ${({ theme }) => theme.colors.gray1};
+`;
diff --git a/src/pages/community/components/banner/Banner.tsx b/src/pages/community/components/banner/Banner.tsx
new file mode 100644
index 00000000..9f65e6b6
--- /dev/null
+++ b/src/pages/community/components/banner/Banner.tsx
@@ -0,0 +1,28 @@
+import imgBanner from '@assets/images/img_banner_attached.png';
+import { ImgTextlogo } from '@assets/svgs';
+import { BANNER_CHIP } from '@pages/community/constants/constants';
+
+import * as S from './Banner.styles';
+
+const Banner = () => {
+ return (
+
+
+
+
+ 자유로운 정보 공유,
+ 커뮤니티에서 살펴보세요.
+
+
+ {BANNER_CHIP.map((item, i) => (
+
+ {item.text}
+
+ ))}
+
+
+
+ );
+};
+
+export default Banner;
diff --git a/src/pages/community/components/card/Card.stories.ts b/src/pages/community/components/card/Card.stories.ts
new file mode 100644
index 00000000..79944661
--- /dev/null
+++ b/src/pages/community/components/card/Card.stories.ts
@@ -0,0 +1,138 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import Card from './Card';
+
+const meta: Meta = {
+ title: 'components/Card',
+ component: Card,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const NoImages: Story = {
+ args: {
+ post: {
+ boardId: 0,
+ toolId: 0,
+ toolName: 'ChatGPT',
+ toolLogo:
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: [],
+ updatedAt: '2024-12-21',
+ nickName: '내이름은고은',
+ commentCount: 63,
+ },
+ },
+};
+
+export const OneImage: Story = {
+ args: {
+ post: {
+ boardId: 1,
+ toolId: 1,
+ toolName: 'ChatGPT',
+ toolLogo:
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요',
+ content: '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: ['https://placehold.co/600x400'],
+ updatedAt: '2024-12-21',
+ nickName: '내이름은고은',
+ commentCount: 63,
+ },
+ },
+};
+
+export const TwoImages: Story = {
+ args: {
+ post: {
+ boardId: 2,
+ toolId: 2,
+ toolName: 'ChatGPT',
+ toolLogo:
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요',
+ content: '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: ['https://placehold.co/600x400', 'https://placehold.co/600x400'],
+ updatedAt: '2024-12-21',
+ nickName: '내이름은고은',
+ commentCount: 63,
+ },
+ },
+};
+
+export const ThreeImages: Story = {
+ args: {
+ post: {
+ boardId: 3,
+ toolId: 3,
+ toolName: 'ChatGPT',
+ toolLogo:
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요',
+ content: '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: ['https://placehold.co/600x400', 'https://placehold.co/600x400', 'https://placehold.co/600x400'],
+ updatedAt: '2024-12-21',
+ nickName: '내이름은고은',
+ commentCount: 63,
+ },
+ },
+};
+
+export const FourImages: Story = {
+ args: {
+ post: {
+ boardId: 4,
+ toolId: 4,
+ toolName: 'ChatGPT',
+ toolLogo:
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: [
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ ],
+ updatedAt: '2024-12-21',
+ nickName: '내이름은고은',
+ commentCount: 63,
+ },
+ },
+};
+
+export const FiveImages: Story = {
+ args: {
+ post: {
+ boardId: 5,
+ toolId: 5,
+ toolName: 'ChatGPT',
+ toolLogo:
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: [
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ ],
+ updatedAt: '2024-12-21',
+ nickName: '내이름은고은',
+ commentCount: 63,
+ },
+ },
+};
diff --git a/src/pages/community/components/card/Card.styled.ts b/src/pages/community/components/card/Card.styled.ts
new file mode 100644
index 00000000..d1c265a2
--- /dev/null
+++ b/src/pages/community/components/card/Card.styled.ts
@@ -0,0 +1,156 @@
+import styled from '@emotion/styled';
+
+export const CardWrapper = styled.li`
+ background: ${({ theme }) => theme.colors.white1};
+ border: 1px solid ${({ theme }) => theme.colors.gray6};
+ border-radius: 1.6rem;
+`;
+
+export const CardLayout = styled.section`
+ display: inline-flex;
+ flex-direction: column;
+ gap: 2rem;
+ align-items: flex-end;
+ width: 100%;
+ padding: 3rem 4.8rem 1.6rem;
+`;
+
+export const CardTopContent = styled.section`
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
+ align-items: flex-start;
+ align-self: stretch;
+ width: 100%;
+
+ header {
+ display: flex;
+ gap: 1.2rem;
+ align-items: center;
+ }
+`;
+
+export const MetaInfo = styled.span`
+ ${({ theme }) => theme.fonts.caption_12_r};
+ display: flex;
+ gap: 0.6rem;
+ align-items: center;
+
+ color: ${({ theme }) => theme.colors.gray2};
+`;
+
+export const CardDivider = styled.div`
+ flex-shrink: 0;
+ width: 100%;
+ height: 0.1rem;
+
+ background-color: ${({ theme }) => theme.colors.gray4};
+
+ stroke: ${({ theme }) => theme.colors.gray4};
+`;
+
+export const CardBottomBar = styled.section`
+ display: flex;
+ align-items: center;
+ align-self: stretch;
+ justify-content: space-between;
+`;
+
+export const CardTitleItem = styled.h1`
+ ${({ theme }) => theme.fonts.body_20_b};
+ color: ${({ theme }) => theme.colors.black};
+ white-space: normal;
+ word-wrap: break-word;
+ word-break: break-word;
+`;
+
+export const CardTextItem = styled.pre<{ $isImgInclude: boolean }>`
+ display: -webkit-box;
+ width: 100%;
+ overflow: hidden;
+
+ ${({ theme }) => theme.fonts.caption_14_m};
+ color: ${({ theme }) => theme.colors.gray1};
+ white-space: pre-wrap;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: ${({ $isImgInclude }) => ($isImgInclude ? 2 : 4)};
+ -webkit-box-orient: vertical;
+
+ word-wrap: break-word;
+ word-break: break-word;
+`;
+
+export const BottomBarLeft = styled.span`
+ display: flex;
+ gap: 1.9rem;
+ align-items: center;
+`;
+export const ImageGrid = styled.div<{ $imageCount: number }>`
+ display: grid;
+ grid-template-rows: ${({ $imageCount }) => {
+ switch ($imageCount) {
+ case 4:
+ return 'repeat(2, 1fr)';
+ case 3:
+ return '1fr';
+ case 2:
+ return '1fr';
+ case 1:
+ return '1fr';
+ default:
+ return 'none';
+ }
+ }};
+ grid-template-columns: ${({ $imageCount }) => {
+ switch ($imageCount) {
+ case 4:
+ return 'repeat(2, 1fr)';
+ case 3:
+ return 'repeat(3, 1fr)';
+ case 2:
+ return 'repeat(2, 1fr)';
+ case 1:
+ return '1fr';
+ default:
+ return 'none';
+ }
+ }};
+ gap: 0.8rem;
+ width: 100%;
+ margin-top: 0.6rem;
+
+ ${({ $imageCount }) =>
+ $imageCount === 5 &&
+ `
+ & > *:nth-child(1) {
+ grid-column: 1 / 4;
+ grid-row: 1 / 2;
+ }
+
+ & > *:nth-of-type(2) {
+ grid-column: 4 / 7;
+ grid-row: 1 / 2;
+ }
+
+ & > *:nth-of-type(3) {
+ grid-column: 1 / 3;
+ grid-row: 2 / 3;
+ }
+
+ & > *:nth-of-type(4) {
+ grid-column: 3 / 5;
+ grid-row: 2 / 3;
+ }
+
+ & > *:nth-of-type(5) {
+ grid-column: 5 / 7;
+ grid-row: 2 / 3;
+ }
+ `}
+
+ img {
+ width: 100%;
+
+ border-radius: 0.8rem;
+ }
+`;
diff --git a/src/pages/community/components/card/Card.tsx b/src/pages/community/components/card/Card.tsx
new file mode 100644
index 00000000..0a27aba6
--- /dev/null
+++ b/src/pages/community/components/card/Card.tsx
@@ -0,0 +1,100 @@
+import { IcCommentGray24, IcBookmark, IcOverflowGray44, ImgModalexit } from '@assets/svgs';
+import SquareButton from '@components/button/squareButton/SquareButton';
+import Chip from '@components/chip/Chip';
+import DropDown from '@components/dropdown/DropDown';
+import { AlterModal } from '@components/modal';
+import { useModal } from '@pages/community/hooks';
+import { formatContent } from '@pages/community/utils';
+
+import * as S from './Card.styled';
+
+interface CardDataProp {
+ post: {
+ boardId: number;
+ toolId: number;
+ toolName: string;
+ toolLogo: string;
+ title: string;
+ content: string;
+ images: string[];
+ updatedAt: string;
+ nickName: string;
+ commentCount: number;
+ };
+}
+
+const Card = ({ post }: CardDataProp) => {
+ const { toolName, toolLogo, title, content, images, updatedAt, nickName, commentCount } = post;
+
+ const { isOpen, handleModalOpen, handleModalClose } = useModal();
+
+ return (
+
+
+
+
+
+
+
+ {toolName}
+
+
+
+ {nickName}
+ |
+ {updatedAt}
+
+
+ {title}
+ = 1}>{formatContent(content, images.length)}
+
+ {images.map((image, i) => (
+
+ ))}
+
+
+
+
+
+ } size="small" stroke={false}>{`${commentCount}개`}
+ } size="small" stroke={false}>
+ 북마크
+
+
+
+
+
+
+
+ {
+ alert('클릭!');
+ }}
+ >
+ 수정하기
+
+
+ 삭제하기
+
+
+
+
+
+
+
+ );
+};
+
+export default Card;
diff --git a/src/pages/community/constants/constants.ts b/src/pages/community/constants/constants.ts
new file mode 100644
index 00000000..84aeece4
--- /dev/null
+++ b/src/pages/community/constants/constants.ts
@@ -0,0 +1 @@
+export const BANNER_CHIP = [{ text: '# 툴 추천' }, { text: '# 툴 사용법' }, { text: '# 구체적인 궁금증' }];
diff --git a/src/pages/community/hooks/index.ts b/src/pages/community/hooks/index.ts
new file mode 100644
index 00000000..94a1b81c
--- /dev/null
+++ b/src/pages/community/hooks/index.ts
@@ -0,0 +1,3 @@
+import useModal from './useModal';
+
+export { useModal };
diff --git a/src/pages/community/hooks/useModal.ts b/src/pages/community/hooks/useModal.ts
new file mode 100644
index 00000000..cb77cd95
--- /dev/null
+++ b/src/pages/community/hooks/useModal.ts
@@ -0,0 +1,21 @@
+import { useState } from 'react';
+
+const useModal = () => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleModalOpen = () => {
+ setIsOpen(true);
+ };
+
+ const handleModalClose = () => {
+ setIsOpen(false);
+ };
+
+ return {
+ isOpen,
+ handleModalOpen,
+ handleModalClose,
+ };
+};
+
+export default useModal;
diff --git a/src/pages/community/mocks/index.ts b/src/pages/community/mocks/index.ts
new file mode 100644
index 00000000..deaefd0d
--- /dev/null
+++ b/src/pages/community/mocks/index.ts
@@ -0,0 +1,105 @@
+export const POST_DATA = [
+ {
+ boardId: 0,
+ toolId: 0,
+ toolName: 'ChatGPT',
+ toolLogo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: [
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ ],
+ updatedAt: '2024-12-21',
+ nickName: '내이름은고은',
+ commentCount: 63,
+ },
+ {
+ boardId: 1,
+ toolId: 0,
+ toolName: 'ChatGPT',
+ toolLogo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: [
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ 'https://placehold.co/600x400',
+ ],
+ updatedAt: '2024-12-21',
+ nickName: '당신이름은용기',
+ commentCount: 63,
+ },
+ {
+ boardId: 2,
+ toolId: 0,
+ toolName: 'ChatGPT',
+ toolLogo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: ['https://placehold.co/600x400', 'https://placehold.co/600x400', 'https://placehold.co/600x400'],
+ updatedAt: '2024-12-21',
+ nickName: '나는야쏘영',
+ commentCount: 63,
+ },
+ {
+ boardId: 3,
+ toolId: 0,
+ toolName: 'ChatGPT',
+ toolLogo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: ['https://placehold.co/600x400', 'https://placehold.co/600x400'],
+ updatedAt: '2024-12-21',
+ nickName: '예스찬영',
+ commentCount: 63,
+ },
+ {
+ boardId: 4,
+ toolId: 0,
+ toolName: 'ChatGPT',
+ toolLogo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: ['https://placehold.co/600x400'],
+ updatedAt: '2024-12-21',
+ nickName: '귀엽재영',
+ commentCount: 63,
+ },
+ {
+ boardId: 5,
+ toolId: 0,
+ toolName: 'ChatGPT',
+ toolLogo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일',
+ content: `대박대박대박
+ 대박대박대박대박
+ 대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대`,
+ images: [],
+ updatedAt: '2024-12-21',
+ nickName: '미니미니치치',
+ commentCount: 63,
+ },
+ {
+ boardId: 6,
+ toolId: 0,
+ toolName: 'ChatGPT',
+ toolLogo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ChatGPT-Logo.svg/2048px-ChatGPT-Logo.svg.png',
+ title: 'PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일 잘 해요PM 일',
+ content:
+ '대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박대박',
+ images: [],
+ updatedAt: '2024-12-21',
+ nickName: '안녕난또이야',
+ commentCount: 63,
+ },
+];
diff --git a/src/pages/community/types/.keep b/src/pages/community/types/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pages/community/utils/formatContent.ts b/src/pages/community/utils/formatContent.ts
new file mode 100644
index 00000000..52a27507
--- /dev/null
+++ b/src/pages/community/utils/formatContent.ts
@@ -0,0 +1,6 @@
+const formatContent = (content: string, ImgCount: number): string => {
+ const limit = ImgCount >= 1 ? 120 : 240;
+ return content.length > limit ? content.slice(0, limit) + '...' : content;
+};
+
+export default formatContent;
diff --git a/src/pages/community/utils/index.ts b/src/pages/community/utils/index.ts
new file mode 100644
index 00000000..5d5fdf77
--- /dev/null
+++ b/src/pages/community/utils/index.ts
@@ -0,0 +1,3 @@
+import formatContent from './formatContent';
+
+export { formatContent };